diff --git a/internal/api/cameras.go b/internal/api/cameras.go index 261720b..755adca 100644 --- a/internal/api/cameras.go +++ b/internal/api/cameras.go @@ -26,12 +26,12 @@ func ListCameras(database *db.DB) http.HandlerFunc { c.friendly_name, s.battery_pct, s.video_remaining_sec, - s.recording_state, + COALESCE(s.recording_state, 0), s.mode, s.resolution, s.fps, - s.online, - s.recorded_at + COALESCE(s.online, 0), + COALESCE(s.recorded_at, c.created_at) FROM cameras c LEFT JOIN ( SELECT camera_id, battery_pct, video_remaining_sec, recording_state, diff --git a/internal/mqtt/subscriber.go b/internal/mqtt/subscriber.go index 7c274d0..abe5cf8 100644 --- a/internal/mqtt/subscriber.go +++ b/internal/mqtt/subscriber.go @@ -282,10 +282,12 @@ func (s *Subscriber) handleStatus(cameraID string, payload []byte) { // ── Heartbeat handler ─────────────────────────────────────────────────── type heartbeatPayload struct { - CameraID string `json:"camera_id"` - Timestamp string `json:"timestamp"` - UptimeSec *int `json:"uptime_sec"` - FreeHeap *int `json:"free_heap"` + CameraID string `json:"camera_id"` + // No Timestamp field: the node sends a numeric millis() value and the + // handler doesn't use it; omitting the field lets it be ignored instead + // of failing JSON unmarshal (number into string). + UptimeSec *int `json:"uptime_sec"` + FreeHeap *int `json:"free_heap"` } func (s *Subscriber) handleHeartbeat(cameraID string, payload []byte) { @@ -360,8 +362,8 @@ func (s *Subscriber) handleAnnounce(cameraID string, payload []byte) { "SELECT camera_id FROM cameras WHERE mac_address = ?", ap.MacAddress, ).Scan(&existingID) - if err == nil { - // Already registered — just update friendly_name + if err == nil && existingID == cameraID { + // Same self-id re-connecting — just refresh friendly_name. _, err = s.db.Exec( "UPDATE cameras SET friendly_name = ?, updated_at = datetime('now') WHERE camera_id = ?", ap.FriendlyName, existingID, @@ -372,6 +374,12 @@ func (s *Subscriber) handleAnnounce(cameraID string, payload []byte) { } log.Printf("MQTT announce: camera %s (%s) re-connected", existingID, ap.FriendlyName) } else { + // MAC known under a different id (legacy cam-NNN from before self-IDs) + // → drop the old row so we re-register under the node's self-id. + if err == nil && existingID != cameraID { + s.db.Exec("DELETE FROM cameras WHERE camera_id = ?", existingID) + log.Printf("MQTT announce: migrating %s -> %s (%s)", existingID, cameraID, ap.FriendlyName) + } // Option B: the node self-assigns its camera_id (the announce topic id). _, err = s.db.Exec(` INSERT INTO cameras (camera_id, friendly_name, mac_address, created_at, updated_at)