# Foundation Plumbing — Design Spec ## Overview Complete four foundational features that are configured/stubbed but not yet implemented: notification delivery, recording playback, HLS.js library, and PIN verification. These are prerequisites for many other planned features. ## F1: Notification Delivery ### Web Push Sender The infrastructure is mostly in place: `pywebpush` is a dependency, push subscriptions are stored in the `push_subscriptions` table, `insert_alert_log` exists, and the service worker (`sw.js`) already handles incoming push events with notification display. **New file: `vigilar/alerts/sender.py`** `send_alert(engine, event_type, severity, source_id, payload)`: 1. Determine notification content from event type and payload (title, body, icon, URL) 2. Query all active push subscriptions from DB 3. For each subscription, call `pywebpush.webpush()` with VAPID credentials 4. Log each send attempt to `alert_log` table (channel=WEB_PUSH, status=SENT|FAILED, error if any) 5. Remove subscriptions that return 410 Gone (expired) VAPID keys: read from env var `VIGILAR_VAPID_PRIVATE_KEY` (primary), falling back to file at `StorageConfig.vapid_private_key_path` if env var is not set. **Notification content mapping:** | Event Type | Title | Body Example | |-----------|-------|-------------| | PERSON_DETECTED | Person Detected | "Person on Front Entrance" | | PET_ESCAPE | Pet Alert | "Angel detected on Back Deck (exterior)" | | UNKNOWN_ANIMAL | Unknown Animal | "Unknown cat on Front Entrance" | | WILDLIFE_PREDATOR | Wildlife Alert | "Bear detected — Front Entrance" | | WILDLIFE_NUISANCE | Wildlife | "Raccoon on Back Deck" | | UNKNOWN_VEHICLE_DETECTED | Unknown Vehicle | "Unknown vehicle on Front Entrance" | | POWER_LOSS | Power Alert | "UPS on battery" | | LOW_BATTERY | Battery Critical | "UPS battery low" | **Integration point:** In `EventProcessor._execute_action`, when action is `alert_all` or `push_and_record`, call `send_alert()` instead of (or in addition to) just publishing to MQTT. The `push_adults` action filters subscriptions by a `role` field (future — for now, send to all subscriptions for any push action). ### Audit Log Every alert-worthy event is already written to the `events` table. The `alert_log` table tracks notification delivery attempts. Additionally: - Configure a dedicated Python logger `vigilar.alerts` that writes to syslog via `logging.handlers.SysLogHandler` - Log format: `[{severity}] {event_type} on {source_id}: {summary}` - Logger is configured in `main.py` at startup, using the system's syslog socket (`/dev/log` on Linux) - This is a logging config addition, not a new module ## F2: Recording Playback & Download ### Current State - Recordings are written as H.264 MP4 by FFmpeg via `AdaptiveRecorder` - The `recordings` table has `encrypted` column (defaults to 1) and `file_path` - `StorageConfig.encrypt_recordings` defaults to True, `key_file` points to `/etc/vigilar/secrets/storage.key` - **No encryption is actually implemented yet** — files are written as plain MP4 despite the schema flag ### Design Since encryption isn't implemented in the recorder yet, implement it end-to-end: **Encryption (recorder side):** - After FFmpeg finishes writing an MP4 segment, encrypt the file using AES-256-CTR with the key from `VIGILAR_ENCRYPTION_KEY` env var - Use `cryptography.hazmat.primitives.ciphers` (AES-CTR) for streaming-friendly encryption (no need to buffer entire file) - Write encrypted file with `.vge` extension (Vigilar encrypted), delete the plain MP4 - Store a random 16-byte IV as the first 16 bytes of the `.vge` file - If `VIGILAR_ENCRYPTION_KEY` is not set and `encrypt_recordings` is True, log a warning and write plain MP4 (graceful degradation) **Key format:** 32-byte hex string in env var (64 hex characters). Decoded to 32 bytes for AES-256. **Decryption (playback side):** - `GET /recordings//download` reads the `.vge` file, extracts the IV from the first 16 bytes, decrypts with AES-256-CTR, streams as `video/mp4` - Response uses `Transfer-Encoding: chunked` to avoid buffering the entire file in memory - If the file is plain MP4 (not encrypted or `encrypted=0` in DB), stream directly **Recording list API:** - `GET /recordings/api/list` queries the `recordings` table with optional filters: `camera_id`, `date`, `detection_type`, `starred` - Returns JSON array of recording metadata (id, camera_id, started_at, ended_at, duration_s, detection_type, starred, file_size) **Recording delete:** - `DELETE /recordings/` removes the DB row and unlinks the file from disk - Returns 404 if recording not found ### Playback UI Integration The existing timeline component (`timeline.js`) renders clickable segments. Wire click events to load the recording in an HTML5 `