Files
vigilar/docs/architecture/subsystems/web.md
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

3.5 KiB

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.pycreate_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.