Adds _handle_arm_request and a dedicated bus.subscribe on
Topics.SYSTEM_ARM_REQUEST. Payload {mode, pin, triggered_by} is
dispatched to ArmStateFSM.transition, which verifies the PIN via
alerts.pin.verify_pin and performs the state change.
This is the missing link for web /system/api/arm to actually move
the system into an armed state. Part of issue #2.
If a config still has the legacy [system] arm_pin_hash set but no
[security] pin_hash, load_config logs a WARNING telling the operator
to re-run 'vigilar config set-pin'. The legacy field is still parsed
(so old configs don't fail validation) but ignored at runtime.
Part of issue #2 PIN hashing unification.
Was: HMAC-SHA256(random, pin) written to [system] arm_pin_hash —
no verifier in the codebase accepted this output.
Now: PBKDF2-SHA256 via alerts.pin.hash_pin written to [security]
pin_hash, matching what the web and FSM paths verify against.
Also fixes show_cmd to redact the new location.
Was: unsalted SHA-256 read from [system] arm_pin_hash.
Now: PBKDF2-SHA256 600k iterations read from [security] pin_hash,
matching the web arm/disarm path and the alerts/pin module.
Also drops the redundant pin re-hash on the arm_state_log audit row
(a fresh PBKDF2 salt made the column valueless for traceability).
Part of issue #2 PIN hashing unification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#1.
The Flask event-timeline was dead: `broadcast_sse_event` existed in
`vigilar/web/blueprints/events.py` but had zero call sites. Clients
subscribed to `/events/stream`, received the initial "connected"
message, and then only keepalives — a page refresh was required to
see new events. (Web Push via VAPID was independent and already worked.)
The root cause was a process-boundary gap: the events subsystem runs
in its own OS process and emits to MQTT, while the Flask app runs in a
separate process with no MQTT client of its own.
This change adds a thin bridge:
- EventProcessor._handle_event now publishes a classified summary
(id, ts, type, severity, source_id, payload) to a new topic
`Topics.EVENTS_PUBLISHED = "vigilar/events/published"` right after
`insert_event()`. Classification logic stays in one place.
- A new module `vigilar/web/sse_bridge.py` provides `forward_event`
(MQTT handler) and `start_sse_bridge(cfg)` (creates a MessageBus,
subscribes forward_event to EVENTS_PUBLISHED, connects, returns the
bus).
- `vigilar/main.py:_run_web` starts the bridge after `create_app(cfg)`
and disconnects it on shutdown. Bridge failure is logged but does
not kill the web process — the UI still works without live updates.
- `create_app` is deliberately NOT changed. Keeping the bridge out of
the app factory means no existing test triggers a real MQTT
connection, and the bridge stays a production-only concern wired by
the supervisor.
Tests (all added with TDD, RED verified before GREEN):
- tests/unit/test_events.py::TestEventsPublishedBroadcast — asserts
`_handle_event` publishes the classified payload for a motion event
and does NOT publish for unclassified topics (heartbeats).
- tests/unit/test_sse_bridge.py — asserts `forward_event` reaches SSE
subscribers, and `start_sse_bridge` wires the handler to
`Topics.EVENTS_PUBLISHED` on a connected bus (fake bus, no real
MQTT in tests).
Also refreshes the docs that previously flagged the dead SSE as a
known limitation (operator guide, web architecture doc).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some web endpoint handlers call _save_and_reload(), which resolves the
target path via VIGILAR_CONFIG env var with a fallback to the relative
"config/vigilar.toml". Any test exercising such an endpoint without
setting the env var rewrites the repo's committed config file via a
Pydantic model_dump round-trip, stripping comments and non-default
fields. The culprit discovered was test_reset_pin_correct_passphrase
in tests/unit/test_system_pin.py.
Add an autouse session-scoped fixture in tests/conftest.py that points
VIGILAR_CONFIG at a path inside pytest's session tmp dir so no test
can touch the real file. Restore the previous env var value on teardown.
Fixes#3.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add GET /kiosk/ambient route and standalone fullscreen template with
rotating camera snapshots (crossfade), top bar clock/date, pet status
bottom bar, SSE-driven alert takeover with HLS playback, configurable
screen dimming, and 6-hour auto-refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add /visitors/ blueprint with REST API endpoints for listing profiles and
visits, labeling (with consent gate), household linking, ignore/unignore,
and cascading forget. Register blueprint in app.py. Add dashboard and profile
detail templates (Bootstrap 5 dark, tab navigation, fetch-based JS). All six
API tests pass (339 total).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add FaceRecognizer class that loads face encodings from the database,
supports runtime add_encoding(), and matches new encodings by L2 distance.
face_recognition import is deferred so the class works without dlib installed.
FaceResult dataclass carries profile_id, name, confidence, crop, and bbox.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add create/get/update/delete_cascade for face_profiles, insert/get for
face_embeddings, and insert/get/get_active for visits. All seven query
functions covered by unit tests (327 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add insert/get/update queries for package_events table, and
notification content for PACKAGE_DELIVERED and PACKAGE_REMINDER events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add wildlife_bp with sightings, stats, frequency, and CSV export
endpoints; Bootstrap 5 dark journal template with live-updating
summary cards, species bars, time-of-day frequency chart, and
paginated/filterable sightings table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add get_wildlife_sightings_paginated, get_wildlife_stats, and
get_wildlife_frequency to queries.py with full test coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Import send_alert into processor.py, store engine as self._engine after
init_db(), extend _execute_action() to accept event_type/severity/source_id
and call send_alert for alert_all and push_and_record actions, and pass
those params from _handle_event().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add vigilar/alerts/sender.py with _CONTENT_MAP for human-readable push
notification titles/bodies, build_notification(), and send_alert() which
retrieves VAPID key, iterates push subscriptions, calls pywebpush, and
logs results to alert_log with auto-cleanup of expired (410) endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After FFmpeg finishes writing, _stop_ffmpeg() now reads VIGILAR_ENCRYPTION_KEY
and encrypts the MP4 to .vge format via encrypt_file(), updating the returned
RecordingSegment to reflect the encrypted file path and size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PIN checking to arm/disarm endpoints using verify_pin() against
cfg.security.pin_hash, and a new POST /system/api/reset-pin endpoint
that verifies the recovery passphrase before updating the PIN hash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the flat pet/detected handler with context-aware classification:
unknown animals (no pet_id) → UNKNOWN_ANIMAL/WARNING, known pets in
exterior/transition zones → PET_ESCAPE/ALERT, known pets indoors →
PET_DETECTED/INFO. Adds four new unit tests covering all three paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements all /pets/* routes (register, status, sightings, wildlife,
unlabeled crops, label, upload, update, delete, train, training-status,
highlights), registers the blueprint in app.py, adds a placeholder
dashboard template, and covers the API with 11 passing unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 4 — Alert Profiles: presence-aware + time-of-day alert routing.
Profiles match household state (EMPTY/KIDS_HOME/ADULTS_HOME/ALL_HOME)
and time windows (sleep hours). Per-detection-type rules control
push/record/quiet behavior with role-based recipients (all vs adults).
Task 5 — Recording Timeline: canvas-based 24h timeline per camera
with color-coded segments (person=red, vehicle=blue, motion=gray).
Click-to-play, date picker, detection type filters, hour markers.
Timeline API endpoint returns segments for a camera+date.
All 5 daily-use feature tasks complete. 140 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>