.rstack/ files
the CLI and the agents read. There is no database and no telemetry leaves the machine. This page
is the front door: how to launch it, how the six-destination navigation shell (issue #278) is
structured, and how the freshness indicator keeps every page honest about how live its data is.
The rest of the Business Hub section covers each destination’s real content in depth.
Launch it
The hub opens automatically when a governed session starts (Pi, the Claude Code SessionStart hook, and Operator all launch it). You can also bring it up manually:Default port is 3008 (override with
RSTACK_BUSINESS_PORT). Disable auto-launch with
RSTACK_NO_BUSINESS_HUB=1. The legacy rstack-observer command is a compatibility alias that
opens the same hub.How it works
Six destinations, not a flat page list
The shell (src/observability/dashboard/ui/navigation.js) is built from a small fixed list of
destinations, each holding one or more children (pages). Only a destination’s non-hidden
children render as secondary-nav links; hidden children are still routable (a URL or deep link
still opens them) but don’t clutter the nav:
Each destination has a
defaultPage — the child it opens on when you click the primary nav
button — and clicking a destination’s primary link expands its secondary nav (navigationGroups
in navigation.js). The mobile nav (mobileNavigationMarkup) renders the same groups inside a
role="dialog" panel with focus trapping and Escape-to-close.
Approvals was promoted into the visible nav in #537: it had been hidden alongside the legacy
review pages even though it is the only surface where a human can approve or reject a gate.
Action Inbox still triages first; Approvals is where you act.
Routing: hash + query params, shareable
parseDashboardRoute({ hash, search }) reads ?page= from the query string and #page=…&run=…§ion=…
from the hash (a bare hash like #traceability with no = is treated as a page id directly, for
backward compatibility). formatDashboardHash writes it back. This means every page — and, for
the Run Workspace, every tab section (summary | work | timeline | artifacts | metrics) — has a
shareable URL; showPage() calls history.pushState/replaceState on every navigation, and the
popstate handler restores both the page and the run scope from the URL on back/forward.
Page-switch motion is a live-DOM fade, not the View Transitions API
showPage() toggles .page.active and lets CSS handle the fade. The comment in
navigationScript documents a deliberate rejection (issue #521): the View Transitions API
suppresses painting and hit-testing of the live page for ~160–250ms during the transition, so a
click landing right after a navigation would vanish. The plain CSS fade keeps the page interactive
on every frame instead.
Page bodies are static shells; data fills them in later
pages/index.js’s pageMarkup() renders one <section class="page" id="page-<id>"> per entry in
navigation.js’s pages list, each with a shared head (page-title, a <div class="last-updated">
slot) and a per-page body from pageBody(id). The bodies here are empty containers — KPI tiles,
<table> skeletons, panel <div>s with ids like command-attention, overview-proof-rail,
ops-transport-body — that the client-side data layer fills once a snapshot arrives. The Command
Center body (the Overview destination’s only child) is the richest: a delivery-outcome banner fed
by the server-owned readiness projection, a Proof Rail (overview-proof-rail), an executive
mission brief, KPI grid, and Needs Attention / Stage Health / Active Delivery / Recent Activity
panels — all real .rstack data, never invented placeholder numbers.
Freshness: the topbar never claims data is live when it isn’t
freshness.js’s classifyFreshness(opts) is a pure function evaluated both server- and
client-side against { hasData, now, lastSnapshotAt, wsConnected, staleMs, disconnectMs }
(defaults: 10s stale, 30s disconnect):
- no data yet →
loading - snapshot older than
disconnectMs→disconnected, regardless of what the socket reports - socket down but a poll or push still landed recently →
reconnecting - socket up but no snapshot inside
staleMs→stale(the server-side poll stalled) - otherwise →
live
freshnessLabel() turns that into the topbar text (“Live · updated HH:MM:SS”, “Stale · data as
of HH:MM:SS”, etc.) and freshnessDotClass() maps it to the existing status-dot styling. The
dashboard is built to never silently show stale data as if it were current.
Scope: project → run, shareable
A project/run switcher in the top bar scopes every page to the run you care about;SCOPE.run and
SCOPE.project persist across reloads and are restored from the URL’s run param
(restoreScopeFromRoute) — so a link like #page=run-workspace&run=<runKey> deep-links straight
into a specific run’s workspace, handy for sharing in Slack.
Multi-project aggregation
The hub reads a global registry at~/.rstack/known-projects.json that every rstack-agents init
and hub call updates, so one running hub instance shows runs across every project on the
machine. Multiple people can point the same hub (or the same notification channel) at it.
Try it
Open the hub, then try the shell mechanics directly:parseDashboardRoute/formatDashboardHash round-trip them, and
popstate will restore the same page + run + section on browser back/forward.
Related
Cockpit Controls
Authenticated, audited actions the Hub can take against a run (resume, checkpoint restore).
Approvals & Governance
The Action Inbox and Approvals destination in depth.
Data Visualizations
Quality/risk dials, the stage subway strip, and Spend Center charts.
Operations Center
Transport freshness, integrations, recovery, and context/memory health in one page.
