ADR 0096 — Console Dashboard V2: Split-View Layout (Option B)
Status: Accepted — 2026-05-16 UTC Deciders: Kristerpher (operator), software-architect Parent epic: #146 Design doc: console-dashboard-v2-option-b-2026-05-16.md
Context
The operator reviewed three console dashboard redesign options (PR #2264):
- Option A — At-a-glance: larger tiles, inline sparklines, no navigation to detail
- Option B — Split view + side panel: compact tile list (60%) + sticky detail panel (40%)
- Option C — Drill-down first: minimal list, full detail on click replaces grid
Option B was selected. This ADR records the key implementation choices within that option that had meaningful alternatives.
Decision 1: CSS Grid over Flexbox for the two-column shell
Chosen: CSS Grid with grid-template-columns: 3fr 2fr and named areas.
Alternatives considered:
- Flexbox (
display: flex; flex: 3/flex: 2): simpler for a two-column layout, but Grid provides named areas that make the responsive collapse at<768pxcleaner (singlegrid-template-areasoverride vs. flex-direction + order manipulation). Grid also makes the "tiles scroll, panel is sticky" constraint easier to express viamin-height: 0on the grid container.
Consequences: Feature-developer must not add overflow: hidden or overflow: auto
to the grid container itself (that would break the panel's sticky positioning).
Decision 2: Side panel state in sessionStorage, not URL query params
Chosen: sessionStorage.dashboard_pinned_surface = "<surface-id>".
Alternatives considered:
- URL query param (
?surface=api-prod): shareable, bookmarkable. Rejected because: the dashboard URL is an HTMX partial target. The 30shx-getpartial swap usesurl_for('dashboard.status_grid_partial')which does not preserve query params. Adding query-param awareness would require changes to the HTMX trigger, the partial endpoint, and the base URL handling — significant complexity for a single-operator internal tool where shareability is not a requirement. localStorage: persists across tabs and sessions. Rejected — if the operator has two console tabs open, pinning a surface in one and switching env in another would cause confusing cross-tab state.sessionStorageis tab-isolated.- Server-side session (AdminSession.pinned_surface column): durable across reloads and devices, but requires a DB column + migration for state that has no audit significance. Rejected as over-engineered.
Consequences: The pinned surface is lost on tab close. This is acceptable for an operator monitoring tool — the operator re-clicks a tile, which is one action.
Decision 3: Flag-gated rollout (not hard cut)
Chosen: FLAG_CONSOLE_DASHBOARD_V2 gates the split view, default OFF.
Alternatives considered:
- Hard cut (remove old code in the same PR): simpler, no flag debt. Rejected because: the flip-card grid has active sub-cards (tile-drawer WCAG pass, build-strip) that may be live on prod. Cutting it hard risks breaking those surfaces if V2 has regressions. The flag allows the operator to A/B on staging first.
- Feature branch merge: develop V2 on a long-lived branch, merge when ready. Rejected per trunk-based SDLC policy (ADR-0050).
Consequences: Two dashboard code paths exist simultaneously for the rollout period. SC-DB-6 (cleanup PR) removes the old path after a 2-week prod soak.
Decision 4: Single always-visible action button per tile
Chosen: One right-aligned button per tile, content determined by surface state (Investigate for degraded/down, Deploy for in-progress build, Detail for healthy).
Alternatives considered:
- Icon-only buttons (3 icons per tile): compact, but harder to discover and harder to make accessible without visible labels. The Option B mockup shows a single labeled button, not an icon tray.
- No tile action button (click tile = pin only): cleaner, but loses the one-click Investigate flow which is the primary emergency affordance. The operator must be able to force-poll without navigating to the side panel first.
- Dropdown per tile: flexible but adds JS complexity and interaction cost. Rejected for a compact one-line tile.
Consequences: Deploy and Investigate share the same button slot. If a surface is DEGRADED and also has a deploy in progress, Investigate takes priority (the deploy is visible in the panel's build section). Feature-developer must implement the priority order from §4.2 of the design doc.
Decision 5: Side panel sparkline lazy-fetched on pin (not inline in tile)
Chosen: The 48-bucket sparkline is fetched once when a tile is pinned, using the
existing GET /dashboard/api/sites/<id>/probe-history endpoint.
Alternatives considered:
- Inline sparkline in each compact tile: would require 9 concurrent probe-history fetches on page load. Rejected — too many requests, and the tile height is too small for a readable sparkline.
- No sparkline in V2: the sparkline was a frequently-cited feature in the P5 epic. Removing it entirely would regress the monitoring experience. Rejected.
- Fetch on hover (not pin): hover is ephemeral; the user might not hover long enough for the fetch to complete. Pin is a stable, deliberate action. Chosen.
Consequences: The sparkline is not visible until the user pins a tile. This is acceptable — the panel is the "detail" view and the sparkline is a detail-level signal.
Decision 6: Preserve the per-surface detail page
Chosen: /dashboard/sites/<id> is preserved. The side panel's "Detail" link
navigates there.
Alternatives considered:
- Absorb detail page into side panel (scroll into deeper sections): would require all detail-page content (48h latency chart, full audit log, dyno info) to be fetched into the panel. That is a large increase in panel complexity and would make the panel unmanageable on smaller screens.
- Remove detail page entirely: the detail page has the 48h latency chart (FLAG_CONSOLE_SITE_LATENCY_CHART) and full audit log. Those are not replicated in the panel (panel shows last 5 audit rows, 48-bucket sparkline). Removing it would be a regression. Rejected.
Consequences: The console has two levels of surface detail: the panel (quick) and the detail page (full). This is intentional. No feature-developer action needed on the detail page for V2 — it is unchanged.
Consequences Summary
- The split-view layout requires no new backend services, no new DB tables, and no new infrastructure.
- One new endpoint (
GET /dashboard/api/sites/<id>/audit) is required for the panel's audit trail section. It must be@require_role("ops"). - All existing polling, caching, and probe data flows are reused without change.
- The cleanup PR (SC-DB-6) removes ~5 JS files, 3 CSS files, and significant template complexity, reducing the maintenance surface for the dashboard module.