The BLE handler was set but never invoked when BLE messages arrived
from nodes. Replace the TODO with actual bleHandler call so BLE scan
data flows to the registry, enabling the existing 5s ble_scan
WebSocket broadcast to dashboard clients.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BroadcastEventFromDB was using inconsistent field names (timestamp_ms,
type, person) that didn't match the canonical event format (ts, kind,
person_name) expected by handleEventMessage in app.js. This caused
DB-sourced events to render with "undefined" kind/person and "Invalid
Date" timestamps.
Also adds table-driven tests for BroadcastEventFromDB covering zone
entry/exit, portal crossing, anomaly, and minimal events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add TestHub_BroadcastEvent covering all event kinds (zone_entry,
zone_exit, portal_crossing, presence_transition). Also wire
presence_transition broadcasts in the ingestion server when
motion state changes on a link.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Connect bleRegistry to the dashboard hub so ble_scan messages are
broadcast every 5s to all connected dashboard clients. Add rssi,
last_seen, and blob_id fields to GetCurrentDevices output to match
the frontend's expected message format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a 5-second ticker in the dashboard hub that broadcasts BLE device
list updates as typed 'ble_scan' messages when devices are present.
The BroadcastBLEScan method and frontend handler already existed but
were never wired up to a periodic timer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify the alert message format {type:'alert', alert:{id,ts,severity,description,acknowledged}}
for anomaly detections and security mode triggers. The BroadcastAlert function and
app.js handler already existed; this adds coverage for critical anomaly, security
mode armed/disarmed, and acknowledged alert scenarios.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up the SQLite Online Backup API streaming endpoint that was
implemented in ad1e17d into the main router.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use modernc.org/sqlite's Online Backup API (NewBackup/Step/Commit)
with in-memory destination and Serialize for consistent hot backups
without temp files. Streams zip directly to HTTP response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FFT-based breathing rate estimator: 512-sample window at 20Hz, zero-padded to 1024,
dominant peak detection in 0.1-0.5 Hz band (6-30 BPM), 60-second EMA smoothing
- Per-night statistics: breathing_rate_avg, breathing_regularity (CV), anomaly count
- Anomaly detection: 30-day personal average (EMA α=0.05), flags when >25% above baseline
- Morning briefing integration: elevated breathing rate warnings with BPM comparison
- SQLite: breathing_anomaly BOOL and breathing_samples_json columns on sleep_records
- API: GET /api/sleep includes breathing anomaly and personal average fields
- Table-driven tests for FFT accuracy, EMA convergence, anomaly thresholds, regularity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update partition layout to 4MB factory/ota_0/ota_1 partitions for larger
firmware images. Add a 60-second validation timer that starts after the
hello message is sent on boot from an OTA partition. The partition is only
marked valid (via esp_ota_mark_app_valid_cancel_rollback) after receiving
a role message from the mothership. If the timer expires without role
receipt, the partition stays unconfirmed and the bootloader rolls back to
the previous firmware on next reset.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BreathingRateEstimator: FFT peak detection on 512-sample (25.6s) phase windows
at 20Hz, zero-padded to 1024 points, 0.1-0.5 Hz band (6-30 BPM), 60s EMA smoothing
- BreathingAnomalyTracker: per-person 30-day rolling EMA (α=0.05), flags elevated
breathing at >25% above personal average
- ComputeBreathingRegularity: coefficient of variation (std/mean) with regular/normal/irregular labels
- Migration 008: add breathing_anomaly BOOL and breathing_samples_json TEXT to sleep_records
- Integration: anomaly check in GenerateMorningReports, anomaly data in report/metrics/handler JSON
- Table-driven tests: synthetic 15/12/20 BPM signals, circular buffer, noise, EMA convergence,
anomaly threshold boundary cases, JSON round-trip, regularity CV computation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Phase 5→6 merge left garbled duplicate portal API code and
missing closing braces. Fixed the zones PUT handler closure, added
missing zones DELETE handler, cleaned up duplicate portal code, and
replaced non-existent GetZoneName() calls with GetZone().Name lookups.
The phase6 build tag and Phase 5 main.go.bak were already removed
in prior work — this commit fixes the remaining compilation errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Route test requests through chi router instead of calling handlers directly
- Fix weekday test to use correct day mapping
- Use context cancellation for timeout test instead of long sleep
- Move mockServer.Close() after assertions to prevent goroutine leak
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use time.Now().In(m.tz) instead of time.Now() in reconcileOccupancy
to correctly compute midnight in the configured timezone
- Fix ReconcileTick to only mark reconciled on exact blob count match
(diff==0), keeping diff==1 as uncertain per spec
- Fix timestamp units consistency (UnixNano → UnixMilli) in crossing
event recording and retrieval
- Fix reconciliation query to use from_zone/to_zone columns
- Reset m.reconciled=false in tests that create uncertain occupancy
after manager construction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- HTTP client with 5s timeout, fire-and-forget webhook delivery
- Payload schema: {trigger_id, trigger_name, condition, blob_id, person, position, zone, dwell_s, timestamp_ms}
- 4xx response: disable trigger, set error_message, push WS alert to dashboard
- 5xx/timeout: log warning, increment error_count, do NOT disable
- error_count resets on first 2xx response
- POST /api/triggers/{id}/test endpoint with synthetic payload
- POST /api/triggers/{id}/enable clears error state and re-enables
- GET /api/triggers/{id}/webhook-log for last N firings
- Audit log via webhook_log table (migration_007)
Dashboard:
- Error badge (ERR) on trigger cards when error_message is set
- Disabled badge when trigger disabled due to 4xx
- Warning badge for transient error_count > 0
- Test Webhook button with real-time response display
- Webhook Log button showing last N firings
- Re-enable button to clear error state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Hub sends full snapshot (type=snapshot) on new client connect before adding
to broadcast list, preventing race with delta messages
- tickDelta at 10 Hz computes byte-level diffs of cached JSON fields, broadcasts
only changed fields with no type field
- Dashboard sets awaitingSnapshot on WS open, drops incremental updates until
snapshot received, rebuilds full state from snapshot
- Fix zone snapshot building (nil→ok check, SizeX fields)
- Remove unused broadcastBLEScan function
- Add drainSnapshot helper and fix TestHub_LinkEvents to account for snapshot
- Add TestHub_SnapshotOnConnect, TestHub_SnapshotBeforeDelta, TestHub_DeltaOmitsTypeField
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Formalizes the webhook audit infrastructure (webhook_log table, trigger_state
persistence, error_message/error_count columns) that was previously created
via ALTER TABLE in Store.init(). All webhook action firing, fault tolerance,
test endpoint, enable/disable, and dashboard UI were already implemented
in prior commits.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change testAlertHandler.mu from sync.Mutex to sync.RWMutex to match
RLock/RUnlock calls in alertCount/webhookCount/escalationCount
- Add proper mutex locking in SendAlert/SendWebhook/SendEscalation
- Fix TestAnomaly_UnknownBLEDevice: use security mode so score (0.8)
exceeds security threshold (0.4) instead of normal threshold (0.6)
- Fix TestAnomaly_UnusualDwell: use security mode for threshold check
- Fix TestAnomaly_SecurityModeThreshold: remove normal mode call that
creates cooldown and prevents security mode detection
- Fix TestAnomaly_AlertChainNormalMode/SecurityMode: add waitForGoroutines()
before checking alerts sent via goroutines
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SetCleanOnConnect → SetCleanSession
- OnDisconnect field + OnConnectionLost → SetConnectionLostHandler
- Remove redundant type assertions in type switch cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unused "math" import, unused startZ/endZ variables, and fix
Position struct json tags to pass go vet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>