Dashboard¶
Flask host for the Python-generated root dashboard and incrementally migrated React dashboard, with shared same-origin APIs.
Details¶
Web dashboard for system observability + control. Served as a sidecar-managed Flask service on port 5127. Accessible remotely via Tailscale Serve.
Two frontends, one authority¶
The service hosts two distinct frontends during incremental migration:
/is the Python-generated root dashboard documented atservices/dashboard/frontend./appis the React dashboard documented atservices/dashboard/react.
The React surface is the primary desktop entry point, but it does not imply that every root-dashboard tab or mutation has moved. Tasks and Journal are explicit completed migrations. /app/tasks owns the task UI, the old root task route redirects there, and same-origin /api/tasks uses native TaskStore authority. /app/journal uses the live HttpJournalProvider and same-origin /api/journal/* routes backed by the native SQLite Journal; the older root Today surface and the React legacy provider remain review-only compatibility views. Both frontends use the same Flask process and same-origin API authority; neither browser may call sibling localhost service ports directly.
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.
The Settings panel also has Embeddings and Inference sub-views. Settings › Inference hosts the per-call provenance activity feed and, above it, a Local model fleet section — one card per machine (reachability, loaded models, multi-GPU hardware), load-on-open + manual refresh, live-updated via the fleet.changed SSE event. Reads serve from GET /api/fleet (cached per-machine snapshot); the inline roster editor writes via POST /api/fleet/roster (add/update or clear a machine's inference.fleet entry; _reject_read_only-gated, mirrors /api/embeddings/vault). See architecture/inference/fleet.
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. - Root frontend: the UI at
/is generated by Python modules underfrontend/and served as content-hashed cached assets. Seeservices/dashboard/frontendfor the module structure, concatenation order, event delegation, escaping, shared widgets, and asset serving. - React app:
GET /app/serves the Vite build fromdashboard-react/dist. It is the primary desktop UI opened bywbuddy launchand by the installer’s console-less native launcher; views move into it incrementally while unmigrated surfaces remain available in the root dashboard.GET /app/manifest.webmanifestandGET /app/icons/*provide optional PWA install metadata and branded icons with explicit media types. The PWA is an identity/standalone-window enhancement, not the runtime bootstrap. Release packaging builds this frontend before assembling the source-tree payload and refuses to build a payload when the dist is absent. - 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. - Frame boundary: Every response sets
Content-Security-Policy: frame-ancestors 'none'andX-Frame-Options: DENYso another site cannot embed dashboard controls. - Read-only HTTP policy:
dashboard.read_only: trueinconfig.yamlenables the central request refusal and frontend mutation restrictions. The separate--read-onlyprocess also enforces storage and process boundaries described below.
The React dashboard's standardized widget runtime, appearance contract, calendar presentation, and native Journal and Tasks views are documented under services/dashboard/react. Registry-driven configuration authority is documented at settings.
Read-only request and process boundaries¶
work_buddy.dashboard.read_only.guard_dashboard_request is the first application before_request hook. In read-only mode it refuses every method outside GET, HEAD, OPTIONS, and TRACE with HTTP 403 unless the endpoint has an explicit read-only exception. The sole exception publishes ephemeral events to in-memory SSE queues. It requires an authenticated local session or a direct loopback host callback with no browser provenance or proxy headers. Exception entries name registered endpoints and carry reasons, checked by validate_exception_registry.
In writable mode the central hook leaves authentication to each route's authority contract. Root-dashboard palette actions, conversation close, form-result delivery, and the Obsidian open-dashboard callback retain their existing callers without acquiring a React session first. Local-identity and operation-bound gesture checks still apply on routes that require them. Disposable live-host control routes independently enforce their nonce and exact-Origin rules. Existing route-level _reject_read_only() calls delegate to the shared policy as defense in depth.
The process entry point is wbuddy dashboard --read-only --port <port>, or uv run python -m work_buddy.dashboard --read-only --port <port> from the repository. Replace <port> with an explicit free port outside 5124, 5126, and 5127. It binds only to 127.0.0.1, reads the selected data and configuration roots, and does not start normal dashboard pollers, identity bootstrap, or store prewarming. Keep the user's normal dashboard process running separately. Use WORK_BUDDY_DATA_DIR and WORK_BUDDY_CONFIG_DIR to select disposable roots during validation.
The process installs its SQLite interposer before importing dashboard stores. On-disk paths and SQLite URIs open with mode=ro; every connection receives PRAGMA query_only = ON, including in-memory connections. The connection authorizer rejects ATTACH and attempts to disable query-only mode. Python file-write and filesystem-mutation operations, outbound connections, and child processes are refused. The sole child-process exception is the exact bundled document-kernel worker, which exchanges document bytes over pipes. That Node worker is trusted application code and does not inherit the Python process's audit protections. Logs use stdout and stderr. These controls apply to the dashboard process; independently invoked shell commands and other services have their own boundaries.
SQLite can still create or update native WAL and SHM coordination files when reading a live database. Account for those separately from authoritative database content, configuration, and document blobs; do not describe the whole filesystem as immutable. External-provider panes can be unavailable because the process cannot connect to sibling services, external providers, or tools. A stored-data read does not establish that those integrations work in this mode.
This process is opt-in. Before treating it as an agent read surface, run the runtime mutation audit and the isolated GET survey:
uv run python -m scripts.audit_dashboard_mutations --json
uv run python -m scripts.survey_dashboard_read_only --json
The mutation audit enumerates non-safe route methods from Flask's URL map, records executed guards and HTTP results, and checks the disposable root for changes. The GET survey uses independently seeded writable and read-only roots, supplies real fixture identities for covered dynamic routes, and records each route's response and observed connection policy. It compares file hashes, table counts, and logical SQLite schema and row digests obtained through a read transaction for each database. The logical digests include row updates stored in WAL without exposing row contents in the report, so unchanged counts or main-database bytes alone cannot pass the authority comparison. Review every SQLite or permission failure and every unexpected response, with a fix or an explicit unavailable/read-only disposition. A missing_resource record exercises rejection, not the existing-resource path; add a fixture or retain that coverage gap. A successful process exit alone does not establish a clean survey or complete route coverage.
Only after that isolated survey is clean, start the separate process on the intended real data root and non-service port. Record authoritative counts or revisions before and after a browser read and a refused non-safe request. Require the read to render, the request to return HTTP 403, and the authority observations to remain unchanged. Do not mint a session, write fixtures, or use real items for mutation tests. Keep provider limitations and untested paths explicit in the review record. See dev/live-testing-directions and dev/dashboard/verification-directions for browser evidence requirements.
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 root dashboard retains the right-side wb-chat-sidebar surface, which hosts the legacy conversation_chat renderer in pane mode. See services/dashboard/chat-sidebar for its compatibility API. React authoring uses the shared Co-work chat primitives and widget-native assisted drafts documented at services/dashboard/react/assisted-drafts; Jobs assistance is available at /app/jobs.
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 root-dashboard form bridge is frozen compatibility infrastructure, not an extension point. See services/dashboard/form-bridge for the retained protocol. dashboard_interact rejects every jobs-add-job action with form_migrated and a link to /app/jobs. New React forms declare host-owned drafts and accept advisory typed patches through services/dashboard/react/assisted-drafts; the user retains final submission authority.
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 pass through the central request policy and retain shared _reject_read_only() checks. They auto-grant the relevant consent (the click IS the consent, same pattern as workflow-launch).
Settings broker endpoints¶
GET /api/settings/registry— definitions, pages, sections, and placements.GET /api/settings/values[?context_id=...]— authoritative effective values for a page or view context.POST /api/settings/values/<setting_id>/preview— validate and describe a proposed value without storing or publishing it.PATCH /api/settings/values/<setting_id>— revision-checked update through the setting's declared authority.DELETE /api/settings/values/<setting_id>— revision-checked reset;POST /api/settings/resetis the body-addressed compatibility form.
Settings responses are no-store. Writes and resets honor dashboard read-only mode and publish settings.changed after success. See settings for registry identity, navigation placement, authority, and persistence.
Form-bridge endpoints¶
POST /api/dashboard/interact— typed entry point for agents driving forms (called by thedashboard_interactMCP skill 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 pass through the central request policy and retain shared _reject_read_only() checks. These are retained compatibility routes and cannot drive the React Jobs authoring form. See services/dashboard/form-bridge for the protocol.
User-job endpoints¶
GET /api/jobs/authoring— report authoring access and the configured time zone without creating a job.POST /api/jobs/authoring— human submission from/app/jobs, gated by dashboard read-only mode and enrolled identity. It validates the draft and delegates to the normal user-job skill and exclusive-create writer.POST /api/user_jobs— retained legacy user-job creation route, not an assisted-draft submission path.POST /api/user_jobs/help— retained migration response: HTTP 410 withjob_authoring_movedand/app/jobs, subject to read-only rejection. It does not spawn an agent or create a conversation.
Existing job management and editing remain in the root Jobs tab. See features/user-jobs and services/dashboard/react/assisted-drafts.
Assisted capture and task proposals¶
Tasks Quick Add, Journal Quick Capture, and Jobs authoring reuse widget-native draft assistance rather than a separate form-driving chat stack. Model assistance is opt-in and advisory; explicit user actions own submission. Task proposals are durable Threads records reviewed through /app/tasks?proposal=<threadId> and realized through the native TaskStore boundary. Journal retains exact capture input in Sources before routing or proposing a follow-up. See services/dashboard/react/assisted-drafts, services/dashboard/react/tasks-view, and journal/source-backed-capture.
Triage flow (no separate dashboard endpoints)¶
Triage runs through the unified source pipeline (the run_source_pipeline skill, currently dispatching to the registered EmailTriagePipeline and ChromeTriagePipeline). Native Journal capture and task proposals use the Journal API and proposal path described above; the retired Markdown JournalBacklogPipeline and inline-capture adapters are not registered source pipelines. 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.
- Keep the central request guard first. Non-safe routes inherit read-only refusal. Any read-only exception must name the exact registered endpoint, state its reason and authentication policy, and pass the runtime audit. Writable routes own their authority checks. Retain shared route-level checks as defense in depth.
- Same-origin only for any fetch from the frontend.
- Silent conversation create for sidebar-bound chats — call
conversations.store.create_conversationdirectly, NOT theconversation_createskill, so_notify_conversation_createddoes not double-mount the conversation as both a CHAT toast/workflow-view tab and a sidebar. - Keep the legacy form bridge frozen. Existing handlers route
dashboard.form.*events throughwbFormBridge; new consumers use widget-native assisted drafts and human-only submission, not DOM-driving events. - Keep request handlers off the hot-path anti-patterns — no per-request config parse, per-open schema work, N+1 store opens, or unbounded synchronous bridge/subprocess calls. Serve expensive reads from a background-refreshed cache and pre-warm at startup. See
architecture/hot-path-discipline.