ADR-054: Three mode-homes become dedicated routes — /ship, /operate, /experiment
Status: Accepted Date: 2026-04-22
Context
ai-frontend ADR-003 (2026-04 draft in ai-frontend/docs/decisions/003-three-modes-platform-ui.md) ruled that the Platform UI's three modes — Ship / Operate / Experiment — were a view concept, not a route concept. Every mode home rendered at /, picked by a dispatcher that read an active-mode value out of localStorage. Cmd+1 / 2 / 3 flipped the mode without navigating.
In practice this decision has cost more than it saved:
- One loader for three pages.
src/routes/+page.tshad to fetch the union of every mode's data — services, alerts, experiments — even when the user was only in Experiment mode. The common path paid every mode's latency. - Mode isn't a URL. Deep-links, tab restoration, external bookmarks, and shareable links collapsed to a single opaque
/. The operator's most common request — "send me the Operate-mode link" — was answered by "open/, then press Cmd+2". - Homepage is not a mode home. Users land at
/expecting Gyrum.ai's identity and orientation (what is this place, where do I go next), not the most-recently-used mode-specific dashboard. The dispatcher's default (Ship) arbitrarily shadowed the homepage. - Mode was a view concept, the page was not. Three very different user jobs (ship → what's live, operate → what's broken, experiment → which bet is winning) were stapled onto one URL because they shared a sidebar. Sharing chrome is not the same as sharing a page.
The UX-redesign session on 2026-04-22 produced the reversal explicitly: / is the homepage, each mode gets its own route, and the mode switcher navigates like any other link.
Decision
The Platform UI splits / into four routes:
/— Gyrum.ai identity + orientation. Headline, lede, five section cards, small "what's live now" strip. Does NOT render a mode home./ship— Ship-mode home (products, recent experiments, alerts strip)./operate— Operate-mode home (fleet health, alerts, admin surfaces). The legacy/operate → /307 is removed;/operatenow responds 200 with the home./experiment— Experiment-mode home (sortable experiments table + chart stubs).
Each route has its own +page.ts loader, fetching only the data that route renders. The mode switcher (and Cmd+1/2/3) uses MODE_HOMES['ship'] = '/ship' etc. and calls goto(path) like a normal navigation. Landing on /operate flips the mode store on mount, which rebinds <html data-mode> and re-themes var(--accent).
The mode switcher, Cmd+1/2/3 shortcuts, and mode-tokens.css palette bindings stay. What changes is the URL contract and the loader boundary.
Consequences
Easier.
- Shareable per-mode links:
https://gyrum.ai/operateis a legitimate deep-link, not an alias. - Dedicated loaders:
/experimentno longer pays the cost of fetching service-health + alerts. Each mode's page is as fast as its own data. - Homepage finally has a job: identity + orientation, not "show the default mode's fleet dashboard".
- Tab/restore/bookmark behaviour matches SvelteKit defaults — no custom "restore the last mode" code.
Harder.
- Anyone who bookmarked the ADR-003 shape (
/→ Operate home because mode was persisted to operate) will land on the homepage instead. The side-rail and mode switcher make the jump one click, but it is one click. - The
<html data-mode>attribute now flips on route mount rather than on a single dispatcher render. Any CSS that depends on mode-tokens on the first paint of a mode home must either read the persisted value synchronously (as today) or accept a one-frame flash. ThemodeStore.set(...)call on mount is synchronous so this is not visible in practice. - ADR-003's worked examples in
ai-frontend/docs/decisions/003-three-modes-platform-ui.mdbecome historical — the ADR is marked superseded by this one.
What we sign up to maintain.
- The dedicated-route contract: each new mode (if we ever add one) gets its own top-level route. The
MODE_HOMEStable is the contract. - The homepage stays calm. It is a navigation surface, not a dashboard — if it starts growing charts, that's scope-creep into Ship mode and needs a separate ADR.
Alternatives considered
- Keep ADR-003 (mode dispatcher at
/). The original choice. Loses on the four ergonomic points above; the single-URL simplicity is not worth it when each mode has a distinct data footprint and user job. - Mode as a query param (
/?mode=operate). Keeps the loader split (viaload({ url })) but makes bookmarking uglier and still leaves the homepage homeless. Rejected: if mode is URL-meaningful, it should be in the path. - Rail-only, no mode switcher. Lose the switcher + Cmd+1/2/3 entirely and treat each "mode" as just a section like Pipelines. Rejected: modes are a palette + density treatment over the same subject matter (services, experiments), not separate sections. The palette switch earns its keep.
Supersedes: ai-frontend ADR-003 (three-modes-platform-ui) — the "modes are not routes" rule is reversed.
Extended by: ADR-058 (five-mode-platform-ui) — the route-per-mode contract stays, widened from 3 to 5 (Home + Infra promoted to modes). The decisions in this ADR still hold for Ship/Operate/Experiment; ADR-058 is the source of truth for Home/Infra.