import { useEffect, useMemo, useState } from "react"; import { api } from "../api.js"; type GateStatus = "pending" | "passed" | "failed" | "blocked" | "not_measurable"; interface Gate { gateKey: string; category: string; nameSv: string; nameEn: string; targetValue: number; comparison: "gte" | "lte" | "eq"; lastValue: number | null; lastEvaluatedAt: string | null; status: GateStatus; blocking: boolean; evaluationWindowDays: number; notes: string | null; } interface Summary { overall: "go" | "no_go" | "pending"; passed: number; failed: number; blocked: number; pending: number; notMeasurable: number; } function statusLabel(s: GateStatus): string { switch (s) { case "passed": return "Godkänd"; case "failed": return "Ej uppnådd"; case "blocked": return "Blockerad"; case "not_measurable": return "Ej mätbar"; default: return "Väntar"; } } function statusClass(s: GateStatus): string { switch (s) { case "passed": return "ok"; case "failed": return "err"; case "blocked": return "err"; case "not_measurable": return "warn"; default: return ""; } } function formatPct(n: number | null): string { if (n === null || Number.isNaN(n)) return "–"; return `${(n * 100).toFixed(1)}%`; } export function ReleaseGatesPage() { const [gates, setGates] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [category, setCategory] = useState("all"); const load = () => { setLoading(true); api<{ gates: Gate[]; summary: Summary }>("/admin/v1/release-gates") .then((res) => { setGates(res.gates); setSummary(res.summary); setError(null); }) .catch((e) => setError(String(e.message))) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, []); const evaluate = async () => { setLoading(true); try { const res = await api<{ gates: Gate[]; summary: Summary }>( "/admin/v1/release-gates/evaluate", { method: "POST" }, ); setGates(res.gates); setSummary(res.summary); setError(null); } catch (e) { setError(String((e as Error).message)); } finally { setLoading(false); } }; const seed = async () => { setLoading(true); try { await api("/admin/v1/release-gates/seed", { method: "POST" }); load(); } catch (e) { setError(String((e as Error).message)); } finally { setLoading(false); } }; const categories = useMemo( () => Array.from(new Set(gates.map((g) => g.category))), [gates], ); const filtered = useMemo( () => (category === "all" ? gates : gates.filter((g) => g.category === category)), [gates, category], ); return (

Release gates

{error &&
{error}
}
{summary && (
{summary.overall === "go" ? "GO" : summary.overall === "no_go" ? "NO-GO" : "Väntar"}
Övergripande beslut
{summary.passed}
Godkända
{summary.failed}
Ej uppnådda
{summary.blocked}
Blockerande
{summary.notMeasurable}
Ej mätbara
)}
{filtered.map((g) => ( ))}
Nyckel Kategori Mål Uppmätt Status Fönster Blockerar Senast utvärderad
{g.nameSv}
{g.gateKey}
{g.category} {g.comparison === "gte" ? "≥ " : g.comparison === "lte" ? "≤ " : "= "} {formatPct(g.targetValue)} {formatPct(g.lastValue)} {statusLabel(g.status)} {g.evaluationWindowDays} dagar {g.blocking ? "Ja" : "Nej"} {g.lastEvaluatedAt ? new Date(g.lastEvaluatedAt).toLocaleString("sv-SE") : "–"}
); }