Follow-up to the holistic review of the PIN-unification branch:
- /system/status now reads the real arm state from the arm_state_log
table via get_current_arm_state, instead of returning a hardcoded
'DISARMED' stub. Without this, polling after the new async 202
arm/disarm flow was a UX dead-end — clients never saw the state
change they just requested. DB read failures degrade gracefully.
- Operator guide: correct the claim that 'vigilar config set-pin'
populates recovery_passphrase_hash. It doesn't. recovery_passphrase
_hash has no CLI helper today; it must be set manually.
- Tests: add a fail-closed regression for verify_pin on malformed
stored hashes, and a companion test confirming the deprecation
warning stays silent on a fully migrated config.
All address specific review comments on the branch; no scope creep.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up to efd5c4a. The plan invented {"accepted": True, ...} for
the new 202 responses, but every other 2xx endpoint in the Flask app
returns {"ok": True, ...} — including cameras.py:108 which is direct
prior art for a 202 with the same convention. The shared JS helper
at static/js/settings.js:54 does 'if (resp.ok && result.ok)' and was
falling into the error branch on our success responses, showing a
bogus "Save failed" toast after every arm/disarm click.
Keep the 202 status. Swap the body key from 'accepted' to 'ok'.
No JS change needed.
Was: /system/api/arm verified the PIN against [security] pin_hash and
returned {ok: true} without ever calling the FSM. State never changed.
Now: the endpoint publishes a SYSTEM_ARM_REQUEST message to the local
MQTT broker. The event processor (see previous commit) picks it up,
ArmStateFSM verifies the PIN via alerts.pin.verify_pin and performs
the transition. Response is 202 Accepted; clients poll /system/status
for the new state.
Design: PIN travels over localhost-only MQTT, which matches the
existing trust boundary for the internal bus.
Code review follow-up on f4d66dd:
- _handle_arm_request signature used "ArmStateFSM" as a string forward
reference even though the type is imported at module top.
_handle_event uses the bare form; match it for consistency.
- Add a test asserting that omitting triggered_by in an arm-request
payload defaults to "unknown". That value feeds the audit log, so
it deserves explicit regression coverage.
No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>