/** Admin data-quality dashboard: overview, flag list, resolve / dismiss. */ import type { FastifyInstance } from "fastify"; import { z } from "zod"; import { envelope, notFound, parseBody, parseQuery } from "../../lib/http.js"; import { intParam, pageParam, strParam } from "../../lib/params.js"; import { listFlags, qualityOverview, setFlagStatus } from "../../repositories/admin/quality.js"; export async function qualityAdminRoutes(app: FastifyInstance): Promise { app.get("/quality", { schema: { summary: "Data-quality overview (QualityOverview): open flags by severity / code, largest published values, review queue, alerts" } }, async () => envelope(await qualityOverview(), { methodology: "Counts are over open quality_flags, claims by status and live (not hidden / merged) projects. 'Largest' lists are plain ORDER BY on published figures; pipeline MW per operator / metro is containment-aware (campus vs buildings never both) plus site-scoped project MW." })); app.get("/quality/flags", { schema: { summary: "Quality flags (QualityFlagDTO[]) ?status=open|resolved|dismissed|all&code=&entity_type=&min_priority=&page=&per_page=" } }, async (req) => { const q = parseQuery(z.object({ status: strParam, code: strParam, entity_type: strParam, min_priority: intParam, page: pageParam, per_page: intParam }), req.query); const res = await listFlags({ status: q.status ?? "open", code: q.code, entityType: q.entity_type, minPriority: q.min_priority, page: q.page, perPage: q.per_page }); return envelope(res.items, { total: res.total, page: res.page, perPage: res.perPage }); }); app.post("/quality/flags/:id/resolve", { schema: { summary: "Resolve a quality flag {resolution}" } }, async (req) => { const { id } = req.params as { id: string }; const body = parseBody(z.object({ resolution: z.string().min(1).max(500) }), req.body); const flag = await setFlagStatus(id, "resolved", body.resolution); if (!flag) throw notFound("quality flag"); return envelope(flag); }); app.post("/quality/flags/:id/dismiss", { schema: { summary: "Dismiss a quality flag {reason?}" } }, async (req) => { const { id } = req.params as { id: string }; const body = parseBody(z.object({ reason: z.string().max(500).optional() }), req.body ?? {}); const flag = await setFlagStatus(id, "dismissed", body.reason ?? null); if (!flag) throw notFound("quality flag"); return envelope(flag); }); }