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-bezierattention curve from visual-language.md §2.3, wrapped inprefers-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):
- Backend
kpi_snapshotstable + cron for Nicety #5 (time-comparison deltas). ADR-059 §10 already anticipated this. - GitHub-proxy
/api/v1/infra/raw/pulls(or/api/v1/ship/pulls) for Dev's "PRs awaiting me" list. Noted in/ship/pullscode asbackendPending: true— same endpoint. - Velocity data source decision for
/team— the placeholder remains until the source is picked. - Operator-identity binding (replace hard-coded handle).
- 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
/opsis worse than a missed update on/shipbecause the reader expected truth. - URL-shape commitment — renaming
/melater is a migration, not a refactor. localStorage.lastVisitedAtis 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.
- Scaffold
/me— minimum "My PRs across fleet" + "Recently touched repos" + KPI strip. Empty state when data is null. - Scaffold
/ops— severity-ranked alerts + down services + KPI strip. Empty state "Everything's green." when no alerts. - Scaffold
/team— placeholder page until velocity source is picked. - Niceties slice A (highest-leverage two): keyboard shortcuts (Nicety #1) + attention-directing badges (Nicety #3).
- Niceties slice B: command palette (Nicety #2), empty-state polish (Nicety #4).
- Niceties slice C: time-deltas (Nicety #5 — gated on backend snapshot).
- Niceties slice D: quick-actions (Nicety #6), what-changed digest (Nicety #7).
- 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).