DB (017-verification-tags.sql): - New columns: price_verified, price_verified_eur, price_verified_url, price_verified_at - New columns: image_verified, details_verified, fully_verified, fully_verified_at - compute_transceiver_verification(uuid): per-product verification logic • price_verified: real scraped URL + price > 0 + observed in last 30 days • image_verified: R2 stored OR image_url from known vendor CDNs (flexoptix.net, fs.com, etc.), no placeholder • details_verified: product_page_url + all core fields (form_factor, speed, reach, fiber_type, part_number) populated • fully_verified: all three true simultaneously - recompute_all_verification(): bulk recompute, returns stats - Initial run: 3575 price_verified, 1173 image_verified, 1380 details_verified, 258 fully_verified - Indexes on price_verified, fully_verified for fast filtering - v_verified_products view API finder.ts: - SELECT now includes all verification fields - Response maps: price_verified, price_verified_eur, price_verified_url, image_verified, details_verified, fully_verified API health.ts: - verification block: counts + coverage percentages in /api/health Dashboard Finder: - 'Verified Price': green checkmark ✓ next to price, tooltip explains source - '100% Verified' stamp: dark green gradient badge top of card, card gets green border - 'price source ↗' link to original scraped URL - Summary bar: 'X × 100% Verified · Y with verified prices'
54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { Router, Request, Response } from "express";
|
|
import { getDbStats } from "../db/queries";
|
|
import { pool } from "../db/client";
|
|
|
|
export const healthRouter = Router();
|
|
|
|
// GET /api/health — Health check with DB stats
|
|
healthRouter.get("/", async (_req: Request, res: Response) => {
|
|
try {
|
|
const start = Date.now();
|
|
const stats = await getDbStats();
|
|
const latencyMs = Date.now() - start;
|
|
|
|
// Verification stats
|
|
const verStats = await pool.query(`
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE price_verified) AS price_verified,
|
|
COUNT(*) FILTER (WHERE image_verified) AS image_verified,
|
|
COUNT(*) FILTER (WHERE details_verified) AS details_verified,
|
|
COUNT(*) FILTER (WHERE fully_verified) AS fully_verified,
|
|
COUNT(*) AS total
|
|
FROM transceivers
|
|
`).catch(() => ({ rows: [{}] }));
|
|
const v = verStats.rows[0] || {};
|
|
|
|
res.json({
|
|
success: true,
|
|
status: "healthy",
|
|
version: "0.3.0",
|
|
uptime: process.uptime(),
|
|
database: {
|
|
connected: true,
|
|
latency_ms: latencyMs,
|
|
stats,
|
|
},
|
|
verification: {
|
|
price_verified: Number(v.price_verified || 0),
|
|
image_verified: Number(v.image_verified || 0),
|
|
details_verified: Number(v.details_verified || 0),
|
|
fully_verified: Number(v.fully_verified || 0),
|
|
total: Number(v.total || 0),
|
|
price_coverage_pct: v.total ? Math.round(Number(v.price_verified) / Number(v.total) * 100) : 0,
|
|
fully_verified_pct: v.total ? Math.round(Number(v.fully_verified) / Number(v.total) * 100) : 0,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
res.status(503).json({
|
|
success: false,
|
|
status: "unhealthy",
|
|
database: { connected: false, error: String(err) },
|
|
});
|
|
}
|
|
});
|