Closes #1. The Flask event-timeline was dead: `broadcast_sse_event` existed in `vigilar/web/blueprints/events.py` but had zero call sites. Clients subscribed to `/events/stream`, received the initial "connected" message, and then only keepalives — a page refresh was required to see new events. (Web Push via VAPID was independent and already worked.) The root cause was a process-boundary gap: the events subsystem runs in its own OS process and emits to MQTT, while the Flask app runs in a separate process with no MQTT client of its own. This change adds a thin bridge: - EventProcessor._handle_event now publishes a classified summary (id, ts, type, severity, source_id, payload) to a new topic `Topics.EVENTS_PUBLISHED = "vigilar/events/published"` right after `insert_event()`. Classification logic stays in one place. - A new module `vigilar/web/sse_bridge.py` provides `forward_event` (MQTT handler) and `start_sse_bridge(cfg)` (creates a MessageBus, subscribes forward_event to EVENTS_PUBLISHED, connects, returns the bus). - `vigilar/main.py:_run_web` starts the bridge after `create_app(cfg)` and disconnects it on shutdown. Bridge failure is logged but does not kill the web process — the UI still works without live updates. - `create_app` is deliberately NOT changed. Keeping the bridge out of the app factory means no existing test triggers a real MQTT connection, and the bridge stays a production-only concern wired by the supervisor. Tests (all added with TDD, RED verified before GREEN): - tests/unit/test_events.py::TestEventsPublishedBroadcast — asserts `_handle_event` publishes the classified payload for a motion event and does NOT publish for unclassified topics (heartbeats). - tests/unit/test_sse_bridge.py — asserts `forward_event` reaches SSE subscribers, and `start_sse_bridge` wires the handler to `Topics.EVENTS_PUBLISHED` on a connected bus (fake bus, no real MQTT in tests). Also refreshes the docs that previously flagged the dead SSE as a known limitation (operator guide, web architecture doc). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
37 lines
1.3 KiB
Python
37 lines
1.3 KiB
Python
"""Bridge classified events from MQTT to Server-Sent Events subscribers.
|
|
|
|
The events subsystem (`vigilar.events.processor.EventProcessor`) publishes
|
|
classified events to `Topics.EVENTS_PUBLISHED`. The Flask web process runs
|
|
in its own OS process, so to make the in-browser event timeline update
|
|
live it must subscribe to that topic via its own `MessageBus` client and
|
|
forward every message to `broadcast_sse_event`.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from vigilar.bus import MessageBus
|
|
from vigilar.config import VigilarConfig
|
|
from vigilar.constants import Topics
|
|
from vigilar.web.blueprints.events import broadcast_sse_event
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def forward_event(topic: str, payload: dict[str, Any]) -> None:
|
|
"""MQTT handler: forward a classified event to SSE subscribers."""
|
|
broadcast_sse_event(payload)
|
|
|
|
|
|
def start_sse_bridge(cfg: VigilarConfig) -> MessageBus:
|
|
"""Create an MQTT client that forwards classified events to SSE clients.
|
|
|
|
Returns the connected `MessageBus` so the caller can disconnect it on
|
|
shutdown.
|
|
"""
|
|
bus = MessageBus(cfg.mqtt, client_id="vigilar-web-sse-bridge")
|
|
bus.subscribe(Topics.EVENTS_PUBLISHED, forward_event)
|
|
bus.connect()
|
|
log.info("SSE bridge started: subscribed to %s", Topics.EVENTS_PUBLISHED)
|
|
return bus
|