import Link from "next/link"; import { badgeVariants, type BadgeVariants } from "@/components/ui/badge"; import BrowseHeader from "@/components/BrowseHeader"; import CopyButton from "@/components/CopyButton"; import { SEVERITIES, countBySeverity, openCount, type Decision, type Severity, } from "@/lib/decisions"; import { openDecisions } from "@/lib/projects"; import { fmtAgo } from "@/lib/format"; export const dynamic = "force-dynamic"; // A STATIC segment under app/browse/, so it wins over app/browse/[song]/ -- // which would otherwise catch /browse/decisions, fail readSong() and 404. Same // trick /browse itself plays against app/[mode]/. The e2e suite asserts it, // because when this regresses the page still renders perfectly in isolation and // only the URL stops working. // // Zero client JS. The filters are links that change searchParams; nothing here // changes without a navigation, and the list is short by construction. // The three severities ARE three badge variants, one for one. That is not a // coincidence to be tidied away: the palette reserves its hues for verdicts, // and a severity is the closest thing this list has to one. const TONE: Record = { blocking: "blocking", open: "open", info: "info", }; export default async function DecisionsPage({ searchParams, }: { searchParams: Promise<{ severity?: string; kind?: string; project?: string }>; }) { const { severity, kind, project } = await searchParams; const all = await openDecisions(); const items = all.filter( (d) => (!severity || d.severity === severity) && (!kind || d.kind === kind) && (!project || d.project === project), ); const counts = countBySeverity(all); const kinds = [...new Set(all.map((d) => d.kind))].sort(); const projects = [...new Set(all.map((d) => d.project))].sort(); const byProject = new Map(); for (const d of items) { const list = byProject.get(d.project); if (list) list.push(d); else byProject.set(d.project, [d]); } const q = (next: Partial<{ severity: string; kind: string; project: string }>) => { const p = new URLSearchParams(); const merged = { severity, kind, project, ...next }; for (const [k, v] of Object.entries(merged)) if (v) p.set(k, v); const s = p.toString(); return `/browse/decisions${s ? `?${s}` : ""}`; }; return (
severity {SEVERITIES.map((s) => ( ))} project {projects.map((p) => ( ))}
kind {kinds.map((k) => ( ))}
{items.length === 0 ? (

{all.length === 0 ? "Nothing undecided. Every cut ships, every live variant is judged, every recipe still describes its file." : "Nothing matches that filter."}

) : (
{[...byProject.entries()].map(([id, list]) => (

{id} {list.filter((d) => d.severity !== "info").length} waiting of {list.length}

    {list.map((d, i) => (
  • {d.severity} {d.target} {d.why} {d.kind} {fmtAgo(d.at)}
  • ))}
))}
)}
); } function Chip({ href, on, label, tone, }: { href: string; on: boolean; label: string; tone?: BadgeVariants["variant"]; }) { return ( {label} ); }