Files
adlee-was-taken 09b59e3bb5 feat: wire MQTT → SSE bridge so the event timeline updates live
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>
2026-04-05 16:55:27 +00:00

44 lines
3.5 KiB
Markdown

# web
## Purpose
Flask application that exposes everything to humans: camera grid and single-camera views, event timeline, recordings browser, sensor dashboard, system/arm controls, kiosk mode, and the pets / visitors / wildlife journals. The web process runs as one `SubsystemProcess` supervised by `vigilar/main.py`, reads state directly from SQLite, and publishes a handful of config/control MQTT messages on behalf of the user.
## Key files
- `vigilar/web/app.py``create_app(cfg)` factory (note: `vigilar/web/__init__.py` is empty; the factory lives here)
- `vigilar/web/blueprints/cameras.py` — HLS streams, snapshots, per-camera view
- `vigilar/web/blueprints/events.py` — event log list, acknowledgement, and `text/event-stream` SSE endpoint for live updates
- `vigilar/web/blueprints/recordings.py` — browse, download, delete recordings; serves decrypted `.vge` content
- `vigilar/web/blueprints/sensors.py` — sensor status and history
- `vigilar/web/blueprints/system.py` — health, arm/disarm, UPS, admin settings, Web Push subscription registration, PIN management; publishes `vigilar/camera/{id}/config` for threshold updates
- `vigilar/web/blueprints/kiosk.py` — fullscreen 2x2 ambient grid
- `vigilar/web/blueprints/pets.py` — pet registration, sightings, labeling, training-image upload
- `vigilar/web/blueprints/visitors.py` — face profiles, visits, labeling, privacy controls
- `vigilar/web/blueprints/wildlife.py` — wildlife sightings journal, stats, CSV export
## MQTT topics
**Subscribes:** none. The web process does not subscribe to the bus at import time; the SSE endpoint accumulates messages in an in-process queue but nothing wires the MQTT bus into that queue today (see Notes).
**Publishes:**
- `vigilar/camera/{camera_id}/config` — runtime camera config updates (sensitivity / motion thresholds) from the system settings page
## Database tables
All access is read-mostly via `vigilar.storage.queries`. Touches essentially every table: `cameras`, `events`, `recordings`, `sensors`/`sensor_states`, `system_events`, `arm_state_log`, `alert_log`, `push_subscriptions`, `pets`/`pet_sightings`/`pet_training_images`/`pet_rules`, `face_profiles`/`face_embeddings`/`visits`, `wildlife_sightings`, `timelapse_schedules`.
## Depends on
- `storage` — engine passed via `app.config["DB_ENGINE"]` / `VIGILAR_CONFIG`
- `camera` — reads HLS segments from `cfg.system.hls_dir` and decrypts recordings via `vigilar.storage.encryption`
- `alerts` — uses `vigilar.alerts.pin.hash_pin`/`verify_pin` and manages `push_subscriptions`
- `config_writer` — persists camera/alert config changes back to TOML
## Consumed by
- Browsers and phones — HTML dashboard, PWA with Web Push (VAPID), HLS grid via `hls.js`, MJPEG fallback for single-camera low-latency view, SSE for the live event timeline
## Notes
Templates use Jinja2 with a Bootstrap 5 dark theme; the camera grid uses `hls.js` for multi-camera HLS playback with an MJPEG fallback on the single-camera page. The app is a PWA (service worker + manifest) with VAPID Web Push for mobile notifications. The event-timeline SSE endpoint lives around line 93 of `blueprints/events.py` and holds a per-client `queue.Queue` fed by a module-level `broadcast_sse_event` function. A dedicated MQTT → SSE bridge (`vigilar/web/sse_bridge.py`) runs inside the web process, subscribes to `Topics.EVENTS_PUBLISHED`, and calls `broadcast_sse_event` for every classified event emitted by the events subsystem — so the in-browser event timeline updates live without a page refresh.