// Package api provides HTTP handlers for camera operations. package api import ( "log" "net/http" "github.com/cubecraft/remoterig/internal/db" "github.com/go-chi/chi/v5" ) // PushStatus accepts a status update from an ESP32 node and persists it. func PushStatus(database *db.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cameraID := chi.URLParam(r, "id") if !validateCameraID(w, cameraID) { return } var req struct { BatteryPct *int `json:"battery_pct"` VideoRemainingSec *int `json:"video_remaining_sec"` Recording bool `json:"recording"` Mode string `json:"mode"` Resolution string `json:"resolution"` FPS int `json:"fps"` Online bool `json:"online"` RawBatteryPct *float64 `json:"raw_battery_pct"` Timestamp *string `json:"ts"` } if !decodeJSONBody(w, r, &req) { return } if !validateStatusFields(w, req.Mode, req.Resolution, req.FPS) { return } // Validate battery percentage range if provided if req.BatteryPct != nil && (*req.BatteryPct < 0 || *req.BatteryPct > 100) { respondError(w, http.StatusBadRequest, "battery_pct must be between 0 and 100") return } // Check if camera is registered var exists int err := database.QueryRowContext(r.Context(), "SELECT COUNT(*) FROM cameras WHERE camera_id = ?", cameraID).Scan(&exists) if err != nil { log.Printf("Error checking camera existence: %v", err) respondError(w, http.StatusInternalServerError, "database error", err.Error()) return } if exists == 0 { respondError(w, http.StatusNotFound, "camera not found") return } // Insert status log result, err := database.ExecContext(r.Context(), ` INSERT INTO status_logs (camera_id, battery_pct, video_remaining_sec, recording_state, mode, resolution, fps, online, raw_battery_pct) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, cameraID, req.BatteryPct, req.VideoRemainingSec, boolToInt(req.Recording), req.Mode, req.Resolution, req.FPS, boolToInt(req.Online), req.RawBatteryPct) if err != nil { log.Printf("Error inserting status log: %v", err) respondError(w, http.StatusInternalServerError, "database error", err.Error()) return } // Check if recording state changed - update recording_events if so var prevRecording int err = database.QueryRowContext(r.Context(), ` SELECT recording_state FROM status_logs WHERE camera_id = ? AND recorded_at > datetime('now', '-60 seconds') ORDER BY recorded_at DESC LIMIT 1 `, cameraID).Scan(&prevRecording) if err == nil && prevRecording != boolToInt(req.Recording) { reason := "manual" if req.Recording { // Start recording - open a new event _, err := database.ExecContext(r.Context(), ` INSERT INTO recording_events (camera_id, started_at, reason) VALUES (?, datetime('now'), ?) `, cameraID, reason) if err != nil { log.Printf("Error inserting recording event: %v", err) } } else { // Stop recording - close the most recent open event _, err := database.ExecContext(r.Context(), ` UPDATE recording_events SET stopped_at = datetime('now') WHERE camera_id = ? AND stopped_at IS NULL ORDER BY started_at DESC LIMIT 1 `, cameraID) if err != nil { log.Printf("Error updating recording event: %v", err) } } } _, _ = result.RowsAffected() // consume the result respondJSON(w, http.StatusOK, map[string]string{ "status": "accepted", }) } } // boolToInt converts a bool to 0 or 1 for SQLite storage. func boolToInt(b bool) int { if b { return 1 } return 0 }