Compare commits
21 Commits
965dc3b13d
...
1633e8b34e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1633e8b34e | ||
|
|
f02dbaad8c | ||
|
|
e38a0dc174 | ||
|
|
68f9454a7c | ||
|
|
5e0d6c8320 | ||
|
|
c4d119310a | ||
|
|
07f5f341e6 | ||
|
|
58622722c7 | ||
|
|
843daf9c0b | ||
|
|
d3db384c35 | ||
|
|
62696e919c | ||
|
|
87d2df1446 | ||
|
|
c1779dfdb8 | ||
|
|
226a473d4d | ||
|
|
67b8dd672c | ||
|
|
c8d8421112 | ||
|
|
484235f74c | ||
|
|
d38b0c4e25 | ||
|
|
0e4e2c1ca7 | ||
|
|
1fd80ad31c | ||
|
|
4dc2db00e0 |
131
README.md
Normal file
131
README.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Vigilar
|
||||
|
||||
> DIY offline-first home security. Your cameras, your house, your data.
|
||||
|
||||
Vigilar runs on a small mini PC in your home, records from your IP cameras,
|
||||
detects motion, and pushes notifications to your phone — with no cloud, no
|
||||
account, and no external service in the critical path.
|
||||
|
||||
<!-- Screenshot placeholder: replace with a real UI grid shot when available. -->
|
||||

|
||||
|
||||
## Why Vigilar
|
||||
|
||||
- **Offline-first.** No cloud dependency. No external calls in the hot path.
|
||||
- **Multi-camera live view.** HLS grid for bandwidth efficiency, MJPEG single
|
||||
view for low-latency focus.
|
||||
- **On-device motion detection.** OpenCV MOG2 with a 5-second pre-motion ring
|
||||
buffer, so the run-up to a trigger is captured.
|
||||
- **Adaptive FPS.** Cameras idle at 2 FPS and jump to 30 FPS on motion.
|
||||
- **Event timeline and highlight reels.** Plus timelapses and visitor, pet,
|
||||
and wildlife tracking as separate views.
|
||||
- **Phone push notifications.** PWA + VAPID web push. No Firebase, no
|
||||
Google Cloud Messaging.
|
||||
- **Encrypted recordings at rest.** AES-256-CTR `.vge` files, key stays on
|
||||
your box. (Confidential, not tamper-evident — see the
|
||||
[Operator Guide](docs/operator-guide.md) for the details of the threat
|
||||
model.)
|
||||
- **UPS aware.** NUT integration, graceful shutdown on critical battery.
|
||||
- **Runs on a cheap mini PC.** 4 GB RAM, 128 GB SSD, any modern x86_64 box.
|
||||
|
||||
## Quick paths
|
||||
|
||||
| I want to… | Start here |
|
||||
|----------------------------------------------|--------------------------------------------------------------|
|
||||
| Set it up at home on a mini PC | [Home User Guide](docs/home-user-guide.md) |
|
||||
| Run it as a self-hoster / sysadmin | [Operator Guide](docs/operator-guide.md) |
|
||||
| Understand how it works internally | [Architecture Overview](docs/architecture/overview.md) |
|
||||
| Pick cameras that work well with it | [Camera Hardware Guide](docs/camera-hardware-guide.md) |
|
||||
|
||||
## 60-second overview
|
||||
|
||||
```
|
||||
[IP cameras] ──RTSP──▶ [mini PC running Vigilar] ──LAN──▶ [phone / browser]
|
||||
│
|
||||
└── optional nightly ──▶ [NAS]
|
||||
```
|
||||
|
||||
- **Language:** Python 3.11+
|
||||
- **Web:** Flask + Bootstrap 5 dark + `hls.js`
|
||||
- **Storage:** SQLite (WAL) via SQLAlchemy Core
|
||||
- **Bus:** Local Mosquitto on `127.0.0.1:1883`
|
||||
- **Video:** OpenCV (capture + motion), FFmpeg subprocess (recording, HLS)
|
||||
|
||||
## Status
|
||||
|
||||
**Alpha.** Works on the author's hardware. Expect rough edges and breaking
|
||||
changes. Not recommended for anyone who cannot read a stack trace.
|
||||
|
||||
Known limitations worth calling out up front:
|
||||
|
||||
- The in-browser event timeline does not yet update live. Push
|
||||
notifications to your phone work in real time; the timeline itself
|
||||
needs a page refresh.
|
||||
- Recording encryption is AES-256-CTR: confidential at rest, but not
|
||||
tamper-evident.
|
||||
- There is no automated database migration framework yet. Schema changes
|
||||
are forward-only.
|
||||
|
||||
See the [Operator Guide](docs/operator-guide.md) for the full list.
|
||||
|
||||
## Installation (TL;DR)
|
||||
|
||||
```bash
|
||||
git clone <repo URL> vigilar
|
||||
cd vigilar
|
||||
sudo ./scripts/install.sh
|
||||
sudo systemctl enable --now vigilar
|
||||
```
|
||||
|
||||
Then open `http://<mini-pc-ip>:49735` on your LAN.
|
||||
|
||||
For the full walkthrough (OS install, camera setup, PIN, push
|
||||
notifications, NAS backup), see the
|
||||
[Home User Guide](docs/home-user-guide.md).
|
||||
|
||||
For configuration, CLI, secrets, backups, and upgrades, see the
|
||||
[Operator Guide](docs/operator-guide.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
- **User guides**
|
||||
- [Home User Guide](docs/home-user-guide.md) — mini PC to working cameras,
|
||||
step by step.
|
||||
- [Operator Guide](docs/operator-guide.md) — configuration, CLI, backups,
|
||||
security, upgrades.
|
||||
- [Camera Hardware Guide](docs/camera-hardware-guide.md) — picking and
|
||||
wiring up cameras.
|
||||
- **Architecture**
|
||||
- [Overview](docs/architecture/overview.md) — process model, MQTT bus,
|
||||
data flow, storage layout.
|
||||
- [Conventions](docs/architecture/conventions.md) — coding rules for
|
||||
contributors.
|
||||
- [Subsystems](docs/architecture/subsystems/) — one short reference per
|
||||
subsystem: [camera](docs/architecture/subsystems/camera.md),
|
||||
[detection](docs/architecture/subsystems/detection.md),
|
||||
[events](docs/architecture/subsystems/events.md),
|
||||
[alerts](docs/architecture/subsystems/alerts.md),
|
||||
[sensors](docs/architecture/subsystems/sensors.md),
|
||||
[ups](docs/architecture/subsystems/ups.md),
|
||||
[storage](docs/architecture/subsystems/storage.md),
|
||||
[highlights](docs/architecture/subsystems/highlights.md),
|
||||
[presence](docs/architecture/subsystems/presence.md),
|
||||
[pets](docs/architecture/subsystems/pets.md),
|
||||
[health](docs/architecture/subsystems/health.md),
|
||||
[web](docs/architecture/subsystems/web.md).
|
||||
|
||||
## License
|
||||
|
||||
Vigilar is distributed under the **GNU General Public License v3.0**. See
|
||||
`LICENSE` for the full text. (If the `LICENSE` file is not yet in the tree,
|
||||
it will be added in a follow-up.)
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and pull requests are welcome. Before sending a PR:
|
||||
|
||||
- Read [`CLAUDE.md`](CLAUDE.md) for the project's code conventions, or the
|
||||
human-friendly distillation at
|
||||
[`docs/architecture/conventions.md`](docs/architecture/conventions.md).
|
||||
- Run `ruff check vigilar/` and `pytest` — both must pass.
|
||||
- Keep commits small and focused.
|
||||
67
docs/architecture/conventions.md
Normal file
67
docs/architecture/conventions.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Coding Conventions
|
||||
|
||||
These are the rules a contributor needs in their head before touching Vigilar.
|
||||
They are distilled from `CLAUDE.md` and from the patterns already in the codebase.
|
||||
|
||||
## Language and style
|
||||
|
||||
- Python 3.11+.
|
||||
- Ruff for linting. Line length 100 (see `pyproject.toml`).
|
||||
- Type hints on all public functions. Internal helpers may omit them when the
|
||||
types are obvious from context.
|
||||
- No docstrings unless the logic is non-obvious. Short, well-named functions
|
||||
should speak for themselves.
|
||||
|
||||
## String constants
|
||||
|
||||
All string constants referenced in more than one place live in
|
||||
`vigilar/constants.py`. The file uses `StrEnum` for enumerated values —
|
||||
`ArmState`, `Severity`, `EventType`, `SensorType`, `AlertChannel`,
|
||||
`RecordingTrigger`, and friends — and a `Topics` class for MQTT topic builders.
|
||||
|
||||
If you are about to write `"MOTION_START"` or `"vigilar/camera/..."` in a second
|
||||
place, stop and add it to the appropriate enum or the `Topics` class instead.
|
||||
Defaults like `DEFAULT_IDLE_FPS` also live in this module.
|
||||
|
||||
## Database access
|
||||
|
||||
- SQLite in WAL mode, opened through `vigilar/storage/db.py`.
|
||||
- SQLAlchemy Core expressions only — no mapped classes, no ORM session.
|
||||
- Schema lives in `vigilar/storage/schema.py`. Named query helpers live in
|
||||
`vigilar/storage/queries.py`.
|
||||
- New tables go in `schema.py`. New query helpers go in `queries.py`. Do not
|
||||
scatter ad-hoc SQL across subsystem code.
|
||||
|
||||
## Processes and the MQTT bus
|
||||
|
||||
- Each subsystem runs in its own `multiprocessing.Process`, spawned by the
|
||||
supervisor in `vigilar/main.py` via the `SubsystemProcess` wrapper. Cameras
|
||||
are the exception: `CameraManager` owns one child process per camera
|
||||
directly.
|
||||
- Subsystems communicate only through the local MQTT broker at
|
||||
`127.0.0.1:1883`. No direct imports or function calls across subsystem
|
||||
boundaries.
|
||||
- Topic naming: `vigilar/<domain>/<id>/<event>`. Every topic comes from the
|
||||
`Topics` class in `vigilar/constants.py` — do not construct topic strings
|
||||
ad hoc.
|
||||
- If you are tempted to reach into another subsystem directly, publish a
|
||||
topic instead.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `config/vigilar.toml` is the only config file the app reads at runtime.
|
||||
- It is validated by Pydantic v2 models in `vigilar/config.py`. Add new fields
|
||||
to the Pydantic models, not just to the TOML.
|
||||
- Secrets are file paths, not inline values. Never put a key, password, or
|
||||
token directly in the TOML.
|
||||
|
||||
## Testing
|
||||
|
||||
- `pytest` from the repo root.
|
||||
- Tests live under `tests/`.
|
||||
|
||||
## Committing
|
||||
|
||||
- `ruff check vigilar/` must pass.
|
||||
- `pytest` must pass.
|
||||
- One logical change per commit. Commit often.
|
||||
170
docs/architecture/overview.md
Normal file
170
docs/architecture/overview.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Vigilar Architecture Overview
|
||||
|
||||
This document explains how Vigilar is put together for someone reading the
|
||||
codebase for the first time. It is short on purpose — per-subsystem details
|
||||
live under `subsystems/`.
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Offline-first.** No external calls in the critical path. Cloud integrations,
|
||||
if any, are opt-in and off the hot path.
|
||||
- **Subsystem isolation.** Each subsystem runs in its own process. A crash in
|
||||
one subsystem cannot take down another — the supervisor in `vigilar/main.py`
|
||||
restarts crashed children with exponential backoff.
|
||||
- **Loose coupling via MQTT.** Subsystems do not call each other directly.
|
||||
They publish and subscribe to a local Mosquitto broker on `127.0.0.1:1883`.
|
||||
- **SQLite (WAL) is the single durable store.** Access goes through
|
||||
SQLAlchemy Core expressions (`vigilar/storage/schema.py`), not ORM mapped
|
||||
classes. WAL mode and `synchronous=NORMAL` are set on every connection in
|
||||
`vigilar/storage/db.py`.
|
||||
- **Adaptive cost.** Cameras idle at 2 FPS and jump to 30 FPS on motion, with
|
||||
a 5-second ring buffer so the moment leading up to the trigger is kept.
|
||||
- **Configuration is typed.** `config/vigilar.toml` is loaded and validated
|
||||
by Pydantic v2. Secrets are never inline — they are file paths under
|
||||
`/etc/vigilar/secrets/`.
|
||||
|
||||
## Process topology
|
||||
|
||||
`vigilar start` loads config and calls `run_supervisor()` in `vigilar/main.py`.
|
||||
The supervisor spawns every subsystem as a `multiprocessing.Process` (via the
|
||||
`SubsystemProcess` wrapper) and monitors them in a 2-second restart loop.
|
||||
Cameras are managed separately by `CameraManager`, which owns one child
|
||||
process per configured camera.
|
||||
|
||||
```
|
||||
systemd (vigilar.service)
|
||||
|
|
||||
v
|
||||
vigilar start (supervisor, main.py)
|
||||
|
|
||||
+--------+-----------+-----------+-----------+----------+
|
||||
| | | | | |
|
||||
v v v v v v
|
||||
web event- sensor- ups- presence- health-
|
||||
(Flask) processor bridge monitor monitor monitor
|
||||
|
||||
CameraManager --> camera worker (front_door)
|
||||
--> camera worker (backyard)
|
||||
--> camera worker (side_yard)
|
||||
--> camera worker (garage)
|
||||
|
||||
^ ^
|
||||
| MQTT |
|
||||
v v
|
||||
mosquitto (127.0.0.1:1883, loopback only)
|
||||
```
|
||||
|
||||
Every arrow touching the broker is a local TCP connection to loopback. The
|
||||
`web` process is a Flask server (`vigilar/web/app.py:create_app`) with one
|
||||
Blueprint per feature area under `vigilar/web/blueprints/`.
|
||||
|
||||
## The MQTT bus
|
||||
|
||||
- Broker: Mosquitto, bound to loopback only (see `systemd/vigilar-mosquitto.conf`:
|
||||
`listener 1883 127.0.0.1`, `allow_anonymous true`, `persistence false`).
|
||||
- Topic convention: every topic starts with `vigilar/` and is defined in
|
||||
`vigilar/constants.py` via the `Topics` class (either static strings or
|
||||
builder functions taking an ID). Real examples:
|
||||
- `vigilar/camera/{camera_id}/motion/start`
|
||||
- `vigilar/camera/{camera_id}/motion/end`
|
||||
- `vigilar/camera/{camera_id}/heartbeat`
|
||||
- `vigilar/sensor/{sensor_id}/{event_type}`
|
||||
- `vigilar/ups/status`, `vigilar/ups/power_loss`, `vigilar/ups/low_battery`
|
||||
- `vigilar/system/arm_state`, `vigilar/system/alert`
|
||||
- Wildcard subscriptions: `vigilar/#`, `vigilar/camera/#`, `vigilar/sensor/#`
|
||||
- Payloads are JSON dicts. Publishers use `bus.publish_event(topic, **kwargs)`
|
||||
from `vigilar/bus.py`; new fields are callers' responsibility.
|
||||
- Why MQTT rather than an in-process queue: crash isolation, introspection with
|
||||
`mosquitto_sub`, and the option to move subsystems to separate hosts later
|
||||
without changing the wire format.
|
||||
|
||||
## Data flow: from motion to phone notification
|
||||
|
||||
1. `vigilar/camera/worker.py:run_camera_worker` — opens the RTSP stream via
|
||||
`cv2.VideoCapture(..., cv2.CAP_FFMPEG)` with reconnect/backoff, pushes every
|
||||
frame into a ring buffer, and drives the capture loop.
|
||||
2. `vigilar/camera/motion.py:MotionDetector.detect` — MOG2 background
|
||||
subtraction on a downscaled frame; when a new motion edge is found,
|
||||
`worker.py` publishes `vigilar/camera/{camera_id}/motion/start` with
|
||||
confidence and zone count.
|
||||
3. `vigilar/camera/recorder.py:AdaptiveRecorder.start_motion_recording` —
|
||||
stops any idle recording, launches a fresh FFmpeg subprocess at
|
||||
`motion_fps` (default 30), and writes the flushed ring-buffer frames
|
||||
(default 5s of pre-roll) before the live frames. On stop, if
|
||||
`VIGILAR_ENCRYPTION_KEY` is set, the MP4 is re-encrypted in place to
|
||||
`.vge` via `vigilar/storage/encryption.py:encrypt_file` (AES-256-CTR).
|
||||
4. `vigilar/events/processor.py:EventProcessor._handle_event` — subscribes
|
||||
to `vigilar/#`, classifies the topic into an `EventType`/`Severity`, and
|
||||
writes a row to the `events` table via
|
||||
`vigilar/storage/queries.py:insert_event`. Wildlife and pet sightings also
|
||||
get rows in `wildlife_sightings` / `pet_sightings`.
|
||||
5. `vigilar/events/rules.py:RuleEngine.evaluate` — matches the event against
|
||||
configured `[[rules]]` from `vigilar.toml` (AND/OR on arm state, sensor
|
||||
event, camera motion, time window), honours per-rule cooldowns, and returns
|
||||
a list of actions.
|
||||
6. `vigilar/alerts/sender.py:send_alert` — for `alert_all` / `push_and_record`
|
||||
actions, builds a notification from the `_CONTENT_MAP` table, loads the
|
||||
VAPID key from `[alerts.web_push].vapid_private_key_file`, and calls
|
||||
`pywebpush.webpush` for every row in `push_subscriptions`. Successes and
|
||||
failures are recorded in `alert_log`; endpoints returning `410 Gone` are
|
||||
pruned.
|
||||
7. Web UI — the browser holds an open SSE connection to a handler in
|
||||
`vigilar/web/blueprints/events.py` (`mimetype="text/event-stream"`), which
|
||||
tails new event rows and pushes them to the timeline live.
|
||||
|
||||
## Storage layout
|
||||
|
||||
- `vigilar.db` under `[system] data_dir` (default `/var/vigilar/data`), SQLite
|
||||
in WAL mode. Tables defined in `vigilar/storage/schema.py`:
|
||||
`cameras`, `sensors`, `sensor_states`, `events`, `recordings`,
|
||||
`system_events`, `arm_state_log`, `alert_log`, `push_subscriptions`,
|
||||
`pets`, `pet_sightings`, `wildlife_sightings`, `package_events`,
|
||||
`pet_training_images`, `pet_rules`, `face_profiles`, `face_embeddings`,
|
||||
`visits`, `timelapse_schedules`.
|
||||
- Recordings: `.vge` files under `[system] recordings_dir` (default
|
||||
`/var/vigilar/recordings`), AES-256-CTR with a random 16-byte IV prefixed
|
||||
to each file. Key at `/etc/vigilar/secrets/storage.key`. **Losing the key
|
||||
means losing the recordings** — there is no recovery path.
|
||||
- HLS: rolling segments under `[system] hls_dir` (default `/var/vigilar/hls`),
|
||||
written by the per-camera `HLSStreamer` in `vigilar/camera/hls.py`.
|
||||
- Backups: DB + `/etc/vigilar` tarball via `scripts/backup.sh`.
|
||||
|
||||
## Configuration and secrets
|
||||
|
||||
- `config/vigilar.toml` is the only configuration file the app reads
|
||||
(systemd points `VIGILAR_CONFIG` at `/etc/vigilar/vigilar.toml` in
|
||||
production).
|
||||
- Validated by Pydantic v2 at startup (`vigilar/config.py`).
|
||||
- Secrets never live in the TOML; they are file paths under
|
||||
`/etc/vigilar/secrets/` (`storage.key`, `vapid_private.pem`).
|
||||
- The arm PIN and admin password are hashed; comparisons are constant-time
|
||||
(see `vigilar/alerts/pin.py`). The PIN hash is written into the TOML via
|
||||
`vigilar config set-pin`, never typed by hand.
|
||||
|
||||
## The web tier
|
||||
|
||||
- Flask with Blueprints, one per feature area under
|
||||
`vigilar/web/blueprints/`: `cameras`, `events`, `kiosk`, `pets`,
|
||||
`recordings`, `sensors`, `system`, `visitors`, `wildlife`. All registered
|
||||
in `vigilar/web/app.py:create_app`.
|
||||
- Jinja2 templates under `vigilar/web/templates/`, Bootstrap 5 dark theme,
|
||||
static assets under `vigilar/web/static/`.
|
||||
- Live view: `hls.js` grid for bandwidth efficiency, MJPEG single view for
|
||||
low latency.
|
||||
- Live timeline updates via Server-Sent Events from
|
||||
`vigilar/web/blueprints/events.py`.
|
||||
- PWA with VAPID web push — no Firebase, no Google Cloud Messaging. Service
|
||||
worker at `vigilar/web/static/sw.js`.
|
||||
|
||||
## What is NOT in the critical path
|
||||
|
||||
- Remote access (`[remote]` section) — optional, bandwidth-shaped HLS over
|
||||
a WireGuard tunnel.
|
||||
- Email alerts (`[alerts.email]`) and webhook alerts (`[alerts.webhook]`)
|
||||
— optional, off by default.
|
||||
- Any cloud service — never.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Conventions](conventions.md) — coding rules distilled for contributors.
|
||||
- [Subsystems](subsystems/) — per-subsystem references.
|
||||
35
docs/architecture/subsystems/alerts.md
Normal file
35
docs/architecture/subsystems/alerts.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# alerts
|
||||
|
||||
## Purpose
|
||||
|
||||
Delivers user-facing notifications for events the rule engine has flagged. Provides Web Push (VAPID) delivery to registered browsers and phones, PIN hashing/verification for arm/disarm flows, and a smart-profile matcher that selects recipients based on household state and time-of-day windows. Every alert attempt is logged through `vigilar.alerts` logger; the main supervisor attaches a dedicated syslog handler to that logger so alerts leave an OS-level audit trail.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/alerts/sender.py` — `send_alert`, `build_notification`, VAPID Web Push dispatcher; reads `push_subscriptions` and writes `alert_log`
|
||||
- `vigilar/alerts/profiles.py` — smart alert profile matcher (household state + time windows)
|
||||
- `vigilar/alerts/pin.py` — PBKDF2-SHA256 PIN hashing and constant-time verification
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** none directly. `alerts` is not a standalone subsystem process; it is invoked synchronously from the events processor when a rule action fires.
|
||||
**Publishes:** No MQTT publishers found at time of writing. The events processor publishes `vigilar/system/alert` on behalf of alert actions.
|
||||
|
||||
## Database tables
|
||||
|
||||
- `alert_log` — one row per delivery attempt (channel + status + error)
|
||||
- `push_subscriptions` — VAPID Web Push subscriptions, read to fan out a notification; stale entries are removed via `delete_push_subscription`
|
||||
|
||||
## Depends on
|
||||
|
||||
- `events` — `send_alert` is called from `EventProcessor._execute_action` for `alert_all` / `push_and_record`
|
||||
- `storage` — reads `push_subscriptions`, writes `alert_log`
|
||||
|
||||
## Consumed by
|
||||
|
||||
- Phones and browsers — receive Web Push notifications via VAPID
|
||||
- `web` — the `system` blueprint manages push subscription registration and PIN verification
|
||||
|
||||
## Notes
|
||||
|
||||
The `vigilar.alerts` logger gets a syslog handler installed by the supervisor (`vigilar/main.py`) so that every alert emission is mirrored into the host's syslog as an out-of-band audit trail, independent of SQLite. PIN verification uses `hmac.compare_digest` to avoid timing leaks. The smart-profile matcher honours `household_state` and HH:MM time windows (including windows that wrap past midnight).
|
||||
44
docs/architecture/subsystems/camera.md
Normal file
44
docs/architecture/subsystems/camera.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# camera
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns all per-camera capture, motion detection, recording, and live streaming. One worker process per configured camera pulls RTSP frames with OpenCV, runs MOG2 background-subtraction motion detection plus optional YOLO object detection, writes H.264 segments via FFmpeg, and exposes HLS for the web grid.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/camera/manager.py` — `CameraManager`, spawns and supervises one `multiprocessing.Process` per camera; polled by main supervisor via `check_and_restart()`
|
||||
- `vigilar/camera/worker.py` — per-camera capture loop: RTSP read, ring buffer push, motion detect, adaptive-FPS record, YOLO classification, MQTT publish
|
||||
- `vigilar/camera/motion.py` — MOG2-based `MotionDetector`
|
||||
- `vigilar/camera/recorder.py` — FFmpeg-subprocess recorder with idle and motion modes; calls `vigilar.storage.encryption.encrypt_file` to produce `.vge` output
|
||||
- `vigilar/camera/ring_buffer.py` — 5-second pre-motion frame buffer
|
||||
- `vigilar/camera/hls.py` — HLS segment writer for the web grid view
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** `vigilar/camera/{id}/config` (runtime threshold updates)
|
||||
**Publishes:**
|
||||
- `vigilar/camera/{id}/motion/start` (MOG2 trigger, and YOLO person/vehicle detections)
|
||||
- `vigilar/camera/{id}/motion/end`
|
||||
- `vigilar/camera/{id}/heartbeat`
|
||||
- `vigilar/camera/{id}/error`
|
||||
- `vigilar/camera/{id}/pet/detected`
|
||||
- `vigilar/camera/{id}/wildlife/detected`
|
||||
- `vigilar/pets/{pet_name}/location` (when a known pet is identified)
|
||||
|
||||
## Database tables
|
||||
|
||||
The camera subsystem does not write to SQLite directly; recording metadata and events are persisted by the `events` subsystem after consuming motion topics. Encrypted clips are written to disk by `recorder.py`.
|
||||
|
||||
## Depends on
|
||||
|
||||
- `storage` — uses `vigilar.storage.encryption.encrypt_file` to seal recordings as `.vge`
|
||||
- `detection` — optional YOLO detector, pet classifier, and wildlife threat classifier are loaded into the worker process
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `events` — subscribes to the whole bus and classifies motion/heartbeat/error topics into rows in `events` and `recordings`
|
||||
- `web` — reads HLS segments for the camera grid and decrypts recordings for playback
|
||||
|
||||
## Notes
|
||||
|
||||
`CameraManager` is the one subsystem NOT wrapped in `SubsystemProcess`; it owns its own child processes and is polled directly from the main supervisor loop. Adaptive FPS runs at 2 FPS idle and 30 FPS while motion is active, with a 5-second ring buffer flushed into the front of each motion clip for pre-event context. YOLO person/vehicle detections are published as extra `motion/start` messages with an additional `detection` field rather than on dedicated topics.
|
||||
44
docs/architecture/subsystems/detection.md
Normal file
44
docs/architecture/subsystems/detection.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# detection
|
||||
|
||||
## Purpose
|
||||
|
||||
Library of computer-vision models and classifiers that the camera worker loads in-process. Provides YOLO/SSD object detection, pet identification, face recognition, wildlife threat scoring, package-delivery state machine, vehicle fingerprinting, activity heatmaps, and sunset-aware scheduling. It is not itself a supervised subsystem; there is no `detection` process and no direct MQTT involvement — the camera worker invokes these modules and publishes on their behalf.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/detection/yolo.py` — YOLO object detector and category classifier (person/vehicle/pet/wildlife)
|
||||
- `vigilar/detection/person.py` — MobileNet-SSD v2 detector for person/car/truck
|
||||
- `vigilar/detection/pet_id.py` — MobileNetV3-Small pet identification classifier
|
||||
- `vigilar/detection/trainer.py` — transfer-learning trainer for pet-ID model
|
||||
- `vigilar/detection/face.py` — dlib/`face_recognition`-based face matcher against `face_profiles`/`face_embeddings`
|
||||
- `vigilar/detection/wildlife.py` — wildlife species + threat-level classifier
|
||||
- `vigilar/detection/package.py` — package-delivery state machine (`IDLE → PRESENT → REMINDED → COLLECTED`)
|
||||
- `vigilar/detection/vehicle.py` — dominant-color and size fingerprinting for vehicles
|
||||
- `vigilar/detection/zones.py` — polygon zones and intersection tests
|
||||
- `vigilar/detection/heatmap.py` — accumulates detection bboxes into a grid for activity heatmaps
|
||||
- `vigilar/detection/crop_manager.py` — saves detection crops into staging and training directories
|
||||
- `vigilar/detection/solar.py` — stdlib NOAA sunset calculator for twilight-aware logic
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** none. Detection modules are library calls inside the camera worker.
|
||||
**Publishes:** none directly. The camera worker publishes detection results on `vigilar/camera/{id}/motion/start` (person/vehicle, with an extra `detection` field), `vigilar/camera/{id}/pet/detected`, `vigilar/camera/{id}/wildlife/detected`, and `vigilar/pets/{pet_name}/location`.
|
||||
|
||||
## Database tables
|
||||
|
||||
No direct writes. `face.py` reads `face_profiles` and `face_embeddings` via `vigilar.storage.queries`.
|
||||
|
||||
## Depends on
|
||||
|
||||
- `storage` — `face.py` loads face profiles and embeddings
|
||||
- `camera` — invoked as a library inside the camera worker process
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `camera` — worker imports and calls these modules per frame
|
||||
- `pets` — downstream consumer of pet-ID results (via MQTT topics published by camera)
|
||||
- `events` — consumes the resulting detection topics off the bus
|
||||
|
||||
## Notes
|
||||
|
||||
Person and vehicle detections are piggybacked on `motion/start` rather than published on their own topics; the events processor currently collapses both back to `MOTION_START`. The package state machine in `package.py` is driven by the camera worker and uses `solar.get_sunset` to decide when a package has been "abandoned" after dusk.
|
||||
43
docs/architecture/subsystems/events.md
Normal file
43
docs/architecture/subsystems/events.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# events
|
||||
|
||||
## Purpose
|
||||
|
||||
Central classifier and persistence layer for everything that happens on the MQTT bus. Subscribes to the whole bus, parses topic paths into `EventType`/`Severity`/`source_id`, writes rows into `events`, evaluates the rule engine against the arm-state FSM, and executes resulting actions (send alert, broadcast system-alert, command all cameras to record).
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/events/processor.py` — `EventProcessor`: subscribes via `bus.subscribe_all`, classifies topics in `_classify_event` (a big if-chain, no routing table), writes via `insert_event`, executes rule actions
|
||||
- `vigilar/events/state.py` — `ArmStateFSM` (HOME/AWAY/NIGHT/DISARMED), publishes `Topics.SYSTEM_ARM_STATE`, persists to `arm_state_log`
|
||||
- `vigilar/events/rules.py` — `RuleEngine` evaluating TOML-configured rules against topic/payload/arm-state
|
||||
- `vigilar/events/history.py` — thin wrapper around `get_events`/`acknowledge_event`
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** `vigilar/#` (wildcard via `bus.subscribe_all`)
|
||||
**Publishes:**
|
||||
- `vigilar/system/arm_state` (from `ArmStateFSM`)
|
||||
- `vigilar/system/alert` (when a rule fires `alert_all`/`push_and_record`)
|
||||
- `vigilar/camera/{id}/command/record` (per-camera record command for `push_and_record`/`record_all_cameras`)
|
||||
|
||||
## Database tables
|
||||
|
||||
- `events` — canonical event log; every classified message becomes a row
|
||||
- `arm_state_log` — arm-state transitions written by the FSM
|
||||
- `pet_sightings` — inserted when classification produces `PET_DETECTED`, `PET_ESCAPE`, or `UNKNOWN_ANIMAL`
|
||||
- `wildlife_sightings` — inserted for `WILDLIFE_PREDATOR`/`NUISANCE`/`PASSIVE`
|
||||
|
||||
## Depends on
|
||||
|
||||
- Every publisher on the bus (camera, sensors, ups, presence, detection, health, highlights, etc.)
|
||||
- `storage` — uses `vigilar.storage.queries` for all inserts
|
||||
- `alerts` — calls `send_alert` from `vigilar.alerts.sender` when a rule requests it
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `alerts` — triggered indirectly via rule actions
|
||||
- `web` — reads `events` table through `vigilar.storage.queries` for the timeline and SSE stream
|
||||
- `highlights` — consumes the `events` table for reel scoring
|
||||
|
||||
## Notes
|
||||
|
||||
Classification is a hand-written if-chain keyed on the topic path. Camera `heartbeat` and `error` topics are intentionally ignored (return `None`). Person and vehicle YOLO detections ride on `camera/{id}/motion/start` and currently collapse back to a plain `MOTION_START` event — a known artefact of the publishing quirk in the camera worker. Arm-state change messages on `vigilar/system/*` are also deliberately ignored by the classifier to avoid feedback loops.
|
||||
39
docs/architecture/subsystems/health.md
Normal file
39
docs/architecture/subsystems/health.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# health
|
||||
|
||||
## Purpose
|
||||
|
||||
Self-monitoring and housekeeping. Periodically checks disk usage and MQTT reachability, publishes an aggregated system-health snapshot, auto-prunes old recordings when the disk gets full, builds a daily "what happened today" digest, and drives the scheduled firing of highlight-reel and timelapse jobs.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/health/monitor.py` — `HealthMonitor` subsystem loop: disk check every 5 min, MQTT port check every 30 s, status publish every 10 s, daily reel trigger, per-minute timelapse schedule check
|
||||
- `vigilar/health/pruner.py` — `auto_prune` / `find_prunable_recordings`; deletes oldest unstarred recordings (and their thumbnails) until usage is back under target
|
||||
- `vigilar/health/digest.py` — `build_digest`: counts person/vehicle events, pet and wildlife sightings, and recording totals over a time window for notification digests
|
||||
- `vigilar/health/__init__.py`
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** none
|
||||
**Publishes:**
|
||||
- `vigilar/system/health` — rolled-up health status with per-check breakdown
|
||||
|
||||
## Database tables
|
||||
|
||||
- `recordings` — read in pruner (`starred = 0`, oldest first) and deleted row-by-row once the file is unlinked
|
||||
- `events`, `pet_sightings`, `wildlife_sightings`, `recordings` — read in `digest.py`
|
||||
- `timelapse_schedules` — read indirectly via `vigilar.highlights.timelapse.check_schedules`
|
||||
|
||||
## Depends on
|
||||
|
||||
- `storage` — all DB access
|
||||
- `highlights` — calls `generate_daily_reel` and `check_schedules` on schedule
|
||||
- Filesystem — `shutil.disk_usage` on `system.data_dir`
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `web` — surfaces `vigilar/system/health` and the digest output in the system blueprint
|
||||
- `events` — ingests `vigilar/system/health` off the bus (subscribes to everything)
|
||||
|
||||
## Notes
|
||||
|
||||
The pruner is keyed on percentages, not gigabytes: it checks `config.health.disk_warn_pct` / `disk_critical_pct` for alerting and prunes toward `config.health.auto_prune_target_pct`. It never deletes a recording whose `starred` flag is set, and it unlinks the thumbnail alongside the video. The monitor owns the scheduling clock for both `generate_daily_reel` (fires when wall-clock matches `highlights.generate_time`) and timelapse generation (polls `check_schedules` every 60 s), so neither `highlights/` nor its cron is wired directly into the supervisor.
|
||||
35
docs/architecture/subsystems/highlights.md
Normal file
35
docs/architecture/subsystems/highlights.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# highlights
|
||||
|
||||
## Purpose
|
||||
|
||||
Generates derived video products from recorded footage: a daily highlight reel (events scored and assembled into a short FFmpeg-concatenated clip) and scheduled per-camera timelapses. These are offline/batch jobs that read the `events` and `recordings` tables and write new rows into `recordings` tagged with a synthetic trigger.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/highlights/reel.py` — event scoring map (`PET_ESCAPE=10`, `WILDLIFE_PREDATOR=9`, ...), clip selection, FFmpeg concat assembly, inserts resulting file via `insert_recording`
|
||||
- `vigilar/highlights/timelapse.py` — per-camera timelapse generator; reads `timelapse_schedules`, walks recordings within the configured hour window, stitches with FFmpeg, inserts a new `recordings` row
|
||||
- `vigilar/highlights/__init__.py`
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** No MQTT subscribers found at time of writing.
|
||||
**Publishes:** No MQTT publishers found at time of writing.
|
||||
|
||||
## Database tables
|
||||
|
||||
- `events` — read-only, source for reel scoring
|
||||
- `recordings` — read (source clips) and write (new reel / timelapse entries via `insert_recording`)
|
||||
- `timelapse_schedules` — read by `timelapse.py` to determine which cameras / hours / generation times are active
|
||||
|
||||
## Depends on
|
||||
|
||||
- `storage` — all table access via `vigilar.storage.queries` and `vigilar.storage.schema`
|
||||
- FFmpeg subprocess for clip concatenation and timelapse assembly
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `web` — the `recordings` blueprint surfaces reels and timelapses alongside regular recordings
|
||||
|
||||
## Notes
|
||||
|
||||
Scoring is a hand-tuned dict in `reel.py`. The top-scored events of the day are pulled, their surrounding recordings are concatenated via FFmpeg, and the result is persisted as a new `recordings` row with a `RecordingTrigger` tag so the UI can distinguish generated from captured clips. Timelapse generation is driven by `timelapse_schedules` rows — each row defines a camera, an hour window, and a `generate_time` at which the day's frames are stitched.
|
||||
34
docs/architecture/subsystems/pets.md
Normal file
34
docs/architecture/subsystems/pets.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# pets
|
||||
|
||||
## Purpose
|
||||
|
||||
Pet rule engine. Defines composable conditions (detected-in-zone, not-seen-for-N-minutes, detected-without-person, time-of-day, etc.) and per-pet state tracking that downstream code uses to trigger pet-specific alerts and automations. The on-camera pet identification itself lives in `vigilar/detection/pet_id.py`; this package is the behavioural layer on top of those sightings.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/pets/rules.py` — `PetState` dataclass, `evaluate_condition`, rule evaluator; reads `pet_rules` via `get_all_enabled_rules`
|
||||
- `vigilar/pets/__init__.py` (empty)
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** No MQTT subscribers found at time of writing. The module is invoked as a library — typically from the events processor or an evaluator that already has topic payloads in hand.
|
||||
**Publishes:** No MQTT publishers found at time of writing.
|
||||
|
||||
## Database tables
|
||||
|
||||
- `pet_rules` — loaded via `vigilar.storage.queries.get_all_enabled_rules`
|
||||
- `pets`, `pet_sightings`, `pet_training_images` — owned schema-wise by this feature area but written from elsewhere (events processor for sightings, detection/trainer for training images, web for pets CRUD)
|
||||
|
||||
## Depends on
|
||||
|
||||
- `storage` — reads `pet_rules`
|
||||
- `alerts.profiles` — reuses `is_in_time_window` for time-of-day conditions
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `events` / rule evaluator — drives pet-specific alert actions
|
||||
- `web` — the `pets` blueprint manages pets, training images, and rules
|
||||
|
||||
## Notes
|
||||
|
||||
`PetState` is per-pet, in-memory, and tracks last-seen camera and time, current zone with entry time, and whether a person is currently present alongside the pet. This makes rules like "cat in kitchen for more than 5 minutes while nobody is home" possible without joining SQLite on every frame. The condition schema is a plain dict with a `type` discriminator, so new condition kinds can be added without schema changes.
|
||||
36
docs/architecture/subsystems/presence.md
Normal file
36
docs/architecture/subsystems/presence.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# presence
|
||||
|
||||
## Purpose
|
||||
|
||||
Tracks whether household members are home by ICMP-pinging their phones on the LAN at a fixed interval. Derives an overall household state (EMPTY, OCCUPIED, etc.) from per-member presence and publishes both to MQTT so the rule engine and arm-state FSM can react.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/presence/monitor.py` — `PresenceMonitor`: ping loop, per-member state machine with `departure_delay_m` grace period, publishes per-member and household-level topics
|
||||
- `vigilar/presence/models.py` — `MemberPresence` dataclass and `derive_household_state` aggregator
|
||||
- `vigilar/presence/__init__.py`
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** none
|
||||
**Publishes:**
|
||||
- `vigilar/presence/{name}` — per-member status (`HOME` or `AWAY`, plus role)
|
||||
- `vigilar/presence/status` — aggregated household state and a `{name: HOME|AWAY}` map
|
||||
|
||||
## Database tables
|
||||
|
||||
none — presence state lives only in memory and on the bus.
|
||||
|
||||
## Depends on
|
||||
|
||||
- LAN reachability of configured `[presence.members]` IPs
|
||||
- `ping` binary on the host
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `events` — rules and the arm-state FSM react to household state changes
|
||||
- `alerts` — smart alert profile matcher in `vigilar/alerts/profiles.py` branches on `HouseholdState`
|
||||
|
||||
## Notes
|
||||
|
||||
A member is marked HOME the moment a ping succeeds, but only marked AWAY after `departure_delay_m` minutes without a successful ping — this prevents a single dropped reply from flipping the household state. The monitor uses `time.monotonic()` for the grace window, so it is immune to wall-clock jumps. Per-member and household topics are republished every poll, not only on transitions, so late subscribers always get current state.
|
||||
38
docs/architecture/subsystems/sensors.md
Normal file
38
docs/architecture/subsystems/sensors.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# sensors
|
||||
|
||||
## Purpose
|
||||
|
||||
Ingests events from physical sensors (Zigbee door/window contacts, motion PIRs, leak sensors, GPIO-attached hardware) and republishes them onto the internal Vigilar bus in a normalized form. Maintains the current sensor state in SQLite so the web UI and rule engine always know whether a contact is open/closed, a leak is active, etc.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/sensors/bridge.py` — `SensorBridge`: subscribes to `zigbee2mqtt/#`, normalizes payloads via `normalize_zigbee_payload`, republishes to `vigilar/sensor/{id}/{event_type}`
|
||||
- `vigilar/sensors/gpio_handler.py` — direct GPIO input handling; publishes the same `vigilar/sensor/{id}/{event_type}` topics
|
||||
- `vigilar/sensors/registry.py` — loads configured sensors, maps Zigbee friendly names to sensor IDs
|
||||
- `vigilar/sensors/models.py` — `SensorEvent` dataclass and protocol enum
|
||||
- `vigilar/sensors/__init__.py`
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** `{zigbee2mqtt.mqtt_topic_prefix}/#` (external Zigbee2MQTT broker, usually `zigbee2mqtt/#`)
|
||||
**Publishes:**
|
||||
- `vigilar/sensor/{id}/{event_type}` — normalized sensor events (contact_open, contact_closed, motion, leak, etc.)
|
||||
|
||||
## Database tables
|
||||
|
||||
- `sensors` — static sensor registry (read by `SensorRegistry`)
|
||||
- `sensor_states` — current-state table, kept up to date via `upsert_sensor_state`
|
||||
|
||||
## Depends on
|
||||
|
||||
- External Zigbee2MQTT broker (same Mosquitto instance, `zigbee2mqtt/...` topic prefix configurable via `[zigbee2mqtt]` TOML section)
|
||||
- `storage` — uses `vigilar.storage.queries.upsert_sensor_state`
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `events` — classifies `vigilar/sensor/#` messages into `CONTACT_OPEN`, `CONTACT_CLOSED`, `MOTION_START`, etc. and persists them to the `events` table
|
||||
- `web` — the `sensors` blueprint reads `sensors` / `sensor_states` for the dashboard
|
||||
|
||||
## Notes
|
||||
|
||||
`SensorBridge` deliberately skips `zigbee2mqtt/bridge/...` status messages and ignores unknown friendly names with a debug log. Both the Zigbee bridge and the GPIO handler produce identical `vigilar/sensor/{id}/{event_type}` topics so the rest of the system cannot tell a Zigbee door contact from a GPIO-wired one — that's intentional.
|
||||
51
docs/architecture/subsystems/storage.md
Normal file
51
docs/architecture/subsystems/storage.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# storage
|
||||
|
||||
## Purpose
|
||||
|
||||
The persistence layer. Defines every SQLite table, opens the WAL-mode database, exposes hand-written SQLAlchemy Core queries for the rest of the codebase, and provides the AES-256-CTR file encryption used to seal recordings as `.vge`. It is a library package, not a supervised subsystem process.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/storage/schema.py` — SQLAlchemy Core `MetaData` and 19 `Table` definitions
|
||||
- `vigilar/storage/db.py` — engine creation, WAL pragmas, `init_db`, `get_db_path`
|
||||
- `vigilar/storage/queries.py` — all `insert_*`/`get_*`/`update_*`/`delete_*` helpers
|
||||
- `vigilar/storage/encryption.py` — AES-256-CTR `encrypt_file` / `decrypt_stream` for `.vge` recordings
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** none
|
||||
**Publishes:** none
|
||||
|
||||
## Database tables
|
||||
|
||||
- `cameras` — configured camera registry
|
||||
- `sensors` — configured sensor registry
|
||||
- `sensor_states` — current key/value state per sensor
|
||||
- `events` — canonical event log written by the events processor
|
||||
- `recordings` — video clip metadata (path, duration, trigger, event_id, starred)
|
||||
- `system_events` — operator-facing component log (info/alert/critical)
|
||||
- `arm_state_log` — arm-state FSM transitions
|
||||
- `alert_log` — one row per alert delivery attempt and outcome
|
||||
- `push_subscriptions` — VAPID Web Push subscription endpoints
|
||||
- `pets` — known pets (name, species, breed, training count)
|
||||
- `pet_sightings` — per-frame pet observations with confidence and crop path
|
||||
- `wildlife_sightings` — wildlife observations with species, threat level, weather context
|
||||
- `package_events` — package delivery state machine rows (detected/reminded/collected)
|
||||
- `pet_training_images` — images staged for pet-ID model retraining
|
||||
- `pet_rules` — per-pet rule definitions with cooldown and priority
|
||||
- `face_profiles` — known faces, household flag, visit count
|
||||
- `face_embeddings` — serialized face vectors linked to a profile
|
||||
- `visits` — per-visit records for recognized faces
|
||||
- `timelapse_schedules` — per-camera timelapse generation schedules
|
||||
|
||||
## Depends on
|
||||
|
||||
- Filesystem: SQLite database under `system.data_dir`, AES key at `/etc/vigilar/secrets/storage.key`
|
||||
|
||||
## Consumed by
|
||||
|
||||
- Every other subsystem. Camera encrypts recordings through it, events/alerts/sensors/ups/pets/highlights/presence/web all import `vigilar.storage.queries`.
|
||||
|
||||
## Notes
|
||||
|
||||
Encryption is AES-256 in **CTR mode** (no GCM), so `.vge` files are confidential but **not tamper-evident** — there is no authentication tag and a bit-flip in the ciphertext produces a corresponding bit-flip in the plaintext without detection. The 16-byte IV is prepended to each file and the plaintext is unlinked after encryption. The codebase uses SQLAlchemy Core exclusively — there are no mapped ORM classes. The database is opened in WAL mode so the web process can read while the events processor writes.
|
||||
42
docs/architecture/subsystems/ups.md
Normal file
42
docs/architecture/subsystems/ups.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# ups
|
||||
|
||||
## Purpose
|
||||
|
||||
Polls a local NUT (Network UPS Tools) daemon for battery status and publishes power state transitions onto the MQTT bus. When the UPS runs below a configured critical-runtime threshold, triggers a graceful shutdown of the host.
|
||||
|
||||
## Key files
|
||||
|
||||
- `vigilar/ups/monitor.py` — `UPSMonitor`: NUT client, status parser, transition detection, MQTT publisher
|
||||
- `vigilar/ups/shutdown.py` — `ShutdownSequence`: publishes `SYSTEM_SHUTDOWN` and executes the OS shutdown
|
||||
- `vigilar/ups/__init__.py`
|
||||
|
||||
## MQTT topics
|
||||
|
||||
**Subscribes:** none
|
||||
**Publishes:**
|
||||
- `vigilar/ups/status` (every poll; status, battery charge %, runtime seconds, input voltage, load %)
|
||||
- `vigilar/ups/power_loss` (transition `OL -> OB`/`LB`)
|
||||
- `vigilar/ups/restored` (transition back to `OL`)
|
||||
- `vigilar/ups/low_battery` (charge below `low_battery_threshold_pct`)
|
||||
- `vigilar/ups/critical` (runtime below `critical_runtime_threshold_s`, triggers shutdown)
|
||||
- `vigilar/system/shutdown` (published by `ShutdownSequence`)
|
||||
|
||||
## Database tables
|
||||
|
||||
- `events` — writes `POWER_LOSS`, `POWER_RESTORED`, `LOW_BATTERY` rows via `insert_event`
|
||||
- `system_events` — writes operator-facing notices (`"Power loss detected — running on battery"`, critical shutdown messages)
|
||||
|
||||
## Depends on
|
||||
|
||||
- External NUT daemon reachable at `[ups] nut_host:nut_port` via the `pynut2` client
|
||||
- `storage` — `insert_event`, `insert_system_event`
|
||||
|
||||
## Consumed by
|
||||
|
||||
- `events` — classifies `vigilar/ups/*` into `POWER_LOSS` / `LOW_BATTERY` / `POWER_RESTORED`
|
||||
- `alerts` — receives those classified events through rule actions
|
||||
- `main` supervisor — reacts to `SYSTEM_SHUTDOWN` so all subsystems can drain cleanly
|
||||
|
||||
## Notes
|
||||
|
||||
The critical shutdown trigger fires when the UPS is not online AND `battery.runtime < ups.critical_runtime_threshold_s` AND no shutdown has already been triggered. The low-battery alert fires when `battery.charge < ups.low_battery_threshold_pct` while off mains. NUT reconnection uses exponential backoff capped at 120 seconds, so the monitor survives a temporary NUT restart without the supervisor having to restart it.
|
||||
43
docs/architecture/subsystems/web.md
Normal file
43
docs/architecture/subsystems/web.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 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 — that function is defined but has no call site in the repo at time of writing, so live SSE updates will only flow once the bridge from MQTT into that queue is wired.
|
||||
337
docs/home-user-guide.md
Normal file
337
docs/home-user-guide.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Vigilar Home User Guide
|
||||
|
||||
This guide takes you from a bare mini PC to working cameras on your phone.
|
||||
It assumes you are comfortable with a Linux command line and can SSH into
|
||||
a machine on your network.
|
||||
|
||||
If you are running Vigilar on a server you already administer, see the
|
||||
[Operator Guide](operator-guide.md) instead.
|
||||
|
||||
## What you will end up with
|
||||
|
||||
- A small always-on box in your house that records from your IP cameras.
|
||||
- A web UI at `http://<mini-pc-ip>:49735` on your LAN.
|
||||
- Push notifications on your phone when motion is detected.
|
||||
- Optional nightly config + database backup to a NAS share.
|
||||
|
||||
```
|
||||
[IP cameras] --RTSP--> [mini PC running Vigilar] --LAN--> [your phone/browser]
|
||||
|
|
||||
+-- optional nightly --> [NAS]
|
||||
```
|
||||
|
||||
## What you need
|
||||
|
||||
- A mini PC. x86_64, 4 GB RAM minimum, 128 GB SSD minimum. Any modern
|
||||
Intel NUC, Beelink, MinisForum, or similar will do.
|
||||
- A USB stick (8 GB or larger) for installing Linux.
|
||||
- One or more RTSP IP cameras on your LAN. See the
|
||||
[Camera Hardware Guide](camera-hardware-guide.md) for picks.
|
||||
- A phone for push notifications.
|
||||
- Optional: a NAS on your LAN for backups.
|
||||
|
||||
## Step 1 — Install Linux on the mini PC
|
||||
|
||||
Install Debian 12 or Ubuntu Server 24.04 (or later). The official installers
|
||||
walk you through it. During install:
|
||||
|
||||
- Choose **OpenSSH server** when asked for tasks/components.
|
||||
- Set a hostname you will remember (e.g. `vigilar`).
|
||||
- Create a user with `sudo`.
|
||||
|
||||
After install, note the LAN IP (`ip addr` on the box) and confirm you can
|
||||
SSH in from your laptop.
|
||||
|
||||
Arch Linux works too — the installer supports both `apt` and `pacman` —
|
||||
but the rest of this guide assumes a Debian/Ubuntu system.
|
||||
|
||||
## Step 2 — Install Vigilar
|
||||
|
||||
SSH into the mini PC, clone the repository, and run the installer:
|
||||
|
||||
```bash
|
||||
git clone <repo URL> vigilar
|
||||
cd vigilar
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
`install.sh` will:
|
||||
|
||||
- Install system packages (`ffmpeg`, `mosquitto`, `python3`, `nut-client`),
|
||||
create a locked-down `vigilar` system user, and lay out `/opt/vigilar`,
|
||||
`/var/vigilar`, and `/etc/vigilar` with the right permissions.
|
||||
- Build a Python virtualenv at `/opt/vigilar/venv`, install the `vigilar`
|
||||
package into it, generate a 32-byte storage encryption key at
|
||||
`/etc/vigilar/secrets/storage.key`, and drop a sample config at
|
||||
`/etc/vigilar/vigilar.toml`.
|
||||
- Install the `vigilar.service` systemd unit and a Mosquitto config that
|
||||
binds the internal MQTT broker to localhost only, then restart Mosquitto.
|
||||
|
||||
Review the `[INFO]` and `[OK]` output. Anything marked `[FAIL]` needs
|
||||
attention before you continue.
|
||||
|
||||
The installer finishes by printing a short list of follow-up scripts it
|
||||
does **not** run for you. The ones you want now are:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/gen_vapid_keys.sh
|
||||
```
|
||||
|
||||
That generates the VAPID keypair used for phone push notifications. If
|
||||
you skip it, cameras and the web UI will still work, but phone push
|
||||
will not.
|
||||
|
||||
The other optional follow-ups are `gen_cert.sh` (TLS certificates for
|
||||
HTTPS on the LAN) and `setup_nut.sh` (UPS battery monitoring). You can
|
||||
come back to those later.
|
||||
|
||||
## Step 3 — Edit the config and set a PIN
|
||||
|
||||
The installer drops a sample config at `/etc/vigilar/vigilar.toml`. Open
|
||||
it in your editor:
|
||||
|
||||
```bash
|
||||
sudoedit /etc/vigilar/vigilar.toml
|
||||
```
|
||||
|
||||
You'll add cameras from the web UI later, so for now you only need to
|
||||
set two things: the arm/disarm PIN and the web UI password.
|
||||
|
||||
### PIN
|
||||
|
||||
Vigilar stores the arm/disarm PIN as a keyed hash — there is no recovery.
|
||||
Generate the hash with the CLI:
|
||||
|
||||
```bash
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config set-pin
|
||||
```
|
||||
|
||||
Enter a PIN when prompted (twice). The command prints a line like:
|
||||
|
||||
```
|
||||
arm_pin_hash = "…:…"
|
||||
```
|
||||
|
||||
Copy that line into the `[system]` section of `/etc/vigilar/vigilar.toml`.
|
||||
|
||||
### Web UI password
|
||||
|
||||
Same idea for the password that protects the web UI:
|
||||
|
||||
```bash
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config set-password
|
||||
```
|
||||
|
||||
Paste the printed `password_hash = "…"` line into the `[web]` section.
|
||||
|
||||
### Validate
|
||||
|
||||
Before starting the service, make sure the config parses:
|
||||
|
||||
```bash
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config validate
|
||||
```
|
||||
|
||||
It should print `Config OK` and a short summary. If it prints
|
||||
`Config INVALID`, fix the file and try again.
|
||||
|
||||
## Step 4 — Start it up
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now vigilar
|
||||
sudo systemctl status vigilar
|
||||
```
|
||||
|
||||
You should see `active (running)`. Now open a browser on any LAN device:
|
||||
|
||||
```
|
||||
http://<mini-pc-ip>:49735
|
||||
```
|
||||
|
||||
Log in with the password you set in Step 3. The web port is configurable
|
||||
under `[web] port` in `/etc/vigilar/vigilar.toml` — the default is
|
||||
`49735`.
|
||||
|
||||
> Screenshot placeholder: `docs/images/first-run-login.png`
|
||||
|
||||
## Step 5 — Add your first camera
|
||||
|
||||
From the web UI, go to **Cameras → Add camera**. You'll need:
|
||||
|
||||
- A name (e.g. "Front door").
|
||||
- The camera's RTSP URL — see the
|
||||
[Camera Hardware Guide](camera-hardware-guide.md) for the format your
|
||||
brand uses.
|
||||
- Username and password for the RTSP stream (often separate from the
|
||||
camera's admin login).
|
||||
|
||||
Click **Test**, then **Save**. The camera appears in the grid. It idles
|
||||
at 2 FPS and jumps to 30 FPS when motion is detected, with a 5-second
|
||||
pre-motion ring buffer so you don't lose the run-up to an event.
|
||||
|
||||
> Screenshot placeholder: `docs/images/add-camera.png`
|
||||
|
||||
Repeat for each camera. The multi-camera grid uses HLS so a handful of
|
||||
streams are comfortable over LAN Wi-Fi; the single-camera view falls
|
||||
back to MJPEG for lower latency.
|
||||
|
||||
## Step 6 — Phone push notifications
|
||||
|
||||
Vigilar is a Progressive Web App, so your phone installs it straight
|
||||
from the browser:
|
||||
|
||||
1. Open `http://<mini-pc-ip>:49735` in your phone's browser on the
|
||||
same Wi-Fi.
|
||||
2. Log in.
|
||||
3. Use your browser's "Add to Home Screen" option (Safari: share sheet;
|
||||
Chrome: three-dot menu).
|
||||
4. Open the installed app from your home screen.
|
||||
5. When prompted, allow notifications.
|
||||
|
||||
As long as `gen_vapid_keys.sh` was run back in Step 2, you do not need
|
||||
to configure anything else. The next motion event will push a
|
||||
notification to your phone.
|
||||
|
||||
**Note on the event timeline:** the in-browser event list does not
|
||||
update live today — you need to refresh the page to see new events.
|
||||
Push notifications to your phone work independently and do arrive in
|
||||
real time. Fixing live timeline updates in the browser is on the
|
||||
roadmap.
|
||||
|
||||
## Step 7 — Optional: NAS backup
|
||||
|
||||
Vigilar ships a `backup.sh` script that tars the SQLite database and
|
||||
the contents of `/etc/vigilar` (config + secrets) once a day. It does
|
||||
**not** back up recordings — those stay on the mini PC's local disk.
|
||||
Recording backup is planned as a later improvement.
|
||||
|
||||
### Mount the NAS share
|
||||
|
||||
NFS example (swap in SMB if that's what your NAS speaks):
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y nfs-common
|
||||
sudo mkdir -p /mnt/nas/vigilar-backups
|
||||
echo "nas.lan:/volume1/vigilar-backups /mnt/nas/vigilar-backups nfs defaults,_netdev 0 0" | sudo tee -a /etc/fstab
|
||||
sudo mount -a
|
||||
```
|
||||
|
||||
Replace `nas.lan:/volume1/vigilar-backups` with your NAS's actual host
|
||||
and export path. Confirm it mounted:
|
||||
|
||||
```bash
|
||||
mount | grep vigilar-backups
|
||||
```
|
||||
|
||||
### Schedule `backup.sh`
|
||||
|
||||
Create `/etc/systemd/system/vigilar-backup.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Vigilar nightly backup
|
||||
After=vigilar.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=VIGILAR_BACKUP_DIR=/mnt/nas/vigilar-backups
|
||||
Environment=VIGILAR_BACKUP_RETENTION_DAYS=30
|
||||
ExecStart=/opt/vigilar/scripts/backup.sh
|
||||
```
|
||||
|
||||
Create `/etc/systemd/system/vigilar-backup.timer`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Run vigilar backup nightly
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
Enable the timer:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now vigilar-backup.timer
|
||||
sudo systemctl list-timers | grep vigilar
|
||||
```
|
||||
|
||||
Test it immediately without waiting for midnight:
|
||||
|
||||
```bash
|
||||
sudo systemctl start vigilar-backup.service
|
||||
ls -lh /mnt/nas/vigilar-backups/
|
||||
```
|
||||
|
||||
You should see a `vigilar-backup-<timestamp>.tar.gz` file. Thirty days
|
||||
of these will rotate automatically thanks to
|
||||
`VIGILAR_BACKUP_RETENTION_DAYS`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### The web UI will not load
|
||||
|
||||
- `sudo systemctl status vigilar` — is it running?
|
||||
- `journalctl -u vigilar -e` — what does the last error say?
|
||||
- Port 49735 blocked or bound elsewhere?
|
||||
`sudo ss -tlnp | grep 49735`.
|
||||
- Wrong address? On the mini PC, `ip addr` shows its LAN IP.
|
||||
|
||||
### A camera will not connect
|
||||
|
||||
- Try the RTSP URL in VLC from your laptop (Media → Open Network
|
||||
Stream). If VLC cannot play it either, it is a camera or network
|
||||
problem, not Vigilar.
|
||||
- Double-check credentials — many cameras have a separate RTSP login
|
||||
distinct from the admin web login.
|
||||
- Some cameras need a specific stream path (e.g. `/stream1`,
|
||||
`/cam/realmonitor?channel=1`). See the
|
||||
[Camera Hardware Guide](camera-hardware-guide.md).
|
||||
|
||||
### No push notifications on my phone
|
||||
|
||||
- Did you run `sudo ./scripts/gen_vapid_keys.sh`? `install.sh` does
|
||||
not do this for you.
|
||||
- Open the PWA from your home screen, not a browser tab. iOS in
|
||||
particular only delivers web push to installed PWAs.
|
||||
- Check the browser's notification permission for the site.
|
||||
- Remember: the in-browser event timeline is not live. Refresh the
|
||||
page to see events land. Push is the real-time channel.
|
||||
|
||||
### Service keeps restarting
|
||||
|
||||
- `journalctl -u vigilar -n 100` and look at the last stack trace.
|
||||
- Common cause: invalid `vigilar.toml`. Validate it:
|
||||
```bash
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config validate
|
||||
```
|
||||
- The supervisor will give up after ten consecutive restarts of the
|
||||
same subsystem, so a persistent crash loop eventually stops on its
|
||||
own — check the logs and fix the underlying cause.
|
||||
|
||||
### I forgot my PIN
|
||||
|
||||
There is no recovery — the PIN is only stored as a hash. Generate a new
|
||||
one and replace the line in `/etc/vigilar/vigilar.toml`:
|
||||
|
||||
```bash
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config set-pin
|
||||
sudoedit /etc/vigilar/vigilar.toml # replace arm_pin_hash in [system]
|
||||
sudo systemctl restart vigilar
|
||||
```
|
||||
|
||||
The same trick works for a forgotten web UI password — use
|
||||
`config set-password` instead and update `password_hash` in `[web]`.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Operator Guide](operator-guide.md) — configuration reference, backups, upgrades, security.
|
||||
- [Camera Hardware Guide](camera-hardware-guide.md) — buying advice and
|
||||
RTSP URL formats per brand.
|
||||
- [Architecture Overview](architecture/overview.md) — how Vigilar is put
|
||||
together under the hood.
|
||||
667
docs/operator-guide.md
Normal file
667
docs/operator-guide.md
Normal file
@@ -0,0 +1,667 @@
|
||||
# Vigilar Operator Guide
|
||||
|
||||
## Audience and scope
|
||||
|
||||
This guide is for administrators installing and operating Vigilar on a
|
||||
server they already manage. It is a reference for the on-disk layout,
|
||||
configuration keys, CLI, systemd integration, secrets, UPS integration,
|
||||
backups, upgrades, health and pruning, remote access, and the current
|
||||
set of known limitations.
|
||||
|
||||
If you are setting up a home system from a bare mini PC for the first
|
||||
time, start with the [Home User Guide](home-user-guide.md) and return
|
||||
here when you need reference-level detail.
|
||||
|
||||
## Layout on disk
|
||||
|
||||
`scripts/install.sh` lays the system out as follows. The paths are
|
||||
fixed in the installer; the configuration keys that reference them are
|
||||
shown in parentheses.
|
||||
|
||||
| Path | Owner | Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/opt/vigilar/` | `vigilar:vigilar` | 0755 | Install root and home of the service user |
|
||||
| `/opt/vigilar/venv/` | `vigilar:vigilar` | 0755 | Python virtual environment with the `vigilar` entry point |
|
||||
| `/etc/vigilar/` | `root:root` | 0755 | Configuration root |
|
||||
| `/etc/vigilar/vigilar.toml` | `root:vigilar` | 0644 | Main config file (`VIGILAR_CONFIG`) |
|
||||
| `/etc/vigilar/secrets/` | `root:root` | 0700 | Storage key, VAPID private key |
|
||||
| `/etc/vigilar/secrets/storage.key` | `root:root` | 0600 | 32-byte AES-256 key for recording encryption |
|
||||
| `/etc/vigilar/secrets/vapid_private.pem` | `root:root` | 0600 | VAPID signing key for Web Push |
|
||||
| `/etc/vigilar/secrets/vapid_public.txt` | `root:vigilar` | 0644 | VAPID public key (base64url) |
|
||||
| `/etc/vigilar/certs/` | `root:vigilar` | 0750 | TLS material |
|
||||
| `/etc/vigilar/certs/cert.pem` | `root:vigilar` | 0644 | TLS certificate (`[web] tls_cert`) |
|
||||
| `/etc/vigilar/certs/key.pem` | `root:vigilar` | 0640 | TLS private key (`[web] tls_key`) |
|
||||
| `/var/vigilar/` | `vigilar:vigilar` | 0750 | Runtime data root |
|
||||
| `/var/vigilar/data/` | `vigilar:vigilar` | 0750 | SQLite database and supporting files (`[system] data_dir`) |
|
||||
| `/var/vigilar/data/vigilar.db` | `vigilar:vigilar` | 0640 | Main SQLite database (WAL mode) |
|
||||
| `/var/vigilar/recordings/` | `vigilar:vigilar` | 0750 | `.vge` encrypted recordings and thumbnails (`[system] recordings_dir`) |
|
||||
| `/var/vigilar/hls/` | `vigilar:vigilar` | 0750 | HLS segments and playlists (`[system] hls_dir`) |
|
||||
| `/etc/systemd/system/vigilar.service` | `root:root` | 0644 | Systemd unit |
|
||||
| `/etc/mosquitto/conf.d/vigilar.conf` | `root:root` | 0644 | Localhost-only MQTT config |
|
||||
|
||||
The `VIGILAR_CONFIG` environment variable is read by the CLI and the
|
||||
web blueprints to locate `vigilar.toml`; the systemd unit sets it to
|
||||
`/etc/vigilar/vigilar.toml`.
|
||||
|
||||
## Installation
|
||||
|
||||
`scripts/install.sh` is idempotent and supports Debian/Ubuntu (apt) and
|
||||
Arch Linux (pacman). It performs eight phases:
|
||||
|
||||
1. **System dependencies.** On apt: `ffmpeg mosquitto python3
|
||||
python3-venv python3-pip nut-client`. On pacman: `ffmpeg mosquitto
|
||||
python python-virtualenv nut`.
|
||||
2. **System user.** Creates the `vigilar` system user and group with
|
||||
`/opt/vigilar` as the home directory and `/usr/sbin/nologin` as the
|
||||
shell.
|
||||
3. **Directories and permissions.** Creates `/var/vigilar/{data,
|
||||
recordings,hls}` owned by `vigilar:vigilar` at 0750, plus
|
||||
`/etc/vigilar/{secrets,certs}` with the modes shown above.
|
||||
4. **Python venv.** Creates `/opt/vigilar/venv` as the `vigilar` user
|
||||
and installs the project in place with `pip install "${PROJECT_DIR}"`.
|
||||
5. **Storage encryption key.** Writes 32 random bytes from
|
||||
`/dev/urandom` to `/etc/vigilar/secrets/storage.key` if it does not
|
||||
already exist. This file is never rewritten by the installer.
|
||||
6. **Sample config.** Copies the repository's `config/vigilar.toml` to
|
||||
`/etc/vigilar/vigilar.toml` if a config does not already exist.
|
||||
7. **Systemd unit.** Installs and enables `vigilar.service`.
|
||||
8. **Mosquitto.** Installs `systemd/vigilar-mosquitto.conf` to
|
||||
`/etc/mosquitto/conf.d/vigilar.conf` and restarts `mosquitto.service`.
|
||||
|
||||
The installer prints recommended follow-up steps: edit the TOML, then
|
||||
run `gen_cert.sh`, `gen_vapid_keys.sh`, and `setup_nut.sh`, then start
|
||||
the service.
|
||||
|
||||
### Systemd unit
|
||||
|
||||
`vigilar.service` runs `/opt/vigilar/venv/bin/vigilar start --config
|
||||
/etc/vigilar/vigilar.toml` as `vigilar:vigilar` with
|
||||
`VIGILAR_CONFIG=/etc/vigilar/vigilar.toml`. It requires
|
||||
`mosquitto.service`, wants `nut-monitor.service`, and uses
|
||||
`Restart=on-failure`, `RestartSec=10`, and `WatchdogSec=120`. The unit
|
||||
applies `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`,
|
||||
`PrivateDevices`, `NoNewPrivileges`, and a `@system-service` syscall
|
||||
filter. `ReadWritePaths` is limited to `/var/vigilar/{data,recordings,
|
||||
hls}`; `/etc/vigilar` is mounted read-only. Output goes to the journal
|
||||
with `SyslogIdentifier=vigilar`.
|
||||
|
||||
### Mosquitto configuration
|
||||
|
||||
`vigilar-mosquitto.conf` binds a single listener on `127.0.0.1:1883`,
|
||||
allows anonymous connections (localhost only), disables persistence
|
||||
(all state lives in SQLite), and logs errors, warnings, notices, and
|
||||
connection events to syslog. Vigilar never authenticates to the broker
|
||||
and never exposes it beyond loopback.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
`config/vigilar.toml` is parsed by `tomllib`, then validated by the
|
||||
Pydantic models in `vigilar/config.py`. The models are the source of
|
||||
truth: any unknown key is rejected, and each section has a default so
|
||||
omitted sections behave sensibly.
|
||||
|
||||
### `[system]`
|
||||
|
||||
- `name` (default `"Vigilar Home Security"`): display name used in
|
||||
logs and the web UI.
|
||||
- `timezone` (default `"UTC"`; sample ships as
|
||||
`"America/New_York"`): used for daily digests, highlight scheduling,
|
||||
and timestamped file paths.
|
||||
- `data_dir` (default `/var/vigilar/data`): SQLite database and
|
||||
derived state.
|
||||
- `recordings_dir` (default `/var/vigilar/recordings`): encrypted
|
||||
`.vge` files.
|
||||
- `hls_dir` (default `/var/vigilar/hls`): HLS segment output.
|
||||
- `log_level` (default `"INFO"`): one of DEBUG, INFO, WARNING, ERROR.
|
||||
- `arm_pin_hash` (default `""`): commented out in the sample; set via
|
||||
`vigilar config set-pin`.
|
||||
|
||||
### `[mqtt]`
|
||||
|
||||
- `host` (default `127.0.0.1`) and `port` (default `1883`): broker
|
||||
address. Leave on loopback unless you deliberately run a shared
|
||||
broker.
|
||||
- `username`, `password` (default `""`): unused by the shipped
|
||||
mosquitto config, present for operators who run their own broker.
|
||||
|
||||
### `[web]`
|
||||
|
||||
- `host` (default `0.0.0.0`) and `port` (default `49735`): Flask
|
||||
listener. Change `host` to `127.0.0.1` if you front with a reverse
|
||||
proxy.
|
||||
- `tls_cert`, `tls_key` (default `""`): PEM paths. `gen_cert.sh`
|
||||
fills these in.
|
||||
- `username` (default `"admin"`): web UI login name.
|
||||
- `password_hash` (default `""`): scrypt hash set via `vigilar config
|
||||
set-password`.
|
||||
- `session_timeout` (default `3600` seconds).
|
||||
|
||||
### `[zigbee2mqtt]`
|
||||
|
||||
- `mqtt_topic_prefix` (default `"zigbee2mqtt"`): used when subscribing
|
||||
to sensor topics from an external Zigbee2MQTT bridge.
|
||||
|
||||
### `[ups]`
|
||||
|
||||
See also the UPS/NUT section below.
|
||||
|
||||
- `enabled` (default `true`).
|
||||
- `nut_host` (default `127.0.0.1`), `nut_port` (default `3493`),
|
||||
`ups_name` (default `"ups"`): matches the `[ups]` block generated by
|
||||
`setup_nut.sh`.
|
||||
- `poll_interval_s` (default `30`).
|
||||
- `low_battery_threshold_pct` (default `20`, range 5–95).
|
||||
- `critical_runtime_threshold_s` (default `300`).
|
||||
- `shutdown_delay_s` (default `60`).
|
||||
|
||||
### `[storage]`
|
||||
|
||||
- `encrypt_recordings` (default `true`): toggles AES-256-CTR
|
||||
encryption of new `.vge` files. Changing this does not re-encrypt
|
||||
existing recordings.
|
||||
- `key_file` (default `/etc/vigilar/secrets/storage.key`): 32-byte
|
||||
raw key.
|
||||
- `max_disk_usage_gb` (default `200`) and `free_space_floor_gb`
|
||||
(default `10`): **legacy keys**. They are defined on the Pydantic
|
||||
model and exposed in the settings UI, but no pruning or recording
|
||||
code currently reads them. The real disk ceiling is the
|
||||
percentage-based pruner in `[health]`. Do not rely on these two
|
||||
fields to cap disk usage today.
|
||||
|
||||
### `[remote]`
|
||||
|
||||
- `enabled` (default `false`): turns on the remote-access bridge.
|
||||
- `upload_bandwidth_mbps` (default `22.0`): informational ceiling.
|
||||
- `remote_hls_resolution` (default `[426, 240]`), `remote_hls_fps`
|
||||
(default `10`), `remote_hls_bitrate_kbps` (default `500`): quality
|
||||
profile for HLS served over the tunnel.
|
||||
- `max_remote_viewers` (default `4`; `0` = unlimited).
|
||||
- `tunnel_ip` (default `"10.99.0.2"`): WireGuard address of the home
|
||||
server, for display only.
|
||||
|
||||
### `[alerts.local]`
|
||||
|
||||
- `enabled` (default `true`).
|
||||
- `syslog` (default `true`): the supervisor installs a `SysLogHandler`
|
||||
on the `vigilar.alerts` logger when this is true.
|
||||
- `desktop_notify` (default `false`): `notify-send` fallback for
|
||||
operator-console deployments.
|
||||
|
||||
### `[alerts.web_push]`
|
||||
|
||||
- `enabled` (default `true`).
|
||||
- `vapid_private_key_file` (default
|
||||
`/etc/vigilar/secrets/vapid_private.pem`).
|
||||
- `vapid_claim_email` (default `"mailto:admin@vigilar.local"`): used
|
||||
as the VAPID `sub` claim.
|
||||
|
||||
### `[alerts.email]`
|
||||
|
||||
- `enabled` (default `false`).
|
||||
- `smtp_host`, `smtp_port` (default `587`), `from_addr`, `to_addr`,
|
||||
`use_tls` (default `true`).
|
||||
|
||||
### `[alerts.webhook]`
|
||||
|
||||
- `enabled` (default `false`).
|
||||
- `url`, `secret`: HMAC secret signs outbound webhook bodies.
|
||||
|
||||
### `[[cameras]]` (array of tables)
|
||||
|
||||
One block per camera. Keys:
|
||||
|
||||
- `id`, `display_name`, `rtsp_url`: required.
|
||||
- `enabled` (default `true`).
|
||||
- `record_continuous` (default `false`), `record_on_motion` (default
|
||||
`true`).
|
||||
- `motion_sensitivity` (default `0.7`, range 0.0–1.0) and
|
||||
`motion_min_area_px` (default `500`).
|
||||
- `motion_zones`, `zones`: polygon and named-zone overrides.
|
||||
- `pre_motion_buffer_s` (default `5`) and `post_motion_buffer_s`
|
||||
(default `30`).
|
||||
- `idle_fps` (default `2`, range 1–30) and `motion_fps` (default
|
||||
`30`, range 1–60): the adaptive FPS pair.
|
||||
- `retention_days` (default `30`).
|
||||
- `resolution_capture` (default `[1920, 1080]`) and
|
||||
`resolution_motion` (default `[640, 360]`): capture size and the
|
||||
downscale used for MOG2 motion detection.
|
||||
- `location` (default `INTERIOR`): `CameraLocation` enum, used for
|
||||
alert profiles.
|
||||
|
||||
Camera IDs must be unique; the Pydantic root validator rejects
|
||||
duplicates.
|
||||
|
||||
### `[[sensors]]` and `[sensors.gpio]`
|
||||
|
||||
Each `[[sensors]]` block has `id`, `display_name`, `type` (e.g.
|
||||
`CONTACT`, `MOTION`, `TEMPERATURE`), `protocol` (`ZIGBEE`, `ZWAVE`,
|
||||
`GPIO`), `device_address`, `location`, and `enabled` (default
|
||||
`true`). `[sensors.gpio] bounce_time_ms` (default `50`) applies to all
|
||||
GPIO sensors. Sensor IDs must also be unique.
|
||||
|
||||
### `[[rules]]`
|
||||
|
||||
Each rule has `id`, `description`, `conditions` (list of `{type,
|
||||
value, sensor_id, event}` maps), `logic` (`AND` or `OR`, default
|
||||
`AND`), `actions` (list of action names like `alert_all` or
|
||||
`record_all_cameras`), and `cooldown_s` (default `60`).
|
||||
|
||||
### `[detection]` and `[vehicles]`
|
||||
|
||||
- `[detection] person_detection` (default `false`), `model_path`,
|
||||
`model_config_path`, `confidence_threshold` (default `0.5`),
|
||||
`cameras` (empty list means all cameras).
|
||||
- `[[vehicles.known]]` entries define recognised vehicles with
|
||||
`name`, `color_profile`, `size_class`, `calibration_file`.
|
||||
|
||||
### `[presence]`
|
||||
|
||||
- `enabled` (default `false`).
|
||||
- `ping_interval_s` (default `30`) and `departure_delay_m` (default
|
||||
`10`).
|
||||
- `method`: `icmp` or `arping`.
|
||||
- `[[presence.members]]` entries with `name`, `ip`, and `role`
|
||||
(`adult` or `child`).
|
||||
- `actions`: mapping of states (`EMPTY`, `ADULTS_HOME`, `KIDS_HOME`,
|
||||
`ALL_HOME`) to arm states.
|
||||
|
||||
### `[health]`
|
||||
|
||||
This is where pruning actually lives.
|
||||
|
||||
- `enabled` (default `true`).
|
||||
- `disk_warn_pct` (default `85`): warning threshold on the partition
|
||||
hosting `data_dir`.
|
||||
- `disk_critical_pct` (default `95`): critical threshold. When crossed
|
||||
and `auto_prune` is true, the health monitor runs the pruner.
|
||||
- `auto_prune` (default `true`).
|
||||
- `auto_prune_target_pct` (default `80`): pruner deletes the oldest
|
||||
non-starred recordings until disk usage drops below this percentage.
|
||||
- `daily_digest` (default `true`) and `daily_digest_time` (default
|
||||
`"08:00"`).
|
||||
|
||||
### `[pets]`, `[visitors]`, `[highlights]`, `[kiosk]`
|
||||
|
||||
Subsystem-specific toggles. See the subsystem references under
|
||||
`docs/architecture/` for per-key behaviour. Notable defaults: `[pets]
|
||||
enabled = false`, `[visitors] enabled = false`, `[highlights] enabled
|
||||
= true`, `[kiosk] ambient_enabled = true`.
|
||||
|
||||
### `[location]` and `[security]`
|
||||
|
||||
- `[location] latitude`, `longitude` (default `0.0`): used for sunrise
|
||||
and sunset lookups.
|
||||
- `[security] pin_hash` and `recovery_passphrase_hash`: populated by
|
||||
`vigilar config set-pin` (the same hash is also stored under
|
||||
`[system] arm_pin_hash` on the `system` model; both fields exist
|
||||
because the web UI uses `[security]` while the CLI helper prints a
|
||||
`[system]` line — pick one location and stick with it).
|
||||
|
||||
## CLI reference
|
||||
|
||||
The entry point is `/opt/vigilar/venv/bin/vigilar`. All commands
|
||||
accept `--version`. In production, run subcommands as the service user
|
||||
so file ownership and venv paths line up:
|
||||
|
||||
```
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar <subcommand>
|
||||
```
|
||||
|
||||
The CLI exposes exactly two top-level commands: `start` and `config`.
|
||||
|
||||
### `vigilar start`
|
||||
|
||||
Starts all services under the supervisor.
|
||||
|
||||
```
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar start \
|
||||
--config /etc/vigilar/vigilar.toml
|
||||
```
|
||||
|
||||
Options: `--config/-c PATH` (defaults to `$VIGILAR_CONFIG` then
|
||||
`config/vigilar.toml`); `--log-level {DEBUG,INFO,WARNING,ERROR}`
|
||||
(overrides `[system] log_level`). On invocation it loads and validates
|
||||
the config, configures a console log formatter, prints a startup
|
||||
summary (camera count, sensor count, UPS state), then hands off to
|
||||
`vigilar.main.run_supervisor`.
|
||||
|
||||
### `vigilar config validate`
|
||||
|
||||
```
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config validate \
|
||||
-c /etc/vigilar/vigilar.toml
|
||||
```
|
||||
|
||||
Parses and validates the TOML against the Pydantic models and prints
|
||||
a summary. Exits non-zero if validation fails. Run this after every
|
||||
edit before restarting the service.
|
||||
|
||||
### `vigilar config show`
|
||||
|
||||
```
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config show \
|
||||
-c /etc/vigilar/vigilar.toml
|
||||
```
|
||||
|
||||
Dumps the parsed config as JSON with `web.password_hash`,
|
||||
`system.arm_pin_hash`, and `alerts.webhook.secret` redacted. Useful
|
||||
for confirming which defaults Pydantic applied for keys you did not
|
||||
set.
|
||||
|
||||
### `vigilar config set-password`
|
||||
|
||||
```
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config set-password
|
||||
```
|
||||
|
||||
Prompts for a web UI password (hidden, confirmed), derives a scrypt
|
||||
hash (`n=16384, r=8, p=1`, random 16-byte salt, 32-byte output), and
|
||||
prints a `password_hash = "salt_hex:key_hex"` line to paste into
|
||||
`[web]`. It does not write the file.
|
||||
|
||||
### `vigilar config set-pin`
|
||||
|
||||
```
|
||||
sudo -u vigilar /opt/vigilar/venv/bin/vigilar config set-pin
|
||||
```
|
||||
|
||||
Prompts for an arm/disarm PIN, generates a random 32-byte HMAC key,
|
||||
computes `HMAC-SHA256(key, pin)`, and prints an `arm_pin_hash =
|
||||
"secret_hex:mac_hex"` line to paste into `[system]`. Again, no file
|
||||
write.
|
||||
|
||||
## Secrets and security
|
||||
|
||||
- `/etc/vigilar/secrets/` is `root:root` mode `0700`. The `vigilar`
|
||||
user cannot list it. Individual files the service needs (for
|
||||
example `vapid_public.txt`) are readable by group `vigilar`.
|
||||
- The storage encryption key is `/etc/vigilar/secrets/storage.key`:
|
||||
32 raw bytes. **If this file is lost, every existing `.vge`
|
||||
recording becomes unrecoverable.** Back it up separately (and
|
||||
offline) from your tar archive whenever you take the system into
|
||||
production.
|
||||
- Recordings use **AES-256-CTR** (see `vigilar/storage/encryption.py`).
|
||||
CTR provides confidentiality but no authentication: `.vge` files
|
||||
are confidential but not tamper-evident. An attacker with write
|
||||
access to the recordings directory can flip bits in a ciphertext
|
||||
without detection. If tamper-evidence matters, keep the recordings
|
||||
volume on integrity-verified storage (dm-integrity, ZFS with
|
||||
checksums) or mirror to write-once media.
|
||||
- The web UI password is a scrypt hash set by `vigilar config
|
||||
set-password` and stored at `[web] password_hash`. The arm PIN is
|
||||
an HMAC stored at `[system] arm_pin_hash` (and/or `[security]
|
||||
pin_hash`).
|
||||
- TLS: `gen_cert.sh` uses `mkcert` if present, otherwise an `openssl`
|
||||
ECDSA P-256 self-signed certificate valid for 3650 days with SANs
|
||||
for `vigilar.local`, `localhost`, `127.0.0.1`, and the detected LAN
|
||||
IP. It patches `[web] tls_cert`/`tls_key` into the config.
|
||||
- VAPID: `gen_vapid_keys.sh` writes
|
||||
`/etc/vigilar/secrets/vapid_private.pem` (mode 0600) and
|
||||
`/etc/vigilar/secrets/vapid_public.txt` (the browser-side key).
|
||||
- Firewall stance: the mosquitto broker and NUT daemon bind only to
|
||||
`127.0.0.1`. The only port Vigilar exposes on the LAN is the web UI
|
||||
port (default `49735`). Open that port only on the interface that
|
||||
serves your LAN, and keep WAN exposure behind the WireGuard tunnel
|
||||
described under `[remote]`.
|
||||
|
||||
## UPS and NUT integration
|
||||
|
||||
`scripts/setup_nut.sh` installs NUT, attempts to detect a USB UPS
|
||||
(using `nut-scanner` first, then a short list of vendor IDs as a
|
||||
fallback), and writes a standalone configuration:
|
||||
|
||||
- `/etc/nut/nut.conf` with `MODE=standalone`.
|
||||
- `/etc/nut/ups.conf` with `[ups] driver=usbhid-ups port=auto` (the
|
||||
block name `ups` matches the default `[ups] ups_name`).
|
||||
- `/etc/nut/upsd.conf` with `LISTEN 127.0.0.1 3493` — loopback only.
|
||||
- `/etc/nut/upsd.users` with a `vigilar` local monitoring user.
|
||||
- `/etc/nut/upsmon.conf` pointing at `ups@localhost`.
|
||||
|
||||
It then enables `nut-driver`, `nut-server`, and `nut-monitor` (or
|
||||
`upsd`/`upsmon` on distros that ship the old unit names). Test with
|
||||
`upsc ups@localhost`. The Vigilar UPS subsystem polls this daemon
|
||||
using the keys under `[ups]`.
|
||||
|
||||
## Backups
|
||||
|
||||
`scripts/backup.sh` produces
|
||||
`${VIGILAR_BACKUP_DIR:-/var/vigilar/backups}/vigilar-backup-YYYYMMDD-HHMMSS.tar.gz`
|
||||
and includes:
|
||||
|
||||
- A consistent SQLite snapshot produced with `sqlite3 … .backup` (or
|
||||
a direct file copy if `sqlite3` is not available), plus any
|
||||
`-wal`/`-shm` files.
|
||||
- The entire `/etc/vigilar/` tree (config, secrets, certs).
|
||||
|
||||
It does **not** include `/var/vigilar/recordings` or
|
||||
`/var/vigilar/hls`. Video is assumed to be either expendable or
|
||||
handled by a separate storage tier.
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `VIGILAR_BACKUP_DIR` — destination directory (default
|
||||
`/var/vigilar/backups`).
|
||||
- `VIGILAR_BACKUP_RETENTION_DAYS` — age in days after which old
|
||||
archives are pruned; set to `0` to keep forever (default `30`).
|
||||
|
||||
The archive is `chmod 0600 root:root` because it contains secrets.
|
||||
|
||||
### Scheduling
|
||||
|
||||
You can run it from cron, as the script comment suggests
|
||||
(`0 3 * * * /opt/vigilar/scripts/backup.sh`), or via a dedicated
|
||||
systemd timer. A minimal pair of units, kept in your local systemd
|
||||
directory (not in the repo):
|
||||
|
||||
```
|
||||
# /etc/systemd/system/vigilar-backup.service
|
||||
[Unit]
|
||||
Description=Vigilar nightly backup
|
||||
After=vigilar.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=VIGILAR_BACKUP_DIR=/srv/backups/vigilar
|
||||
ExecStart=/opt/vigilar/scripts/backup.sh
|
||||
```
|
||||
|
||||
```
|
||||
# /etc/systemd/system/vigilar-backup.timer
|
||||
[Unit]
|
||||
Description=Run Vigilar backup nightly
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
Enable with `sudo systemctl enable --now vigilar-backup.timer`.
|
||||
|
||||
### Restore
|
||||
|
||||
1. `sudo systemctl stop vigilar.service`.
|
||||
2. Extract the archive to a staging directory.
|
||||
3. Copy `etc/vigilar/` back into `/etc/vigilar/`, preserving
|
||||
permissions. Double-check `/etc/vigilar/secrets/storage.key` is
|
||||
`root:root 0600`.
|
||||
4. Copy the database snapshot to `/var/vigilar/data/vigilar.db` and
|
||||
remove any stale `vigilar.db-wal`/`vigilar.db-shm` files.
|
||||
5. `sudo chown -R vigilar:vigilar /var/vigilar/data`.
|
||||
6. `sudo -u vigilar /opt/vigilar/venv/bin/vigilar config validate
|
||||
-c /etc/vigilar/vigilar.toml`.
|
||||
7. `sudo systemctl start vigilar.service` and watch the journal.
|
||||
|
||||
## Upgrades
|
||||
|
||||
1. `sudo systemctl stop vigilar.service`.
|
||||
2. `cd /path/to/vigilar && git pull`.
|
||||
3. `sudo -u vigilar /opt/vigilar/venv/bin/pip install --upgrade .`
|
||||
4. Diff the shipped `config/vigilar.toml` against `/etc/vigilar/
|
||||
vigilar.toml` and merge any new keys by hand; Pydantic will reject
|
||||
unknown keys but is tolerant of missing keys that have defaults.
|
||||
5. `sudo -u vigilar /opt/vigilar/venv/bin/vigilar config validate
|
||||
-c /etc/vigilar/vigilar.toml`.
|
||||
6. `sudo systemctl start vigilar.service`.
|
||||
|
||||
**Schema migrations:** there is no migration framework. Vigilar does
|
||||
not ship Alembic; `vigilar/storage/schema.py` defines the tables
|
||||
(`cameras`, `sensors`, `sensor_states`, `events`, `recordings`,
|
||||
`system_events`, `arm_state_log`, `alert_log`, `push_subscriptions`,
|
||||
`pets`, `pet_sightings`, `wildlife_sightings`, `package_events`,
|
||||
`pet_training_images`, `pet_rules`, `face_profiles`, `face_embeddings`,
|
||||
`visits`, `timelapse_schedules`) and new columns are added by code
|
||||
path at startup or not at all. Take a backup before every upgrade so
|
||||
you can roll back if a column assumption changes.
|
||||
|
||||
## Logs and health
|
||||
|
||||
All subsystem output goes to the journal under the `vigilar` syslog
|
||||
identifier:
|
||||
|
||||
```
|
||||
sudo journalctl -u vigilar.service -f
|
||||
sudo journalctl -u vigilar.service --since "1 hour ago"
|
||||
sudo journalctl -u vigilar.service -p warning
|
||||
```
|
||||
|
||||
The alerts subsystem additionally mirrors messages to syslog via the
|
||||
`vigilar.alerts` logger when `[alerts.local] syslog = true`, which is
|
||||
the default; the supervisor installs the handler at startup.
|
||||
|
||||
Set `[system] log_level = "DEBUG"` (or pass `--log-level DEBUG` to
|
||||
`vigilar start`) to trace MQTT traffic, motion scoring, and FFmpeg
|
||||
invocations. Expect a significant volume increase; revert to `INFO`
|
||||
once you have the evidence you need.
|
||||
|
||||
The only HTTP endpoint currently exposing health is
|
||||
`GET /system/status` on the web UI, which returns a JSON blob with
|
||||
arm state, camera counts, and sensor counts. The richer health data
|
||||
(disk percentage, MQTT reachability) is published to the
|
||||
`vigilar/system/health` MQTT topic by `HealthMonitor` every ten
|
||||
seconds and is not yet surfaced as a REST endpoint.
|
||||
|
||||
## Pruning and disk management
|
||||
|
||||
`vigilar/health/monitor.py` runs a disk check every five minutes
|
||||
against `[system] data_dir` using `shutil.disk_usage`. When usage
|
||||
crosses `[health] disk_critical_pct` and `[health] auto_prune` is
|
||||
true, it calls `vigilar.health.pruner.auto_prune`:
|
||||
|
||||
- Selects up to 20 unstarred recordings at a time, ordered oldest
|
||||
first.
|
||||
- Deletes the file on disk, any thumbnail, and the row from the
|
||||
`recordings` table.
|
||||
- Loops until disk usage drops below `[health] auto_prune_target_pct`
|
||||
or no more candidates exist.
|
||||
|
||||
Starred recordings (`recordings.starred = 1`) are never auto-pruned.
|
||||
Per-camera `retention_days` is enforced separately by the camera
|
||||
subsystem. There is no hard byte ceiling; the pruner is entirely
|
||||
percentage-driven. The `[storage] max_disk_usage_gb` and
|
||||
`[storage] free_space_floor_gb` keys described above are not
|
||||
consulted by the pruner.
|
||||
|
||||
## Remote access
|
||||
|
||||
`[remote]` controls the lower-bitrate HLS profile that Vigilar serves
|
||||
through a WireGuard tunnel. The tunnel itself is not set up by this
|
||||
project — you are expected to bring your own WireGuard server and
|
||||
peer configuration. Once the tunnel is up:
|
||||
|
||||
- `enabled = true` turns on the remote bridge.
|
||||
- `tunnel_ip` is the home server's address inside the tunnel (default
|
||||
`10.99.0.2`), shown in the UI for reference.
|
||||
- `upload_bandwidth_mbps` caps the advertised upstream.
|
||||
- `remote_hls_resolution`, `remote_hls_fps`, `remote_hls_bitrate_kbps`
|
||||
define the transcode profile used when a client connects through
|
||||
the tunnel instead of the LAN.
|
||||
- `max_remote_viewers` bounds concurrent remote sessions; set to `0`
|
||||
for unlimited.
|
||||
|
||||
Do not expose port `49735` directly on the WAN; require the tunnel.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **Event timeline is not live.** The web UI event timeline requires
|
||||
a page refresh to show new events. `broadcast_sse_event` exists in
|
||||
`vigilar/web/blueprints/events.py` but has zero call sites today;
|
||||
events are not pushed to browsers via SSE. Web Push notifications
|
||||
via VAPID are independent of the timeline and do work: you will
|
||||
get mobile alerts as motion happens, but the in-page timeline lags
|
||||
until you reload.
|
||||
- **Recording integrity is not authenticated.** AES-256-CTR gives you
|
||||
confidentiality, not tamper-evidence. If an attacker reaches the
|
||||
recordings directory they can modify ciphertext unnoticed. See the
|
||||
security section.
|
||||
- **Camera supervision is asymmetric.** Most subsystems run under
|
||||
`SubsystemProcess` in `vigilar/main.py`, which polls every two
|
||||
seconds and applies an exponential backoff up to `max_restarts=10`.
|
||||
Cameras do not: `CameraManager` in `vigilar/camera/manager.py`
|
||||
owns its own per-camera child processes outside that supervisor.
|
||||
A repeatedly crashing camera may thrash differently from, say, a
|
||||
crashing UPS poller. Watch the journal for per-camera restart
|
||||
messages independently from the top-level supervisor log.
|
||||
- **Legacy storage keys.** `[storage] max_disk_usage_gb` and
|
||||
`[storage] free_space_floor_gb` are editable but do nothing. Use
|
||||
`[health]` for real disk policy.
|
||||
- **No schema migrations.** There is no Alembic (or equivalent) in
|
||||
the tree. Rollbacks rely on your backup discipline.
|
||||
- **Duplicate PIN fields.** `vigilar config set-pin` writes to
|
||||
`[system] arm_pin_hash`, while the web arm/disarm flow reads from
|
||||
`[security] pin_hash`. Both models exist. If you set one and the
|
||||
other side does not behave as expected, mirror the value manually.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Supervisor crash loops.** `journalctl -u vigilar.service` will show
|
||||
a subsystem crashing and the supervisor attempting to restart it. If
|
||||
the same subsystem exceeds ten restarts, the supervisor gives up on
|
||||
that subsystem and logs `exceeded max restarts, giving up`. Fix the
|
||||
root cause (bad config, missing secret, missing model file for
|
||||
detection) and restart the unit.
|
||||
|
||||
**Mosquitto will not start.** Confirm that
|
||||
`/etc/mosquitto/conf.d/vigilar.conf` is present and that no other
|
||||
listener is bound to `127.0.0.1:1883`. Run `sudo systemctl status
|
||||
mosquitto.service` and `sudo journalctl -u mosquitto.service`. The
|
||||
Vigilar unit `Requires=mosquitto.service`, so Vigilar will refuse to
|
||||
start until mosquitto is healthy.
|
||||
|
||||
**Camera thrashing.** Because cameras are not under the main
|
||||
supervisor's backoff, a camera whose RTSP URL is wrong or whose
|
||||
remote end is rebooting can respawn quickly. Look for repeated
|
||||
`camera <id>` messages in the journal. Disable the camera in the
|
||||
config (`enabled = false`) while you fix the upstream, then
|
||||
re-enable.
|
||||
|
||||
**Disk full.** Check `[health] disk_critical_pct` and confirm
|
||||
`auto_prune` is on. If the partition is already past the target
|
||||
percentage and nothing is being deleted, there are no unstarred
|
||||
recordings left to prune — unstar something or lower retention. The
|
||||
legacy `[storage]` keys will not help here; see the pruning section.
|
||||
|
||||
**HLS stalls.** The HLS directory lives at `[system] hls_dir`
|
||||
(default `/var/vigilar/hls`) and is mounted `ReadWritePath` in the
|
||||
systemd unit. Stalls usually mean FFmpeg has died on a camera;
|
||||
check the journal for FFmpeg stderr and verify the RTSP URL is still
|
||||
reachable from the server with `ffprobe`.
|
||||
|
||||
**Config validation fails.** Run `sudo -u vigilar
|
||||
/opt/vigilar/venv/bin/vigilar config validate -c
|
||||
/etc/vigilar/vigilar.toml`. Pydantic error messages include the
|
||||
section, key, and reason. The two common traps are duplicate camera
|
||||
or sensor IDs (root validator rejects them) and a TOML table that
|
||||
should be an array of tables (`[cameras]` instead of `[[cameras]]`).
|
||||
|
||||
**Forgotten arm PIN.** Run `vigilar config set-pin` to mint a new
|
||||
hash and paste it in; restart the service. If you also forgot the
|
||||
recovery passphrase set up through the UI, the web
|
||||
`/system/api/reset-pin` endpoint cannot help you — fall back to the
|
||||
CLI.
|
||||
|
||||
**Forgotten web password.** Run `vigilar config set-password` and
|
||||
paste the new hash into `[web] password_hash`, then restart. No
|
||||
database state needs to change.
|
||||
1720
docs/superpowers/plans/2026-04-05-project-documentation.md
Normal file
1720
docs/superpowers/plans/2026-04-05-project-documentation.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
# Vigilar Project Documentation — Design Spec
|
||||
|
||||
**Date:** 2026-04-05
|
||||
**Status:** Approved, ready for implementation plan
|
||||
**Scope:** Create user-facing and architectural documentation for Vigilar, plus a polished top-level `README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Vigilar currently has no top-level `README.md`, no user guide, and no architectural reference. Contributors and would-be home users have to read source code to understand what the project is or how to run it. This effort closes that gap with a single coordinated documentation pass.
|
||||
|
||||
The docs must:
|
||||
|
||||
- Give a homeowner with a mini PC a clear linear path from bare hardware to working cameras on their phone.
|
||||
- Give a self-hoster a reference for config, CLI, secrets, backups, upgrades, and troubleshooting.
|
||||
- Give a contributor enough architectural context to navigate the codebase without reading every file.
|
||||
- Match the project's ethos: plain, no bloat, no cloud, no extra tooling.
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
- No doc site / MkDocs build (files are organized *as if* MkDocs-ready, but no tooling is added).
|
||||
- No rewrites of existing code or config.
|
||||
- No changes to `docs/camera-hardware-guide.md` (already exists, untouched).
|
||||
- No changes to anything under `docs/superpowers/` other than this spec.
|
||||
- No CI doc-linting, no link-checker automation beyond a one-time manual verification pass.
|
||||
- No recording-backup-to-NAS feature work. Docs describe only what exists today; future backup improvements are noted as planned, not documented as present.
|
||||
|
||||
## 3. Audience & doc layout (Approach 3 — Hybrid)
|
||||
|
||||
Three organizing principles, one tree:
|
||||
|
||||
- **User-facing guides** are monolithic linear narratives (humans read top to bottom once).
|
||||
- **Architecture docs** are split reference material (contributors jump to the subsystem they're touching).
|
||||
- **README** is the front door that ties everything together.
|
||||
|
||||
Final tree:
|
||||
|
||||
```
|
||||
README.md # NEW — top-level front door
|
||||
docs/
|
||||
├── home-user-guide.md # NEW — monolithic, linear
|
||||
├── operator-guide.md # NEW — monolithic, reference
|
||||
├── architecture/
|
||||
│ ├── overview.md # NEW — process model, bus, data flow
|
||||
│ ├── conventions.md # NEW — coding conventions distilled
|
||||
│ └── subsystems/
|
||||
│ ├── camera.md # NEW
|
||||
│ ├── detection.md # NEW
|
||||
│ ├── events.md # NEW
|
||||
│ ├── alerts.md # NEW
|
||||
│ ├── sensors.md # NEW
|
||||
│ ├── ups.md # NEW
|
||||
│ ├── storage.md # NEW
|
||||
│ ├── highlights.md # NEW
|
||||
│ ├── presence.md # NEW
|
||||
│ ├── pets.md # NEW
|
||||
│ ├── health.md # NEW
|
||||
│ └── web.md # NEW
|
||||
└── camera-hardware-guide.md # EXISTING — untouched
|
||||
```
|
||||
|
||||
## 4. `README.md` (top-level)
|
||||
|
||||
Sells the project, orients newcomers, links into the doc tree.
|
||||
|
||||
**Required sections, in order:**
|
||||
|
||||
1. **Title + tagline** — "Vigilar — DIY offline-first home security"
|
||||
2. **One-line pitch** (~20 words)
|
||||
3. **Hero image placeholder** — `` with a comment noting it's a placeholder. Do not create the image.
|
||||
4. **Why Vigilar** — bullet list of differentiators: offline-first, HLS grid + MJPEG single, OpenCV MOG2 motion with 5-sec pre-motion ring buffer, event timeline + highlight reels + timelapses, visitor/pet/wildlife tracking, PWA with VAPID push (no Firebase), AES-256 encrypted recordings, NUT UPS monitoring, runs on cheap mini PC.
|
||||
5. **Quick paths table** — 4 rows mapping intent → doc: home user guide, operator guide, architecture overview, camera hardware guide.
|
||||
6. **60-second overview** — small ASCII diagram (cameras → mini PC → phone/browser) + 4 tech-stack bullets (Python 3.11, Flask+Bootstrap 5, SQLite WAL, MQTT, FFmpeg).
|
||||
7. **Status** — "Alpha. Works on the author's hardware. Expect rough edges and breaking changes."
|
||||
8. **Installation TL;DR** — 3-line fenced block (`git clone`, `sudo ./scripts/install.sh`, `sudo systemctl start vigilar`), followed by links to the full home-user and operator guides.
|
||||
9. **Documentation** — nested bullet list mirroring the `docs/` tree, each entry with a one-line description.
|
||||
10. **License** — GPL-3.0. If no `LICENSE` file exists, the README states the intent and notes a `LICENSE` file will follow; we do **not** create the license file as part of this effort unless asked.
|
||||
11. **Contributing** — short stub: "Issues and PRs welcome. See `CLAUDE.md` for code conventions."
|
||||
|
||||
## 5. `docs/home-user-guide.md`
|
||||
|
||||
Monolithic, linear, ~1500–2500 words. Target reader: homeowner with a mini PC, some Linux comfort, wants cameras on phone.
|
||||
|
||||
**Required sections, in order:**
|
||||
|
||||
1. **What you'll end up with** — outcome in 2 sentences plus a small ASCII diagram.
|
||||
2. **What you need** — hardware checklist: mini PC (x86_64, 4GB+ RAM, 128GB+ SSD), USB stick for OS install, one or more RTSP cameras, phone, optional NAS. Link to `camera-hardware-guide.md` for camera picks.
|
||||
3. **Step 1 — Install Debian/Ubuntu Server on the mini PC** — brief, points at upstream installer docs, tells the user to enable SSH. No hand-holding on the OS install itself.
|
||||
4. **Step 2 — Get Vigilar onto the box** — `git clone`, `sudo ./scripts/install.sh`, plus 3 bullets summarizing what `install.sh` does (read `scripts/install.sh` at write-time to ground these bullets).
|
||||
5. **Step 3 — First boot** — `sudo systemctl enable --now vigilar`, then open `http://<mini-pc-ip>:49735` in a browser on the same LAN. Mention the port is configurable under `[web]` in `vigilar.toml`.
|
||||
6. **Step 4 — Set your PIN** — UI walkthrough, 2–3 sentences, screenshot placeholder.
|
||||
7. **Step 5 — Add your first camera** — UI walkthrough: RTSP URL, credentials, test stream, save. Point at `camera-hardware-guide.md` for URL formats.
|
||||
8. **Step 6 — Phone push notifications (PWA)** — open web UI on phone, "Add to Home Screen", allow notifications. Under-the-hood note: VAPID keys already generated by `install.sh`.
|
||||
9. **Step 7 — Optional: NAS backup of config + database** — mount NAS share at `/mnt/nas/vigilar-backups`, set `VIGILAR_BACKUP_DIR`, and set up a nightly run of `scripts/backup.sh`. The guide provides a copy-pasteable systemd service + timer snippet inline (no new units are shipped in the repo as part of this effort). **Explicitly state** that this backs up DB + `/etc/vigilar` (config + secrets) only, and that **recordings stay local** — point at a "planned" note for recording backup.
|
||||
10. **Troubleshooting** — camera won't connect, no push notifications, service won't start (`journalctl -u vigilar`), motion detection too sensitive, how to reset PIN.
|
||||
11. **Where to go next** — links to Operator Guide and Architecture Overview.
|
||||
|
||||
**Grounding rule:** every shell command must correspond to a real file in `scripts/` or a real `vigilar` CLI subcommand. Verify before writing.
|
||||
|
||||
## 6. `docs/operator-guide.md`
|
||||
|
||||
Monolithic, reference-oriented, ~2500–4000 words. Target reader: self-hoster tuning, upgrading, securing.
|
||||
|
||||
**Required sections, in order:**
|
||||
|
||||
1. **Audience & scope** — for admins, not first-time home users. Points at home-user-guide.md for initial setup.
|
||||
2. **Layout on disk** — table of `/opt/vigilar`, `/etc/vigilar/{vigilar.toml, certs/, secrets/}`, `/var/vigilar/{data/vigilar.db, recordings/, hls/, backups/}`.
|
||||
3. **Installation** — what `scripts/install.sh` does, `systemd/vigilar.service` summary, `systemd/vigilar-mosquitto.conf` summary, system dependencies (ffmpeg, mosquitto, sqlite3, Python 3.11+).
|
||||
4. **Configuration reference (`vigilar.toml`)** — one subsection per `[section]` in the default TOML. Each key: default, what it controls, when to change. Sections to cover (from current `config/vigilar.toml`): `system`, `mqtt`, `web`, `zigbee2mqtt`, `ups`, `storage`, `remote`, `alerts.local`, `alerts.web_push`, `alerts.email`, plus any additional sections discovered by re-reading the TOML at write-time.
|
||||
5. **CLI reference (`vigilar ...`)** — enumerated at write-time by reading `vigilar/cli/`. One subsection per top-level command. Do not guess commands.
|
||||
6. **Secrets & security** — `/etc/vigilar/secrets/` layout and permissions; `vigilar config set-pin`; `vigilar config set-password`; TLS via `scripts/gen_cert.sh` → `[web] tls_cert/tls_key`; VAPID via `scripts/gen_vapid_keys.sh`; storage encryption key (`storage.key`) — **explicit warning: do not lose it, recordings are unrecoverable without it**; recommended firewall stance (LAN-only by default).
|
||||
7. **UPS / NUT integration** — `scripts/setup_nut.sh`, `[ups]` options, shutdown behavior, low-battery thresholds.
|
||||
8. **Backups** — what `scripts/backup.sh` captures (DB + `/etc/vigilar`) and what it does **not** (recordings); `VIGILAR_BACKUP_DIR` and `VIGILAR_BACKUP_RETENTION_DAYS`; copy-pasteable systemd service + timer snippet (inline in the doc; no new unit files added to the repo); restore procedure.
|
||||
9. **Upgrades** — `git pull` + `pip install -e .` + `systemctl restart vigilar`; rollback by restoring a backup tarball. If DB migrations exist, note how they're applied; if they don't, say so.
|
||||
10. **Logs & health** — `journalctl -u vigilar`, `log_level` in `[system]`, health endpoints (enumerated at write-time by reading `vigilar/web/blueprints/system.py` and `vigilar/health/`).
|
||||
11. **Remote access** — `[remote]` section, tunnel-based remote HLS, bandwidth-shaped downscaled streams, reiterated not-a-cloud.
|
||||
12. **Troubleshooting** — service crash loops, MQTT broker won't start, camera worker thrashing, disk full / `free_space_floor_gb` triggered, HLS stalling.
|
||||
|
||||
**Grounding rule:** every TOML key, every CLI command, every file path, every endpoint must be verified against the current code before writing. Any that can't be verified must be omitted, not guessed.
|
||||
|
||||
## 7. `docs/architecture/overview.md`
|
||||
|
||||
~1000–1500 words. Target reader: contributor new to the codebase.
|
||||
|
||||
**Required sections, in order:**
|
||||
|
||||
1. **Design principles** — offline-first, subsystem isolation via multiprocessing, loose coupling via local MQTT bus, SQLite WAL as single durable store, SQLAlchemy Core (not ORM), adaptive FPS (2 idle / 30 on motion) with ring buffer.
|
||||
2. **Process topology** — ASCII or Mermaid diagram showing parent supervisor + N subsystem processes + mosquitto + Flask web.
|
||||
3. **The MQTT bus** — broker location, topic naming convention `vigilar/<subsystem>/<entity>/<event>`, retention/QoS notes (verify at write-time), rationale for MQTT over an in-process queue.
|
||||
4. **Data flow: the motion → alert path** — numbered sequence from RTSP capture through motion detection, recording, event creation, highlight scoring, push notification, and UI update. Each step names the actual file/function where it happens (verify at write-time).
|
||||
5. **Storage layout** — SQLite table summary (enumerate at write-time by reading `vigilar/storage/`), recordings (`.vge`, AES-256-GCM, key path), HLS segments, backups.
|
||||
6. **Configuration & secrets** — TOML → Pydantic v2 validation, secrets as file paths (never inline), PIN & password hashing with constant-time compare.
|
||||
7. **The web tier** — Flask + Blueprints, Jinja2 + Bootstrap 5 dark, HLS grid + MJPEG single view rationale, PWA + VAPID.
|
||||
8. **What's NOT in the critical path** — remote access (optional), email alerts (optional), cloud (never).
|
||||
|
||||
## 8. `docs/architecture/conventions.md`
|
||||
|
||||
~400 words. Distilled from `CLAUDE.md` but written for human contributors, not the AI. Covers: StrEnum for string constants (`vigilar/constants.py`), SQLAlchemy Core only (no mapped ORM classes), type hints on public functions, no docstrings unless logic is non-obvious, Ruff line-length 100, multiprocessing-per-subsystem rule, MQTT topic naming, Pydantic-validated TOML config, secrets-as-file-paths.
|
||||
|
||||
## 9. `docs/architecture/subsystems/*.md` (12 files)
|
||||
|
||||
One file per subdirectory under `vigilar/`: `camera`, `detection`, `events`, `alerts`, `sensors`, `ups`, `storage`, `highlights`, `presence`, `pets`, `health`, `web`.
|
||||
|
||||
**Uniform template** (≈150–400 words each):
|
||||
|
||||
```
|
||||
# <Subsystem name>
|
||||
|
||||
## Purpose
|
||||
One paragraph — what this subsystem is responsible for.
|
||||
|
||||
## Key files
|
||||
- `vigilar/<sub>/foo.py` — role
|
||||
- ...
|
||||
|
||||
## MQTT topics
|
||||
**Subscribes:** `vigilar/...`
|
||||
**Publishes:** `vigilar/...`
|
||||
|
||||
## Database tables
|
||||
`table_name` — what it holds. Or "none."
|
||||
|
||||
## Depends on
|
||||
- sister subsystem X (via topic Y)
|
||||
|
||||
## Consumed by
|
||||
- sister subsystem Z (via topic W)
|
||||
|
||||
## Notes
|
||||
Gotchas or perf notes, only if any.
|
||||
```
|
||||
|
||||
**Grounding rule (hard):** every topic name, every table name, every file role must come from reading the actual code. If a topic cannot be found, the doc must say "no MQTT publishers found at time of writing" — not invent one. This rule is the most important verification step in the plan.
|
||||
|
||||
## 10. Verification checklist (before completion)
|
||||
|
||||
1. **Link check** — every relative link in every new file resolves to a real path.
|
||||
2. **Command check** — every shell command in the user guides exists as a real script under `scripts/` or a real `vigilar` CLI subcommand.
|
||||
3. **Grounding check** — every topic name, table name, file path, and endpoint is verified against code, or omitted. Nothing guessed.
|
||||
4. **TOML coverage check** — every `[section]` in `config/vigilar.toml` is covered in the operator guide's configuration reference.
|
||||
5. **Subsystem coverage check** — every subdirectory in `vigilar/` (matching the 12-file list) has a corresponding subsystem doc.
|
||||
6. **Read-through pass** — tone and terminology consistent across all files.
|
||||
7. **README link check** — all doc tree links in `README.md` resolve.
|
||||
|
||||
## 11. Out of scope (explicit)
|
||||
|
||||
- `LICENSE` file creation (the README declares GPL-3.0; creating the file is a separate request).
|
||||
- Screenshot/image creation (placeholders only).
|
||||
- MkDocs configuration.
|
||||
- Any code changes.
|
||||
- Any changes to `docs/camera-hardware-guide.md`.
|
||||
- Any doc-linting CI.
|
||||
- Recording-backup-to-NAS feature or docs beyond the "planned" note.
|
||||
- Migration documentation beyond noting whether migrations exist.
|
||||
|
||||
## 12. Success criteria
|
||||
|
||||
- A new homeowner can go from a bare mini PC to working cameras on their phone using only `README.md` + `docs/home-user-guide.md`.
|
||||
- A self-hoster can answer any "how do I configure / back up / upgrade / troubleshoot" question from `docs/operator-guide.md` alone.
|
||||
- A new contributor can identify which subsystem owns a given behavior within 5 minutes using `docs/architecture/overview.md` + the subsystem files.
|
||||
- Every claim in every doc is either verified against current code or explicitly flagged.
|
||||
Reference in New Issue
Block a user