page.tsx88 lines · main
1import { TriangleAlertIcon } from '@/components/ui/triangle-alert';
2import { UsersIcon } from '@/components/ui/users';
3import { ZapIcon } from '@/components/ui/zap';
4
5import { apiJson } from '@/lib/api';
6import { apiOrigin } from '@/lib/env';
7
8import { EmptyState } from '../_components/empty-state';
9import { Section } from '../_components/section';
10import { MaintenanceControl, OpenSignupsControl } from './launch-controls';
11
12export const metadata = { title: 'launch controls · admin' };
13export const dynamic = 'force-dynamic';
14
15/** The maintenance object returned inside /v1/admin/launch-status. */
16interface MaintenanceState {
17 active: boolean;
18 scheduled: boolean;
19 upcoming: boolean;
20 startsAt: string | null;
21 endsAt: string | null;
22 message: string | null;
23 manualOverride: boolean;
24}
25
26/** The slice of /v1/admin/launch-status these two switches need. */
27interface LaunchStatus {
28 openSignups: boolean;
29 openSignupsEnvDefault: boolean;
30 maintenanceMode: boolean;
31 maintenance: MaintenanceState;
32}
33
34/**
35 * Launch controls — the two platform-wide switches that actually exist
36 * today: maintenance mode and open signups. Both read from and write to
37 * /v1/admin/launch-status/*; nothing here is decorative. Feature flags and
38 * staged rollouts get added only when their apis land.
39 */
40export default async function AdminLaunchPage() {
41 const launch = await apiJson<LaunchStatus>('/v1/admin/launch-status').catch(() => null);
42
43 return (
44 <div className="flex flex-col gap-10">
45 <header className="flex flex-col gap-2">
46 <div className="flex items-center gap-2">
47 <span className="text-[var(--color-primary)]">
48 <ZapIcon size={20} />
49 </span>
50 <h1 className="font-mono text-xl tracking-tight">launch controls</h1>
51 </div>
52 <p className="max-w-prose font-mono text-sm text-[var(--color-text-muted)]">
53 the go-live switchboard. two switches live here today — the maintenance brake and the
54 signup gate — because those are the only platform-wide controls the api actually has.
55 more switches appear when their endpoints exist, not before.
56 </p>
57 </header>
58
59 {launch === null ? (
60 <EmptyState
61 icon={<TriangleAlertIcon size={28} />}
62 title="launch status unavailable"
63 message="the api didn't answer /v1/admin/launch-status — it may be restarting or your session may have expired. reload the page to try again; the switches only render with real state, never a guess."
64 />
65 ) : (
66 <>
67 {/* ── maintenance brake ─────────────────────────────────────── */}
68 <Section title="maintenance mode" icon={<TriangleAlertIcon size={16} />}>
69 <MaintenanceControl
70 initial={launch.maintenanceMode}
71 schedule={launch.maintenance}
72 apiOrigin={apiOrigin}
73 />
74 </Section>
75
76 {/* ── signup gate ───────────────────────────────────────────── */}
77 <Section title="signups" icon={<UsersIcon size={16} />}>
78 <OpenSignupsControl
79 initialOpen={launch.openSignups}
80 envDefault={launch.openSignupsEnvDefault}
81 apiOrigin={apiOrigin}
82 />
83 </Section>
84 </>
85 )}
86 </div>
87 );
88}