import type { FastifyInstance } from "fastify"; import { eq } from "drizzle-orm"; import { schema, evaluateReleaseGates, seedReleaseGates, summarizeReleaseGates } from "@app/database"; import { releaseGateUpdateSchema } from "@app/validation"; import { z } from "zod"; import { errors, parse } from "../lib/errors.js"; import { audit } from "../lib/helpers.js"; /** * Release gate administration (spec ยง17). * Lists built-in go/no-go criteria, evaluates them against analytics events, * and lets admins tune thresholds. */ export async function adminReleaseGateRoutes(app: FastifyInstance) { const admin = { preHandler: [app.requireAdmin] }; /** Seed built-in gates. Safe to call multiple times. */ app.post("/admin/v1/release-gates/seed", admin, async (req) => { await seedReleaseGates(app.db); await audit(app.db, { actorUserId: req.userId, actorType: "admin", action: "release_gates.seeded", }); return { ok: true }; }); /** Evaluate all gates now. */ app.post("/admin/v1/release-gates/evaluate", admin, async (req) => { const gates = await evaluateReleaseGates(app.db); const summary = summarizeReleaseGates(gates); await audit(app.db, { actorUserId: req.userId, actorType: "admin", action: "release_gates.evaluated", metadata: { summary }, }); return { gates, summary }; }); /** List current gate definitions without re-evaluating. */ app.get("/admin/v1/release-gates", admin, async () => { const gates = await app.db.select().from(schema.releaseGates).orderBy(schema.releaseGates.category, schema.releaseGates.gateKey); const summary = summarizeReleaseGates( gates.map((g) => ({ gateKey: g.gateKey, category: g.category, nameSv: g.nameSv, nameEn: g.nameEn, targetValue: g.targetValue, comparison: g.comparison, value: g.lastValue, status: g.status, lastEvaluatedAt: g.lastEvaluatedAt?.toISOString() ?? new Date().toISOString(), blocking: g.blocking, notes: g.notes ?? undefined, })), ); return { gates, summary }; }); /** Update gate configuration. */ app.put("/admin/v1/release-gates/:key", admin, async (req) => { const params = z.object({ key: z.string().max(64) }).parse(req.params); const input = parse(releaseGateUpdateSchema, req.body); const [existing] = await app.db .select() .from(schema.releaseGates) .where(eq(schema.releaseGates.gateKey, params.key)) .limit(1); if (!existing) throw errors.notFound("Gate finns inte."); const [updated] = await app.db .update(schema.releaseGates) .set({ ...input, updatedAt: new Date(), }) .where(eq(schema.releaseGates.gateKey, params.key)) .returning(); await audit(app.db, { actorUserId: req.userId, actorType: "admin", action: "release_gates.updated", targetType: "release_gate", targetId: params.key, metadata: input, }); return updated; }); }