Decisions

ADR-054: Three mode-homes become dedicated routes — `/ship`, `/operate`, `/experiment`

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.…

#054

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:

  1. One loader for three pages. src/routes/+page.ts had 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.
  2. 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".
  3. 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.
  4. 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; /operate now 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/operate is a legitimate deep-link, not an alias.
  • Dedicated loaders: /experiment no 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. The modeStore.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.md become 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_HOMES table 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 (via load({ 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.