Skip to content

Dashboard Event Bus

In-process pub/sub + SSE stream + cross-process bridge that powers real-time dashboard updates without a global panel-refresh timer.

Details

Why

The dashboard updates in real time from server-pushed events delivered over Server-Sent Events. Each event mutates only the specific DOM nodes it concerns; a panel is never wholesale-rewritten in response to an event. This preserves user state — focused inputs, scroll position, drilled-in <details>, chip rails — across every update.

Surfaces

  • Python
  • work_buddy.dashboard.events.EventBus — thread-safe in-process pub/sub with per-subscriber bounded deque + threading.Condition.
  • events.publish(event_type, payload) — in-process publish.
  • events.publish_cross_process(event_type, payload) — POSTs bus.event to messaging service (port 5123).
  • events.publish_auto(event_type, payload) — routes by process flag (mark_dashboard_process() set in service.main()).
  • events.start_heartbeat(interval, bus) — publishes bus.heartbeat every interval seconds (default 10 s).
  • dashboard.messaging_bridge.start_messaging_bridge(...) — daemon polling the messaging service for bus.event rows at 500 ms cadence and republishing on the in-process bus.
  • work_buddy.clarify.capabilities.triage_review_pool.compose_entry_presentation_group(entry) — single-entry presentation composer used by ClarifyPool.submit / submit_raw for fat-add events.

  • HTTP

  • GET /api/events — SSE stream. No read-only gate. Cache-Control: no-cache, X-Accel-Buffering: no. 15 s idle keepalive comment to defeat intermediary idle-close.

  • Browser

  • window.eventBus.{on, off, isConnected, lastHeartbeat} — per-event-type dispatcher.
  • window.<panel>Surface — per-panel handle (Review / Tasks / Settings / Costs / Jobs / Projects).
  • Connection-status dot at #event-bus-status in the dashboard header.

Vendored dependency

frontend/vendor/morphdom-umd.min.js v2.7.4 (12 KB, MIT) — the surgical-update primitive used by Phoenix LiveView and Hotwire/Turbo.

Event taxonomy

Event Payload Publisher
bus.heartbeat {interval} events.start_heartbeat (every 10 s)
pool.entry_added {run_id, item_id, source, adapter, raw?, group} (fat) ClarifyPool.submit / submit_raw
pool.entry_state_changed {run_id, item_id, state, reason?, outcome?} ClarifyPool.mark_state / mark_reviewed (and the wrappers)
pool.attraction_passes_bumped {run_id, item_id, count} ClarifyPool.increment_attraction_pass
pool.forced_context_stored {run_id, item_id} ClarifyPool.store_forced_context
task.created {task_id, state, urgency, contract} tasks.mutations.create_task
task.state_changed {task_id, state, reason} update_task (when state is set) + toggle_task
task.description_changed {task_id, description} update_task_description
project.created {project_id, slug, status, author} projects.store.upsert_project (new row)
project.updated {project_id, slug, author} projects.store.upsert_project (existing row), update_project
project.deleted {project_id, slug, author} projects.store.delete_project (soft-delete)
project.folders_changed {project_id, action, path, author} (action: add | remove | archive | unarchive) projects.store.add_folder / remove_folder / set_folder_archived
project.aliases_changed {project_id, action, alias, author} (action: add | remove) projects.store.add_alias / remove_alias
project.description_confirmed {project_id, revision_id} projects.store.confirm_description
component.health_changed {component_id, available, reason} tools.probe_all / reprobe_one (transition-only)
component.preference_changed {component_id, wanted, reason} health.preferences.apply_preference_updates
llm.call_logged {model, task_id, input_tokens, output_tokens, estimated_cost_usd, execution_mode, cached} llm.cost.log_call
cron.hot_reload {old_count, new_count} Scheduler._hot_reload (when fingerprints change)
user_job.created {name, file_path} api_user_job_create (after successful write)

Replay semantics

The in-process bus does not replay events from before a subscriber registered. The cross-process bridge drains every pending bus.event row currently in the messaging-service inbox on each poll, including events fired while the dashboard was down. Browser sees events from the in-process bus AFTER its EventSource reconnects. The browser's visibilitychange listener refreshes the active tab when it returns to foreground after a backgrounded period.

Refresh patterns per surface

Most surfaces use window._wbMorphReplace to merge fresh server-rendered HTML into the live container surgically — user state (focused inputs, scroll, drilled-in <details>) is preserved natively by morphdom. The projectsSurface currently uses a full refetch + re-render of the project list sidebar (the detail pane is untouched, so per-card inline edits are preserved). Upgrading projects to morphdom-merge is a planned follow-up.

Operational notes

  • SSE survives Werkzeug --dev and Tailscale Serve.
  • Multi-worker deployment is out of scope (each worker has its own bus; an external broker would be required).
  • Slow subscribers drop oldest events with an exposed counter rather than blocking publishers (per-subscriber deque capped at 1000).