page.tsx89 lines · main
1import { ShieldCheckIcon } from '@/components/ui/shield-check';
2import { TriangleAlertIcon } from '@/components/ui/triangle-alert';
3
4import { apiJson } from '@/lib/api';
5
6import { EmptyState } from '../_components/empty-state';
7import { Section } from '../_components/section';
8import { IncidentCreateForm } from './create-form';
9import { IncidentRow } from './incident-row';
10
11type Severity = 'critical' | 'major' | 'minor' | 'maintenance';
12
13interface Incident {
14 id: string;
15 startedAt: string;
16 resolvedAt: string | null;
17 severity: Severity;
18 services: readonly string[];
19 summary: string;
20 postmortem: string;
21 createdBy: string | null;
22 createdAt: string;
23 updatedAt: string;
24}
25
26export const dynamic = 'force-dynamic';
27
28function publicApiOrigin(): string {
29 return process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? '';
30}
31
32export default async function AdminIncidentsPage() {
33 const { incidents } = await apiJson<{ incidents: Incident[] }>('/v1/admin/incidents').catch(() => ({ incidents: [] as Incident[] }));
34 const open = incidents.filter((i) => i.resolvedAt === null);
35 const resolved = incidents.filter((i) => i.resolvedAt !== null);
36
37 return (
38 <div className="flex flex-col gap-10">
39 <header className="flex flex-col gap-2">
40 <div className="flex items-center gap-2">
41 <span className="text-[var(--color-primary)]">
42 <TriangleAlertIcon size={20} />
43 </span>
44 <h1 className="font-mono text-xl tracking-tight">incidents</h1>
45 </div>
46 <p className="max-w-prose font-mono text-sm text-[var(--color-text-muted)]">
47 operator-published status events. open one when something customer-impacting starts,
48 edit the narrative as it unfolds, mark resolved when restored. status page + RSS feed
49 read from this list. mutations are step-up-gated per CLAUDE.md §5.4 — the freshness
50 pill in the header is the universal re-attest path.
51 </p>
52 </header>
53
54 <IncidentCreateForm apiOrigin={publicApiOrigin()} />
55
56 <Section title={`ongoing · ${open.length}`} icon={<TriangleAlertIcon size={16} />}>
57 {open.length === 0 ? (
58 <EmptyState
59 icon={<ShieldCheckIcon size={28} />}
60 title="nothing ongoing"
61 message="the platform looks healthy."
62 />
63 ) : (
64 <ul className="flex flex-col gap-6">
65 {open.map((i) => (
66 <IncidentRow key={i.id} incident={i} apiOrigin={publicApiOrigin()} />
67 ))}
68 </ul>
69 )}
70 </Section>
71
72 <Section title={`resolved · ${resolved.length}`} icon={<ShieldCheckIcon size={16} />}>
73 {resolved.length === 0 ? (
74 <EmptyState
75 icon={<ShieldCheckIcon size={28} />}
76 title="no resolved incidents yet"
77 message="resolved incidents stay here as the public record."
78 />
79 ) : (
80 <ul className="flex flex-col gap-6">
81 {resolved.slice(0, 20).map((i) => (
82 <IncidentRow key={i.id} incident={i} apiOrigin={publicApiOrigin()} />
83 ))}
84 </ul>
85 )}
86 </Section>
87 </div>
88 );
89}