Notification delivery, recording playback/encryption, HLS.js bundle, and PIN verification with recovery passphrase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.9 KiB
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):
- Determine notification content from event type and payload (title, body, icon, URL)
- Query all active push subscriptions from DB
- For each subscription, call
pywebpush.webpush()with VAPID credentials - Log each send attempt to
alert_logtable (channel=WEB_PUSH, status=SENT|FAILED, error if any) - 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.alertsthat writes to syslog vialogging.handlers.SysLogHandler - Log format:
[{severity}] {event_type} on {source_id}: {summary} - Logger is configured in
main.pyat startup, using the system's syslog socket (/dev/logon 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
recordingstable hasencryptedcolumn (defaults to 1) andfile_path StorageConfig.encrypt_recordingsdefaults to True,key_filepoints 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_KEYenv var - Use
cryptography.hazmat.primitives.ciphers(AES-CTR) for streaming-friendly encryption (no need to buffer entire file) - Write encrypted file with
.vgeextension (Vigilar encrypted), delete the plain MP4 - Store a random 16-byte IV as the first 16 bytes of the
.vgefile - If
VIGILAR_ENCRYPTION_KEYis not set andencrypt_recordingsis 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/<id>/downloadreads the.vgefile, extracts the IV from the first 16 bytes, decrypts with AES-256-CTR, streams asvideo/mp4- Response uses
Transfer-Encoding: chunkedto avoid buffering the entire file in memory - If the file is plain MP4 (not encrypted or
encrypted=0in DB), stream directly
Recording list API:
GET /recordings/api/listqueries therecordingstable 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/<id>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 <video> element with the download URL as source. The browser handles MP4 playback natively.
F3: HLS.js Bundle
Download hls.js v1.5.x stable release (minified, ~200KB), commit to vigilar/web/static/js/hls.min.js, replacing the current placeholder that just logs to console.
The existing camera grid template already has the <script> tag and initialization code that expects Hls to be globally available. No template changes needed — just the library file.
F4: PIN Verification
PIN Storage
Two secrets stored as hashes in vigilar.toml:
[security]
pin_hash = "" # bcrypt hash of household PIN
recovery_passphrase_hash = "" # bcrypt hash of recovery passphrase
Hashed with hashlib.pbkdf2_hmac (SHA-256) — stdlib, no new dependency, sufficient for a local-only system. Store as pbkdf2_sha256$<hex_salt>$<hex_hash> string. 16-byte random salt per hash.
Config Model Addition
class SecurityConfig(BaseModel):
pin_hash: str = ""
recovery_passphrase_hash: str = ""
Add to VigilarConfig as security: SecurityConfig.
PIN Setup Flow
If pin_hash is empty (first run), the system is "unprotected" — arm/disarm works without PIN. The settings UI provides a "Set PIN" form that:
- Accepts new PIN + recovery passphrase
- Hashes both
- Writes hashes to config via the existing
config_writer.py
Endpoint Changes
POST /system/api/arm — Verify pin from request body against pin_hash. Return 401 if wrong. If pin_hash is empty, accept without PIN (backward compatible).
POST /system/api/disarm — Same PIN check.
POST /system/api/reset-pin — Accepts recovery_passphrase + new_pin. Verify passphrase against recovery_passphrase_hash. If valid, hash and store new pin_hash. Return 401 if passphrase wrong.
Audit Logging
All arm/disarm attempts (success and failure) written to arm_state_log table (already exists with pin_hash column). Failed attempts logged with the action attempted but NOT the submitted PIN value.
Dependencies
New Packages
None — all implementation uses existing dependencies (cryptography, pywebpush, py-vapid) and stdlib (hashlib, logging.handlers).
New Files
| File | Purpose |
|---|---|
vigilar/alerts/sender.py |
Web Push notification sender + audit logging |
vigilar/web/static/js/hls.min.js |
HLS.js library (replace placeholder) |
Modified Files
| File | Changes |
|---|---|
vigilar/events/processor.py |
Call send_alert() from _execute_action |
vigilar/camera/recorder.py |
Add AES-256-CTR encryption after recording |
vigilar/web/blueprints/recordings.py |
Implement list, download (decrypt), delete |
vigilar/web/blueprints/system.py |
PIN verification on arm/disarm, reset-pin endpoint |
vigilar/config.py |
Add SecurityConfig model |
vigilar/main.py |
Configure syslog handler for alerts logger |
Out of Scope
- Email and webhook notification channels (future)
- Rate limiting on PIN attempts / lockout (YAGNI for local system)
- Push subscription management UI (subscribe/unsubscribe) — the service worker and
push.jsalready handle this - Recording thumbnail generation (already has
thumbnail_pathcolumn — separate feature)