Dashboard¶
Web dashboard for system observability — Flask service, dev mode, remote access, development rules
Details¶
Web dashboard for system observability + control. Served as a sidecar-managed Flask service on port 5127. Accessible remotely via Tailscale Serve.
Tabs¶
Static: Overview, Threads, Today, Tasks, Jobs, Chats, Contracts, Projects, Costs. Plus a Settings panel reached via a gear icon in the header (off the nav bar by design — Settings is a configuration surface, not a peer of the daily-use tabs).
Dynamic: Threads, Triage, and Notifications appear via workflow views, the thread system, and the notification log.
Settings tab¶
Primary consumer of the control graph (see architecture/control-graph for the aggregator; architecture/health for the four-layer mental model the graph fuses). The Settings panel has two sub-tabs: Status — the control-graph tree (domain → subsystem → component hierarchy with effective_state badges, preference toggles (Want / No thanks / Undecided, hidden for is_core components), Configure / Walk me through action buttons for fixable requirements, universal ? help buttons that spawn interactive Claude Code sessions with structured briefs, a per-component ↻ reprobe button, and clickable bulk-state chips that jump to the first problem node of that state) — and Activity, a registry-driven set of cards (Obsidian bridge sparkline, sidecar event log, recent-notifications log). See the Card registry section below.
Modes and endpoints¶
- Dev mode:
python -m work_buddy.dashboard --dev(auto-reloads on file changes). Not enabled in sidecar config — use manually for local development only. - Frontend layout: Each tab is HTML + JS in the
frontend/package. JS lives underfrontend/scripts/in three buckets:core/(event bus, page shell, helpers, workflow polling, notifications, palette, chat sidebar, form bridge, shared pager, card registry),tabs/(one module per panel, withtabs/threads/*as a sub-cluster andtabs/cards/*for registry card renderers), andsurfaces/(workflow-view renderers). Each module exposesscript() -> strand optionallystyles() -> str;frontend/scripts/__init__.pydefines the load-bearing concatenation order via theSCRIPTSandSTYLESregistries. - Shared pager:
frontend/scripts/core/pager.pyexposeswindow.wbRenderPager(containerId, total, currentPage, pageSize, onPageFnName). Tabs that paginate mount a<div class="wb-pager" id="..."></div>container and call the renderer after their data fetch resolves. The pager hides itself whentotal <= pageSize. Class names are.wb-pager*(styled centrally instyles.py); the Threads tab and the Costs sessions table both use it. - Adding a tab (5-step pattern):
- Add a
<button>to the tab bar inhtml.py_html(). - Add a
<div class="tab-panel" id="panel-<name>">in the panels section. - Create
frontend/scripts/tabs/<name>.pyexposingscript() -> str(and optionallystyles() -> str). - Add the loader to
staticLoadersinfrontend/scripts/core/page.py. - Add the new module's
script(andstylesif applicable) to the ordered registry infrontend/scripts/__init__.py. Settings is atypical — its trigger lives inheader-metarather than the tab bar, but the panel structure is the same.
- Add a
- Remote access: Published privately via
tailscale serve --bg 5127— thetailscalecomponent (registered inCOMPONENT_CATALOG) gates this with click-to-fix requirements; seearchitecture/health/componentsandstatus/tailscale-status-directions. The browser only hits same-origin/api/...routes; all local service reads happen server-side. - Read-only mode:
dashboard.read_only: trueinconfig.yamlgates mutating POST routes (403) and hides mutation controls in the frontend.
Card registry (feature cards)¶
The Settings → Activity sub-view is registry-driven: its widgets (Obsidian bridge sparkline, sidecar event log, recent-notifications log) are DashboardCards, not hand-coded render blocks. loadActivity() calls window.wbMountCards('activity', ...), which fetches the active card list and renders each registered renderer. A card may carry a gate — a boolean expression over component-active state — so a card whose component is opted out simply does not mount (no placeholder). The bridge card is gated on the obsidian component; opting Obsidian out also stops the backend bridge probe in get_system_state(). See architecture/feature-cards for the full pattern — gate AST, registry, endpoint, and how to add a card (including from a plugin).
GET /api/dashboard/cards/<mount_point>— active card descriptors for a mount point, gates evaluated against current component preferences. Read-only.
Right-rail surface (chat sidebar)¶
The dashboard has a persistent right-side surface — wb-chat-sidebar — that slides in beside the main content, which squishes left via html { padding-right }. Hosts a conversation_chat renderer in pane mode. See services/dashboard/chat-sidebar for the full reusable API; first consumer is the Jobs tab's 💬 Help me create a job button (endpoint POST /api/user_jobs/help).
Distinct from the conversation_chat workflow-view tab — same renderer, different mount point: a workflow-view tab is a full-tab pane reached via the CHAT toast, while the chat sidebar opens directly without a toast and squishes the active tab rather than replacing it.
Agent ↔ form bridge¶
The chat sidebar (above) is the conversation surface; the form bridge is the interaction surface — schema-driven, typed, and reusable across forms. See services/dashboard/form-bridge for the full design. Agents call the single MCP capability dashboard_interact to fill fields, open the form, click submit, and read state; the dashboard validates against the form's registered FormSchema and routes events through window.wbFormBridge to per-form handlers.
Real-time updates¶
The dashboard updates in real time from server-pushed events delivered over GET /api/events (Server-Sent Events). Each event mutates only the specific row(s) it concerns; panels are never wholesale-rewritten. bus.heartbeat published every 10 s as a liveness signal. See architecture/event-bus for the full design.
Control-graph endpoints (added with the Settings tab)¶
GET /api/control/graph[?force=1]— serialized graph + cache info.POST /api/control/preference— toggle component preferences.POST /api/control/fix/<req_id>— apply a fix (programmatic / input_required / agent_handoff).POST /api/control/help/<node_id>— spawn an interactive help session.POST /api/control/reprobe— re-run every tool probe, rebuild the graph.POST /api/reprobe/<component_id>— pre-existing; per-component reprobe, reused by Settings' ↻ button.
All mutating control endpoints are gated by _reject_read_only() and auto-grant the relevant consent (the click IS the consent, same pattern as workflow-launch).
Form-bridge endpoints¶
POST /api/dashboard/interact— typed entry point for agents driving forms (called by thedashboard_interactMCP capability and any other process). Body{action, form_id, field?, value?, timeout_seconds?}.POST /api/dashboard/interact/result/<request_id>— frontend's postback for rendezvous-backed actions (form_submit,form_get_state). Body{ok, error?, errors_by_field?, fields?}.
Both gated by _reject_read_only(). See services/dashboard/form-bridge for the protocol.
User-job endpoints¶
POST /api/user_jobs— create a user-job file from the Add-job form. Same path the chat-walkthrough agent goes through (via the form bridge'ssubmitHandler), so any future change to validation or payload shape benefits both flows.POST /api/user_jobs/help— open a chat-driven walkthrough. Silently creates a conversation, fire-and-forgets a headless Claude session bound to it, returns{ok, conversation_id, title}for the frontend to feed intowbChatSidebar.open. Auto-grantssidecar:agent_spawnonce-consent inside the spawn helper.
Both gated by _reject_read_only().
Triage flow (no separate dashboard endpoints)¶
Triage runs through the unified source pipeline (run_source_pipeline capability, dispatching to EmailTriagePipeline / ChromeTriagePipeline / JournalBacklogPipeline / inline-capture). Spawned Threads land on the Threads tab for the user to approve/reject/defer per child. There is no separate Review-tab surface or Resolution-Surface endpoints — those were retired in the clarify → Threads migration. Per-cluster actions resolve via the standard Threads action-chip dispatch path.
CRITICAL for all agents modifying dashboard code¶
- Never add browser-side fetches to sibling localhost ports (5123, 5124, 27125, etc.) — these break on mobile and over Tailscale. All cross-service reads must happen server-side.
- Gate new POST routes with
_reject_read_only()so read-only deployments stay read-only. - Same-origin only for any fetch from the frontend.
- Silent conversation create for sidebar-bound chats — call
conversations.store.create_conversationdirectly, NOT theconversation_createcapability, so_notify_conversation_createddoes not double-mount the conversation as both a CHAT toast/workflow-view tab and a sidebar. - Do not subscribe to
dashboard.form.*events directly from per-tab JS. ThewbFormBridge(core/form_bridge.py) owns that event family; tab modules register handlers viawindow.wbFormBridge.register(form_id, ...).