Skip to content
GRPNR.

05 — UX/UI Specification: Fleet Monitor

2026-07-18. Written against 01-initial-recommendation.md (the recommendation under test) and packets R2/R3/R6/R7. Doc-writing convention below: VERIFIED (source+date, checked live today), PRIOR-RESEARCH (sourced to an earlier dated doc, not rechecked today), DESIGN (a specification choice made in this doc, not a measurement), ASSUMPTION (inferred, not stated anywhere, flagged so it isn’t mistaken for a decision).

Where this doc disagrees with or sharpens 01, stated up front, not buried:

  • Kanban tension (R7 §e.1): this doc sides fully with 01’s removal of a kanban board — evidence supports 01 here, so no override needed, just confirmation with a reason (§2).
  • The drawer and Zaruba strip are not a third/fourth surface despite 01 saying “no third surface” — they’re zoom-levels of the same Board canvas, not new navigable pages. Stated explicitly so this reads as a deliberate interpretation, not scope creep (§2).
  • 01’s own trial metric (“cost/merged-unit visible at all, yes/no”) cannot pass in week 1 because the work-ledger it depends on is decided-but-not-built (R7 §d). §6 replaces it with measurable substitutes rather than silently inheriting a metric the trial can’t test.
  • 01 never names the precondition behind its own “<5 s raised hand” claim: it only holds if the Board is genuinely left open and glanced at. This doc surfaces that as the top UX risk (§7), not a solved problem.
  • Zaruba watch strip scope is not decided (GitHub Actions ingestion, R7 §e.5) — this doc ships the strip against the one signal that’s actually decided (Temporal workflow stream) and marks the rest a placeholder, rather than inventing scope.

1. UX vision

Intended feeling: calm control. Not command-center adrenaline, not dashboard boredom. Norman’s target mental model (01, PRIOR-RESEARCH): “my workers raise hands.” Default emotional register is “nothing to do” — no idle animation, no ambient chart-churn, nothing competing with the one signal that matters. When something needs a decision it should be unmissable; the rest of the time the screen should be nearly silent.

Primary value moment: a raised hand seen in under 5 seconds (DESIGN target, from the brief — not independently measured; §6 explains why this can’t be instrumented directly and what stands in for it in the trial).

Time-to-first-value: <1 day install (DESIGN target). The adopted layer — docker-compose OTel→Prometheus→Grafana, claude agents — is a known, scriptable stack (PRIOR-RESEARCH, research/12 §2); CLAUDE_CODE_ENABLE_TELEMETRY=1 plus a handful of documented hooks (VERIFIED, R6) is same-day work. This number is only credible for the ledger-less v1 (§2’s degrade path) — if v1 insists on the ledger join before shipping, <1 day is not honest, because the ledger is “decided, not built” (R7 §d).

Trust principles (binding, not aspirational — these gate every screen in §3):

  • Never show a status the system cannot back. An unknown state renders as unknown, never as a stale copy of the last known state dressed up as current.
  • Staleness is always visible, not merely detectable-if-you-check. Every data-bearing element carries a visible last-updated marker. Nothing in this product is allowed to be silently frozen.
  • Confidence is shown for inferred states. “Stalled” is a computed guess (active-time plateau vs wall-clock climb — canonical metric #4, R7 §c); it must never carry the same visual certainty as “needs input,” which is a hard signal (an explicit blocking event). Conflating a guess with a fact is the exact mapping failure 01 names as its top risk.

2. Information architecture

One live canvas, one overlay, one contextual drawer, one static export. No router, no sidebar, no settings page in v1.

Object What it is Source Available without the ledger?
Unit One item of rebuild work: status enum, cost, attempts, disposition Work-ledger (Postgres, Encore) — decided, not built (R7 §d) No
Session One Claude Code process: status, active-time, tokens, cost, host OTel + Agent View (claude agents --json) Yes (VERIFIED, R6)
Event A lifecycle occurrence: hook fire, tool call, prompt, stop Hooks stream (PreToolUse/PostToolUse/UserPromptSubmit/Stop, etc. — VERIFIED, R6 §2) Yes
Question The text an agent is blocked on, one open per blocked Unit/Session Ledger block() call, or inferred from a Stop/needs-input hook pre-ledger Partial — text exists pre-ledger via the raw event, but has no owner/disposition until the ledger exists
Metric rework rate, cost/merged-unit, queue depth, stall detection (canonical four, R7 §c) Computed, joins Unit+Session+Event Queue depth and stall detection: yes (session-level). Rework rate and cost/merged-unit: no, ledger-dependent by definition

Why no navigation tree: the five objects collapse onto one canvas by construction, not by omission. Board renders Unit⊕Session rows. Queue renders the list of open Questions (a filtered, re-sorted view of the same rows, not a different dataset). The drawer renders one row’s full Event history — a zoom-in, not a new page. The Zaruba strip is a labeled sub-group of rows at the top of the same Board, not a separate screen. Metric is always a summary strip, never a drill-down destination — 01 already removes “historical analytics suite” from concept, so there is nowhere for a metrics page to lead. Five objects, one live surface plus one static export (the Digest, which is pushed, not navigated to) is the entire IA.

Reconciling with 01’s “no third surface”: 01 states “Two surfaces. Board… and queue… No third surface.” The drawer and the Zaruba strip are not counted as violating this because they’re bound to and dependent on the Board — they have no independent existence, no URL, no persistent state once closed. Calling this out explicitly so it reads as an interpretation of 01, not a silent expansion past it.

Kanban tension, resolved: research/12 named Vibe Kanban as a UI-shape donor (R7 §e.1); 01 explicitly removes “a kanban board” from concept because the ledger already owns state and a board would duplicate it. This doc sides with 01 without reservation — the work-ledger’s own risk register names “two sources of truth” as its top usability risk, and a draggable kanban is exactly a second, human-editable copy of state the ledger exists to prevent. Vibe Kanban’s only surviving donor value here is narrower than “adopt its board”: row-density and keyboard-nav patterns for the Queue overlay, nothing structural.

The ledger-degrade path is load-bearing, not a footnote. Whenever the ledger isn’t live — which is true today, and may still be true at MVP ship per R7 §d — the IA silently narrows from five objects to three (Session, Event, Metric-lite). This is where §1’s “never show unbacked status” principle does its real work: cost/merged-unit and rework-rate tiles must read “unavailable — ledger not connected,” never a session-level number dressed up as the real metric.


3. Core screens

3.1 Board

  • Objective: answer “does anything need me right now” in one glance.
  • Primary action: none — read-only. Click a row → drawer. Press a key → Queue.
  • Hierarchy: needs-input (red) top, stalled (amber) next, working/idle (blue-gray, recedes) below, done rows fade out after a few seconds.
  • Data: Unit⊕Session rows, 4 metric tiles, Zaruba strip, staleness footer.
  • Success: “0 blocked” rendered large — the goal state, not an absence (01, PRIOR-RESEARCH).
  • Empty: same as success — see §3.6, it’s one state, not two designs.
  • Error/degraded: see §3.7.
  • Events logged: no page-view telemetry — nobody queries usage-of-the-monitor at N=1 operator (skip it, add if a third operator ever needs usage evidence). Rendered signals are native: claude_code.session.count, Agent View status (Working/Needs input/Idle/Completed/Failed/Stopped — VERIFIED, R6 §3), claude_code.active_time.total, claude_code.cost.usage.

3.2 Queue / triage overlay

  • Objective: clear every open Question in the fewest keystrokes.
  • Primary action: reply inline, verbatim, to the selected item.
  • Hierarchy: oldest-blocked-first (DESIGN choice — simplest honest ordering; don’t silently blend in a second axis like cost-at-risk without deciding to).
  • Data: open Questions — text, unit/session id, blocked-since duration, one-line context.
  • Success: queue empties, returns to Board automatically.
  • Empty: brief confirmation, auto-dismiss to Board — the Queue has no empty-state design of its own, because its “empty” and the Board’s “0 blocked” are the same fact.
  • Error: reply send fails (dead session / network) → inline error under the box, text stays in the input, never silently dropped.
  • Events logged: DESIGN custom fleet_monitor.reply_sent (unit/session id, latency since question raised) — this is the product’s own action, nothing native covers it, and §6’s trial needs it.

3.3 Unit detail drawer

  • Objective: “why is this row this color” — full context without tab-cycling to tmux.
  • Primary action: reply (same as Queue, contextual) or kill session (destructive, confirmed).
  • Hierarchy: current state first, then Event timeline (reverse-chron), then cost/token totals.
  • Data: full Event stream for the Session (+ Unit ledger fields if present).
  • Success: matches 01’s own primary-journey target — decide in <60s, zero tab-cycling.
  • Empty: N/A — only opens on an existing row.
  • Error: history fetch fails → show what’s cached with an explicit stale marker, never a blank drawer.
  • Events logged: none custom; renders native PreToolUse/PostToolUse/UserPromptSubmit/Stop hook events (VERIFIED, R6 §2).

3.4 Zaruba watch strip

  • Objective: Robert sees Tomas’s groupon2 deployment/workflow health without a second tool.
  • Primary action: none — read-only, same grammar as Board.
  • Hierarchy: fixed strip at the top of the Board, not a separate page (§2).
  • Data — scope genuinely undecided, flag it as such: decided signal = internal-plane Temporal workflow-execution stream (already the ledger’s own projection source, PRIOR-RESEARCH, 00-SYNTHESIS.md architect addendum #1). Undecided: whether GitHub Actions/CI status is ingested too — R7 §e.5 names this a gap, not a scope item, “contrary to how 01’s framing might read at a glance.” Recommendation: ship the strip against the Temporal stream only in v1 (already flowing, zero new integration), leave a visibly labeled placeholder for CI status pending an actual scoping conversation with Tomas.
  • Success/empty/error: same visual grammar as Board rows, second row-group with a distinct “ZARUBA” label so Robert never confuses his fleet’s red with Tomas’s.
  • Events logged: none new — consumes the existing workflow-execution stream.

3.5 Digest (static daily artifact)

  • Objective: Tomas gets a terse, receipts-first daily state without ever opening a dashboard — he is explicitly not a dashboard user (01).
  • Primary action: none — pushed via Google Chat, house standard (PRIOR-RESEARCH, R7 §f), not pulled.
  • Hierarchy: today’s disposition counts, cost, blocked/stalled counts — each carrying its MEASURED/MODELED/unavailable passport per Tomas’s own comm standard, already committed by 01.
  • Data: yesterday’s rollup, generated once, not live.
  • Success: readable in the time it takes to read a chat message; a Board link is optional context, not the point.
  • Empty: “0 blocked, 0 stalled, N merged” — same success framing as the Board, ported to text.
  • Error: an unavailable number gets its own line stating so (“cost/merged-unit: unavailable, ledger offline”) — never a silently omitted line, because omission reads as “nothing happened,” which is worse than “we don’t know.”
  • Events logged: DESIGN custom fleet_monitor.digest_sent (date, recipient, which numbers were unavailable) — cheap, and doubles as the audit trail R7 §f asks any Tomas-facing artifact to carry.

3.6 Empty state

“0 blocked” is the success state — not a placeholder illustration, not a friendly empty-box message. Large, quiet, green-adjacent but not full saturated green (full green risks reading as a “go/all-clear” alarm rather than “nothing to look at” — it should recede, not celebrate). Direct application of 01’s “empty state is the goal state” and Rams’ honesty: don’t dress up “nothing here” as either an achievement banner or a broken page.

3.7 Degraded states

Three distinct banners — never merged into one generic “error,” because different problems need different operator responses (Norman):

  1. Collector down (OTel Collector/Prometheus unreachable) — full-width red-adjacent banner, all data marked stale, Board frozen at last-known state with a loud timestamp. Worst case; should look like it.
  2. Telemetry gap (collector up, one session/host stopped reporting) — scoped to the affected rows only. Those rows go neutral-gray/unknown, they don’t disappear — a vanished row reads as “resolved,” which may be a lie.
  3. Ledger unavailable (thin-layer join source down, or simply not built yet, per §2) — scoped to the metric tiles only. Board/Queue keep working off Session+Event. Tiles show “unavailable” text, never a zero — zero looks like a real measurement of “no cost”; unavailable looks like what it is.

Staleness threshold (MODELED): flag data older than 2× the configured export interval — default OTEL_METRIC_EXPORT_INTERVAL = 60000ms (VERIFIED, R6), so 120s by default. Config-driven, not hardcoded, since operators may tune the export interval.


4. Interaction design

  • Keyboard model: Board has no interaction beyond entering Queue (one key, e.g. /). Queue: j/k or arrows to move, Enter to focus reply, type verbatim, Enter to send, Esc to return without sending (input text preserved, not discarded). Drawer: Esc to close, nothing else needed at this scope.
  • Progressive disclosure: Board shows state + one line of context; drawer shows full history. No middle “expand row” state — 01 already rules out a third surface, and a row-expand would be a fourth.
  • Confirmation only for destructive: kill session is the only destructive action in v1 (auto-remediation/auto-kill is out of MVP per 01, so kill is always human-initiated). One confirm dialog naming what’s lost (uncommitted work, in-flight tool call). No confirmation on reply-send.
  • Undo: reply needs none — send again. Kill has none possible (the process is dead); the confirm dialog is the undo — prevention over recovery (Norman).
  • Long-running ops: none in v1 scope — no bulk actions, no batch kill (ASSUMPTION, consistent with MVP minimalism in 01 though not stated there explicitly; flagged as inferred, not decided).
  • AI-content behavior for inline replies: operator text is sent to the session verbatim — no LLM rewriting, no autocorrect-style mutation, no “clean this up” pass. Binding, not a nice-to-have: an agent parses the reply as an instruction, and any silent paraphrase changes what it does next in a way the operator didn’t say and can’t audit.
  • Staleness/confidence indicators: every row/tile carries a relative last-updated stamp. Stalled rows carry a distinct marker (§5) plus a hover/tap tooltip stating the inference basis in plain numbers (“active time flat 4m12s while wall-clock advanced 4m40s”) — never a color with no explanation, since a color alone is exactly the “binary lie” §1 and 01 both warn against.
  • Notification policy: near-zero. No push, no toast, no sound, no browser-notification API in v1 — the Board itself, kept open on an ambient display, is the notification. Justification against alarm-fatigue evidence (R3 Part C, VERIFIED-TODAY): the Google SRE book’s actionability doctrine — a page that “merely merits a robotic response… shouldn’t be a page” — argues against a second competing alert channel for one operator watching one calm board; NASA’s tiered-severity model and PagerDuty’s queue-not-wall pattern both put the actionable list (here: Queue) ahead of any alert stream, which this design already does structurally. Risk this policy accepts, stated plainly: it assumes the Board is genuinely ambient — open and glanced at — at all times. If that’s false, “near-zero notifications” silently becomes zero notifications, and nobody decided that on purpose. 01 never names this precondition; this doc surfaces it as UX risk #1 (§7).

5. Visual direction

Dark, dense, calm, single theme (DESIGN/ASSUMPTION — no light-mode toggle in v1; this is a pinned ambient screen for one operator, not a general-audience artifact. Revisit only if a third operator needs daytime legibility — tied to risk #4, §7).

Color = state semantics only. Five values, nothing else in the product:

# State Color Notes
1 Needs input (blocked on a question) Red Highest priority, always sorts top
2 Failed/crashed Red, distinguished by icon (✕), not a second hue Keeps the eye keyed on “red = look now” without multiplying the palette (Rams: fewer colors once the base signal is set)
3 Stalled (inferred) Amber, always paired with a confidence marker Never the same visual weight as red — it’s a guess, not a fact
4 Working / idle Near-invisible blue-gray Deliberately recedes — the “everything else” 01 asks for
5 Done / merged Green, transient — fades out over a few seconds A wall of persistent green is as much a metric-wall failure as a wall of red
Unknown / no signal Neutral gray, reserved exclusively for this Never reused for a health meaning — gray always means “we don’t know,” never “fine”

Typography/density (DESIGN, unvalidated starting point for the prototype to tune): 14px UI text; monospace/tabular figures for cost and token columns (scan-ability over aesthetics); row height sized so roughly 20–25 units are visible without scrolling on a 1080p secondary monitor. Density reference, existence-proof only, not a dependency: recon (VERIFIED-TODAY, R2 — Rust TUI, 258★, last push 2026-07-17) demonstrates this density is achievable in a terminal-grade dense table with 2s polling.

Motion near-zero: state-color changes cross-fade over ~150–250ms (a hard pop reads as a false-alarm flash). One deliberate exception: the stalled-amber marker gets a slow (~2s cycle) subtle pulse — it needs more attention than static idle gray but less certainty-weighted urgency than solid red. Motion is doing confidence-signaling work here, not decoration.

Data-viz rules: sparklines, not walls. The four canonical metrics (rework rate, cost/merged-unit, queue depth, stall detection — R7 §c) render as big-number tiles with a small inline trend sparkline each. No pie charts, no gauges, no 3D, no dedicated charts page — a historical analytics suite is explicitly out of concept per 01. Per-unit cost trend, if shown at all, is a tiny inline sparkline in the drawer, never a separate screen.

Board wireframe:

┌─ FLEET ───────────────────────────────────────── updated 3s ago ─┐
│ ZARUBA  [grn] deploy: groupon2-payments   workflow: 3 running    │
├────────────────────────────────────────────────────────────────┤
│ [RED]  unit-2214  api-cart-migrate     blocked 4m  "which flag?" │
│ [RED]  unit-2201  legacy-export        FAILED  crashed 1m ago    │
│ [AMB]  unit-2188  checkout-parity      stalled ~4m  (low conf)   │
│ [   ]  unit-2190  inventory-sync       working                   │
│ [   ]  unit-2192  pricing-rules        working                   │
│ [grn]  unit-2177  refund-flow          done, merged (fading)     │
│                        … 18 more, all working …                  │
├────────────────────────────────────────────────────────────────┤
│  REWORK      COST/UNIT     QUEUE DEPTH    STALL                  │
│  unavailable unavailable   2  ▁▁▂▁        1  ▁▁▂▂                │
│  (ledger offline — session/event data only)                      │
└────────────────────────────────────────────────────────────────┘
          press / for Queue  ·  click a row for detail

Queue wireframe:

┌─ QUEUE (2 open) ──────────────────────────────── esc: board ──┐
│ ▸ unit-2214  api-cart-migrate      blocked 4m                  │
│              "which feature flag gates the new cart api?"      │
│   unit-2201  legacy-export         FAILED — kill? (x)          │
├─────────────────────────────────────────────────────────────┤
│ reply> _                                                        │
├─────────────────────────────────────────────────────────────┤
│  j/k move · enter reply · x kill (confirm) · esc cancel         │
└─────────────────────────────────────────────────────────────┘

6. Prototype recommendation

Which journey first: Board → Queue → reply loop, running on Session+Event data only (no ledger, §2’s degrade path). That’s the entire primary value moment (§1) and is buildable today against VERIFIED-TODAY native signals (R6) with zero dependency on the not-yet-built ledger.

Fidelity: functional, not polished. Real state colors, copy, and keyboard bindings; skip fade/motion timing, the Zaruba strip’s chrome, and Digest formatting. It should look plain, not slick — polishing before the reply loop is validated is exactly the cockpit-vs-doorbell temptation 01 already names.

What stays fake, named explicitly so nobody mistakes a placeholder for a shipped number:

  • Rework rate and cost/merged-unit tiles literally render “unavailable, ledger not connected” for the whole trial week — honest, per §2/§3.7, not a session-data number dressed up as the real metric.
  • Zaruba strip: static, wired to the Temporal stream only; the CI-status slot is visibly marked “not yet decided.”
  • Digest: hand-written for week 1, not automated — automating format before anyone’s confirmed the content is right is premature.

1-week live-trial success criteria — revising 01’s own proposed measures where the ledger dependency makes them not-yet-testable (a direct correction, not a restatement):

  1. Zero missed needs-input events over the week, checked daily by Robert against tmux/Agent View directly. One miss fails the trial outright regardless of every other number — trust is binary per §1.
  2. Median time from question-raised to reply-sent (via fleet_monitor.reply_sent, §3.2) — target <2 minutes (DESIGN target, recalibrated down from the vision’s “<5s seen,” which isn’t directly instrumentable; reply-sent is the measurable proxy for “was seen and acted on”).
  3. Stall-flag accuracy — false-positive/false-negative rate on amber markers, checked manually against what Robert independently knows was actually stuck. The trial’s only way to test whether the confidence signal (§1’s third trust principle) is honest.
  4. Cost/merged-unit and rework rate: explicitly out of scope for week 1’s pass/fail. They can’t be validated until the ledger exists. 01’s original criterion (“cost/merged-unit visible at all, yes/no”) reads “no” for the entire trial, and that is not a trial failure — it’s the ledger dependency (R7 §d) surfacing exactly where flagged.

7. UX risks, ranked

  1. Trust collapse (highest — matches 01’s own top risk, reinforced by every source read this pass). One wrong or stale needs-input row sends Robert back to tmux permanently; the product’s entire value is contingent on staying trusted every single time. Mitigation: the degraded-state banners (§3.7) are designed to be louder than silence, and §1’s “never show unbacked status” principle is the whole defense — there is no clever UI trick that substitutes for actually being right.
  2. Alarm fatigue via the notification policy’s hidden precondition (§4). If Robert stops treating the Board as ambient, near-zero notifications silently becomes zero, without anyone deciding that on purpose. Mitigation: this is a monitoring-of-the-monitor problem with no clean UI fix inside this product; the honest mitigation is the Digest, whose “unavailable” line (§3.5) gives Tomas independent visibility if Robert’s own attention lapses. Flagged as real and unresolved, not solved.
  3. Metric-wall creep. Four metrics today; the standard failure mode of every internal dashboard is “just one more panel.” Mitigation: test any fifth panel proposal against 01’s own bar — “does this change what the operator does next” — not against “would this be interesting to know.”
  4. Expert-only opacity for a future third operator. Every color, keybinding, and inferred-vs-hard distinction here is tuned to one operator’s accumulated mental model. Lowest-ranked because it’s explicitly not this pass’s problem (multi-tenant is out of MVP per 01) — flagged now because retrofitting legibility later (a legend, onboarding copy) is far cheaper to plan for than to bolt on after the fact.