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>
163 lines
7.9 KiB
Markdown
163 lines
7.9 KiB
Markdown
# 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/<id>/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/<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`:
|
|
|
|
```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
|
|
|
|
```python
|
|
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:
|
|
1. Accepts new PIN + recovery passphrase
|
|
2. Hashes both
|
|
3. 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.js` already handle this
|
|
- Recording thumbnail generation (already has `thumbnail_path` column — separate feature)
|