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>
Describes the canonical [security] pin_hash key, the PBKDF2 format
emitted by 'vigilar config set-pin', and the deprecation warning for
the legacy [system] arm_pin_hash. Drops the three-way mismatch
known-limitation.
Code review on 9f203d8 caught a silent-failure mode: MessageBus.connect
logs and returns without raising when the MQTT handshake times out, so
an overloaded broker would let bus.publish() enqueue into paho's outbox
only to be discarded by the immediate disconnect(). The web endpoint
would return 202 even though the FSM never received the request.
Guard with 'if not bus.connected: raise RuntimeError'. The existing
try/except in arm_system/disarm_system catches the exception and turns
it into a 503 with the same log message as other bus failures.
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.
Code review follow-up on c9dd348 — the log = logging.getLogger(__name__)
assignment was interleaved between 'import X' and 'from X import Y'
statements. Move it below all imports per standard ordering.
No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Adjacent secret leak in show_cmd noticed during Task 3 code review.
SecurityConfig has two sensitive fields and the redaction block only
covered pin_hash. vigilar config show would print the recovery
passphrase hash verbatim whenever one was configured.
One-line fix; same redaction pattern as the surrounding secrets.
Part of issue #2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Code review feedback on the Task 2 commit (7fda351):
- The 'verify_pin as _verify_pin_hash' alias was unnecessary — the
method self.verify_pin and the module-level verify_pin do not
collide (one is accessed via self, the other via the bare name).
Removing the alias matches how web/blueprints/system.py already
imports verify_pin and makes the call site read cleanly.
- The comment on the insert_arm_state None argument now explains
WHY (PBKDF2 salt is fresh per call, so re-hashing is worthless for
audit correlation) instead of only referencing the issue.
No behavior change. Part of issue #2.
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>
Topic for web-originated arm/disarm requests that the event processor
will subscribe to and dispatch to ArmStateFSM.transition. Part of the
PIN unification work (issue #2).
Plan document for issue #2 — the three-way PIN hash mismatch across
CLI, events FSM, and web arm/disarm. Proposes canonicalizing on
PBKDF2-SHA256 via alerts/pin and [security] pin_hash, deprecating
[system] arm_pin_hash, and wiring web arm/disarm through MQTT to the
FSM so the web buttons actually transition state.
Nine tasks, TDD throughout. No code changes in this commit.
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>
Convert the "Where to go next" items in the architecture overview from
plain text to proper Markdown links. This was the only finding from the
Task 18 verification pass; everything else (links, commands, TOML
coverage, subsystem coverage, terminology) is self-consistent across
the 17 new doc files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Front-door for the repo. Pitches the project, surfaces known limitations
honestly (timeline not live, CTR not authenticated, no migrations), and
links into the three doc trees: home user guide, operator guide, and
architecture overview + 12 subsystem references.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Linear walkthrough from bare mini PC to working cameras on phone, with
optional NAS backup. Verified against real install.sh, backup.sh, and
CLI subcommands; honest about the in-browser event timeline not being
wired to SSE yet (push notifications do work).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
18 tasks covering README, home user guide, operator guide, architecture
overview + conventions, and 12 per-subsystem reference docs. Each task
is grounded in reading real source to avoid invented facts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Specify that backup timer snippets are inline in the guides, not
shipped as new unit files, to match the no-code-changes scope.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Captures scope and structure for top-level README, home user guide,
operator guide, and architecture docs (overview + conventions + 12
per-subsystem files). Approach 3 (hybrid): monolithic user guides,
split architecture reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Group A (Foundation): Web Push notifications, AES-256-CTR recording
encryption/playback, HLS.js v1.5.17, PIN verification on arm/disarm
Group C (Detection Intelligence): Activity heatmaps, wildlife journal
with Open-Meteo weather correlation, package delivery detection with
NOAA sunset-aware reminders
Group E (Pet Lifestyle): Composable per-pet rules engine with 6
condition types, cooldown management, CRUD API, 6 quick-add templates
Group D (Visitor Recognition): Local face recognition via dlib,
face profile/embedding storage, consent-gated labeling, visitor
dashboard with privacy controls (forget/ignore)
Group B (Daily Delight): Daily highlight reel generator with event
scoring, kiosk ambient mode with alert takeover, time-lapse generator
with scheduled presets
37 commits, 63 files changed, 360 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `requests` to pyproject.toml dependencies (used by detection/weather.py)
- Wire PACKAGE_DELIVERED/REMINDER/COLLECTED and KNOWN/UNKNOWN_VISITOR event
types in EventProcessor._classify_event
- Add normalized bbox to all detection payloads in camera worker
(domestic_animal, wildlife, person, vehicle)
- Integrate highlight reel and timelapse scheduling in HealthMonitor.run()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use hmac.compare_digest() in verify_pin() to prevent timing-based PIN oracle attacks
- Redact entire [security] section (pin_hash, recovery_passphrase_hash) from /api/config response
- Sunset sign fix was skipped: existing longitude - ha formula is correct per NOAA equations and verified by test_sunset_equator; longitude + ha produces sunrise, not sunset
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 three new SQLAlchemy Core tables to schema.py for visitor recognition:
face_profiles (identity store), face_embeddings (per-profile encodings),
and visits (arrival/departure log). Indexes on profile_id and arrived_at.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add KNOWN_VISITOR, UNKNOWN_VISITOR, VISITOR_DEPARTED to EventType enum.
Add VisitorsConfig model to config.py and wire into VigilarConfig.
Add face_recognition>=1.3.0 under [face] optional-dependencies in pyproject.toml.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>