page.tsx88 lines · main
| 1 | import { TriangleAlertIcon } from '@/components/ui/triangle-alert'; |
| 2 | import { UsersIcon } from '@/components/ui/users'; |
| 3 | import { ZapIcon } from '@/components/ui/zap'; |
| 4 | |
| 5 | import { apiJson } from '@/lib/api'; |
| 6 | import { apiOrigin } from '@/lib/env'; |
| 7 | |
| 8 | import { EmptyState } from '../_components/empty-state'; |
| 9 | import { Section } from '../_components/section'; |
| 10 | import { MaintenanceControl, OpenSignupsControl } from './launch-controls'; |
| 11 | |
| 12 | export const metadata = { title: 'launch controls · admin' }; |
| 13 | export const dynamic = 'force-dynamic'; |
| 14 | |
| 15 | /** The maintenance object returned inside /v1/admin/launch-status. */ |
| 16 | interface 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. */ |
| 27 | interface 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 | */ |
| 40 | export 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 | } |