Decisions

ADR-065: Platform UI adopts persona-aware routes + a niceties framework

ADR-054/058 gave the Platform UI a five-mode lens (Home / Ship / Operate / Infra / Experiment). ADR-059 added a second orthogonal axis — project. Both answer "what lens am I looking through?" and "which thing am I…

#065

ADR-065: Platform UI adopts persona-aware routes + a niceties framework

Status: Accepted Date: 2026-04-22 Extends: ADR-058 (five-mode-platform-ui), ADR-059 (project-first-ia), ADR-062 (design-system-and-component-strategy)

Context

ADR-054/058 gave the Platform UI a five-mode lens (Home / Ship / Operate / Infra / Experiment). ADR-059 added a second orthogonal axis — project. Both answer "what lens am I looking through?" and "which thing am I looking at?" well.

They do not answer who is asking the question. Four distinct readers open the Platform UI every day with four distinct first questions:

Reader First question Wants (high-signal list)
Dev "What's mine to do? What's blocking me?" Open PRs awaiting me, failing tests on branches I own, repos I touched recently, next-up work
SRE "What's on fire? What changed?" Active alerts (severity-ranked), services degraded/down, recent deploys with blast radius, on-call
Manager "How's the team? What ships this week?" Velocity, release schedule, cost trends, at-risk work, cross-project rollup
Boss/Owner "How's the business?" Revenue, product health, which bets are winning, spend

/ today serves the Boss reasonably well (ADR-059 §8 homepage — "What's on fire" band, KPI strip, products grid, DAG, timeline) — that was deliberate. Dev / SRE / Manager still have to synthesise their views from mode-home pages plus project pages plus the infra layer. The mode axis answers "what lens"; it is structurally silent on "who is asking".

The 2026-04-22 IA review surfaced this as the next load-bearing gap. The same review also surfaced a separate-but-adjacent complaint: the app reads as informational rather than helpful — numbers sit still, nothing proposes a next action, keyboard parity is limited to the mode switcher, and the shortcuts that do exist are not discoverable.

This ADR resolves both together because they are two halves of the same goal: make the Platform UI feel tuned to a specific reader and actively helpful at the tuning.

Decision

1. Persona-as-URL-prefix, not persona-as-toggle

Four persona routes, each a first-class home page for its reader. Mode and project axes coexist unchanged — any persona can still drill into Ship / Operate / Infra / Experiment, and any persona page retains the five-mode pill so the orthogonal axis remains one click away.

Persona Route First question answered Landing layout
Owner / "How's the business?" Current homepage (ADR-059 §8). Unchanged.
Dev /me "What's mine?" KPI strip (open PRs, reviews needed, failing tests) + two-column (PRs awaiting me / recently touched repos) + what-changed digest
SRE /ops "What's on fire?" KPI strip (alerts / services down / recent deploys) + severity-ranked alert list + degraded services + deploy timeline
Manager /team "How's the team?" KPI strip (merged PRs this week / avg cycle time / incidents) + release schedule + at-risk work. First cut uses placeholders until the velocity data source is chosen.

Each persona URL is deep-linkable, shareable, and bookmarkable. A team rotation change is "point your browser at /me every morning", not "remember which filter was set".

2. Why not a persona dropdown on one home

Rejected. Persona is cognitively orthogonal to mode, and adding a second dropdown on every mode-home surface adds a permanent tax — "which persona are these numbers for?" — on every page. Per-persona URLs stay honest: the URL is the contract.

This also matches ADR-058's mode-per-route principle. ADR-058 rejected the "mode as a persisted toggle on /" shape for the same reasons that apply here: shareability, deep-linkability, and not conflating independent axes on one surface.

3. Sidebar behaviour on persona routes

Per ADR-058 §2, the sidebar is mode-dependent. Persona routes live under Home mode — they are a first-class fleet-wide surface, not a separate mode. The Home mode sidebar gains three entries (My work, Ops, Team) alongside the existing five platform actions. Mode-tinting rules are unchanged; the palette is calm slate across all four personas.

modeFromPath('/me'), modeFromPath('/ops'), and modeFromPath('/team') all return home. The mode switcher's active-mode underline stays on Home.

4. Niceties framework

Seven opinionated niceties that make the UI feel helpful rather than informational. Each is small (≤1 day of work) and independently shippable; the framework is the commitment to land them as a set and hold them to a quality bar.

# Nicety Problem it solves Implementation sketch Scope
1 Keyboard shortcuts Mode switch is keyboard-driven (Cmd+1..5); personas, search, shortcut help are not Global keydown module ($lib/shortcuts/shortcuts.ts) with chorded g h / g m / g o / g t / g s / g i / g e / g p, ?, /. Drops typing-in-inputs per standard guard S
2 Command palette (⌘K / Ctrl+K) Jumping to any project / PR / service / runbook is a multi-click navigation today Modal with fuzzy filter over /api/v1/projects + /api/v1/infra/raw/repos + /api/v1/infra/raw/services + a static action registry ("deploy X", "audit Y") M
3 Attention-directing badges ModeSwitcher buttons have no signal when a mode genuinely needs attention count prop on each mode button; red dot when > 0. Initial wiring: Operate glows during active alerts S
4 Useful empty states Today's empty-state audit is inconsistent — some blank tables, some dashed boxes, some illustrations Apply ADR-062 / visual-language.md §3.7: every list handles empty with calm copy + §6.2 dashed box or §3.7 illustration, never a blank <tbody> S
5 Time-comparison deltas KPI tiles render absolute numbers with no context ("12 PRs" vs what?) Every KPI returns {current, yesterday, delta} via the kpi_snapshots table ADR-059 §10 introduced; tile renders ↑ 3 vs yesterday chip M (needs backend snapshot cron)
6 Contextual quick-actions Project pages are informational — no one-click "run audit" / "open latest PR" / "view catalog diff" Action strip below ProjectHeader; actions come from a per-project capability map (audit? deploy? catalog?) M
7 What-changed digest (on /me) No summary of "what happened since I last looked" anywhere localStorage lastVisitedAt timestamp per route; digest region on /me renders "3 new alerts, 2 PRs merged, 1 deploy" since that stamp S

Principles the framework holds:

  • Calm by default, loud only when actionable. A nicety that shouts gets rejected — see attention-badges (#3): the red dot appears only when count > 0, and it follows the 0.24s cubic-bezier attention curve from visual-language.md §2.3, wrapped in prefers-reduced-motion: no-preference.
  • Every nicety has a keyboard path. Command palette opens on ⌘K, shortcuts overlay on ?, focus search on /. Mouse-only niceties fail review.
  • Every nicety respects reduced motion. Badges, palette opens, digest animations all wrap their motion in @media (prefers-reduced-motion: no-preference).
  • Every nicety is discoverable. Shortcuts print on the relevant button; the shortcut overlay lists them all; the command palette lists its shortcut on hover of the first row.

5. Data sources for personas

Personas do not introduce new backend endpoints. They consume what already exists:

Persona Sources used
Owner (/) Unchanged — ADR-059 §8
Dev (/me) /api/v1/infra/raw/pulls (TBD — see §8), /api/v1/infra/raw/repos (ordered by pushed_days_ago), /api/v1/operate/alerts filtered client-side to "assigned to me" when author identity is known
SRE (/ops) /api/v1/operate/alerts (severity-ranked), /api/v1/operate/service-health (filtered to down/degraded), /api/v1/infra/raw/services (recently-changed)
Manager (/team) Placeholder until velocity source decision lands

6. Operator identity

Persona filtering on /me needs an "operator identity" — the GitHub handle to filter PRs and alerts to. First shipping cut: hard-coded to jonathanleahy with a TODO pointing at a follow-up config / auth-source decision. A future ADR (065-follow-up) binds this to whichever auth surface ships first (public session cookie, a local-only env var, or a /api/v1/me stub that the backend fills from the host OS user).

7. Homepage relationship

/ stays the Owner view — unchanged. The homepage gains three new entries in its sidebar (My work / Ops / Team) so Dev / SRE / Manager find their home from the Owner home without memorising a URL.

8. Follow-up tickets

Explicit, time-boxed follow-ups (tracked in GitHub issues after this ADR lands):

  1. Backend kpi_snapshots table + cron for Nicety #5 (time-comparison deltas). ADR-059 §10 already anticipated this.
  2. GitHub-proxy /api/v1/infra/raw/pulls (or /api/v1/ship/pulls) for Dev's "PRs awaiting me" list. Noted in /ship/pulls code as backendPending: true — same endpoint.
  3. Velocity data source decision for /team — the placeholder remains until the source is picked.
  4. Operator-identity binding (replace hard-coded handle).
  5. Command palette (Nicety #2), quick-actions (#6), digest (#7), empty states (#4) — each is a standalone PR after the scaffolding lands.

Consequences

Easier.

  • Each reader's first question is answered in one click. No synthesis, no filter gymnastics.
  • Personas scale — a fifth reader (e.g. "Auditor") gets a new route, not an overloaded homepage.
  • Persona URLs are shareable — the operator can send a link and the recipient sees the same surface.
  • Niceties are first-class. Their shared framework (keyboard parity, reduced-motion, calm-by-default) stops them from drifting into shouting or inconsistency.

Harder.

  • Four persona homes to keep fresh as infra evolves. A missed update on /ops is worse than a missed update on /ship because the reader expected truth.
  • URL-shape commitment — renaming /me later is a migration, not a refactor.
  • localStorage.lastVisitedAt is browser-local — it does not survive incognito, and it silently differs across the operator's devices. The digest is best-effort, documented as such.
  • Niceties framework is a promise to maintain a bar across seven small things. Drift on any one (a nicety that ships loud, or without reduced-motion, or without keyboard parity) erodes the whole set.

What we sign up to maintain.

  • Persona pages as living surfaces. A persona page whose data is 6 months stale is worse than no persona page.
  • The niceties bar — keyboard parity, reduced-motion, discoverability. New niceties earn the bar; old ones hold it.
  • The Home-mode classification of persona routes. If a persona ever outgrows its mode, it earns its own mode (ADR-058 contract).
  • The homepage sidebar entries for My work / Ops / Team — stay in sync with this ADR's persona model.

Alternatives considered

  • Dropdown persona toggle on one home. Rejected — see §2. Adds a second axis on every surface.
  • Separate product for each persona (different app at dev.gyrum.ai / ops.gyrum.ai). Rejected — too much infrastructure; shares no chrome; loses the "one URL for the platform" promise.
  • Role-based auth + conditional render on one home. Rejected as v1 — role is a future problem, and "conditional render" is how homepages get bloated. Separate routes stay honest even as roles arrive later.
  • Ship niceties without the persona work. Considered. Rejected — the niceties (what-changed digest, time-deltas, command palette action registry) all want a persona-aware home to live on. The two pieces are mutually reinforcing.
  • Add a sixth mode = Persona. Rejected — Cmd+1..5 is the five-mode muscle-memory contract (ADR-058 §1.5); persona is not a mode, it is a first-question.

Migration path

Small PRs, each independently shippable. First three land before this ADR's PR merges so the ADR ships with concrete proof.

  1. Scaffold /me — minimum "My PRs across fleet" + "Recently touched repos" + KPI strip. Empty state when data is null.
  2. Scaffold /ops — severity-ranked alerts + down services + KPI strip. Empty state "Everything's green." when no alerts.
  3. Scaffold /team — placeholder page until velocity source is picked.
  4. Niceties slice A (highest-leverage two): keyboard shortcuts (Nicety #1) + attention-directing badges (Nicety #3).
  5. Niceties slice B: command palette (Nicety #2), empty-state polish (Nicety #4).
  6. Niceties slice C: time-deltas (Nicety #5 — gated on backend snapshot).
  7. Niceties slice D: quick-actions (Nicety #6), what-changed digest (Nicety #7).
  8. Sidebar entries — add My work / Ops / Team to Home-mode sidebar. Ships with slice 1-2 so the routes are reachable without manual URL entry.

Steps 1, 2, 4, and 8 ship in a single ai-frontend PR alongside this ADR (scope ≤ 3000 reviewable lines). Steps 3 and 5-7 are follow-ups.

Related ADRs

  • ADR-058: 5-mode UI — persona routes live under Home mode; Cmd+1..5 unchanged.
  • ADR-059: project-first IA — ProjectHeader-quick-actions (Nicety #6) hang off project pages. Niceties #5's delta backing also depends on ADR-059 §10's kpi_snapshots.
  • ADR-060: catalog-driven infra — "View catalog diff" is a Nicety #6 quick-action on project pages that own catalogs.
  • ADR-062: design system — niceties inherit calm-by-default, reduced-motion, and the §3.7 empty-state illustration set.

Followed by

Placeholder — a future ADR-06X will revisit /team once the velocity data source is picked; a separate follow-up binds operator identity to a real auth source.


Ship PRs: ai-frontend persona-routes-and-niceties (to be filed alongside this ADR).