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 boundeddeque+threading.Condition.events.publish(event_type, payload)— in-process publish.events.publish_cross_process(event_type, payload)— POSTsbus.eventto messaging service (port 5123).events.publish_auto(event_type, payload)— routes by process flag (mark_dashboard_process()set inservice.main()).events.start_heartbeat(interval, bus)— publishesbus.heartbeateveryintervalseconds (default 10 s).dashboard.messaging_bridge.start_messaging_bridge(...)— daemon polling the messaging service forbus.eventrows 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 byClarifyPool.submit/submit_rawfor 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-statusin 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
--devand 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
dequecapped at 1000).