generated from CubeCraft-Creations/Tracehound
CUB-186: Camera list and register API handlers + Chi router wiring
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
// Package api provides HTTP handlers for camera operations.
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/cubecraft/remoterig/internal/db"
|
||||
"github.com/cubecraft/remoterig/pkg/models"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// ListCameras returns a handler for GET /cameras.
|
||||
// Returns all cameras with their latest status from status_logs.
|
||||
func ListCameras(database *db.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Query: join cameras with their most recent status_logs row
|
||||
rows, err := database.QueryContext(ctx, `
|
||||
SELECT
|
||||
c.camera_id,
|
||||
c.friendly_name,
|
||||
s.battery_pct,
|
||||
s.video_remaining_sec,
|
||||
s.recording_state,
|
||||
s.mode,
|
||||
s.resolution,
|
||||
s.fps,
|
||||
s.online,
|
||||
s.recorded_at
|
||||
FROM cameras c
|
||||
LEFT JOIN (
|
||||
SELECT camera_id, battery_pct, video_remaining_sec, recording_state,
|
||||
mode, resolution, fps, online, recorded_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY camera_id ORDER BY recorded_at DESC) as rn
|
||||
FROM status_logs
|
||||
) s ON c.camera_id = s.camera_id AND s.rn = 1
|
||||
ORDER BY c.camera_id
|
||||
`)
|
||||
if err != nil {
|
||||
log.Printf("Error querying cameras: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var statuses []models.CameraStatus
|
||||
for rows.Next() {
|
||||
var sl models.StatusLog
|
||||
var c models.Camera
|
||||
if err := rows.Scan(
|
||||
&c.CameraID, &c.FriendlyName,
|
||||
&sl.BatteryPct, &sl.VideoRemainingSec,
|
||||
&sl.RecordingState, &sl.Mode, &sl.Resolution, &sl.FPS,
|
||||
&sl.Online, &sl.RecordedAt,
|
||||
); err != nil {
|
||||
log.Printf("Error scanning camera row: %v", err)
|
||||
continue
|
||||
}
|
||||
statuses = append(statuses, models.NewCameraStatus(c, sl))
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("Error iterating camera rows: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
|
||||
return
|
||||
}
|
||||
|
||||
if statuses == nil {
|
||||
statuses = []models.CameraStatus{}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, statuses)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCamera returns a handler for POST /cameras.
|
||||
func RegisterCamera(database *db.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
CameraID string `json:"camera_id"`
|
||||
FriendlyName string `json:"friendly_name"`
|
||||
MacAddress *string `json:"mac_address,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.CameraID == "" || req.FriendlyName == "" {
|
||||
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "camera_id and friendly_name are required"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := database.ExecContext(r.Context(), `
|
||||
INSERT INTO cameras (camera_id, friendly_name, mac_address)
|
||||
VALUES (?, ?, ?)
|
||||
`, req.CameraID, req.FriendlyName, req.MacAddress)
|
||||
if err != nil {
|
||||
if err.Error() == "UNIQUE constraint failed: cameras.mac_address" {
|
||||
respondJSON(w, http.StatusConflict, map[string]string{"error": "camera with this mac_address already registered"})
|
||||
return
|
||||
}
|
||||
log.Printf("Error registering camera: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Registered camera %s (%s)", req.CameraID, req.FriendlyName)
|
||||
resp := map[string]interface{}{
|
||||
"camera_id": req.CameraID,
|
||||
"friendly_name": req.FriendlyName,
|
||||
}
|
||||
if req.MacAddress != nil {
|
||||
resp["mac_address"] = *req.MacAddress
|
||||
}
|
||||
respondJSON(w, http.StatusCreated, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCameraDetail returns a handler for GET /cameras/{id}.
|
||||
func GetCameraDetail(database *db.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cameraID := chi.URLParam(r, "id")
|
||||
if cameraID == "" {
|
||||
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "camera_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get camera info
|
||||
var c models.Camera
|
||||
err := database.QueryRowContext(r.Context(), `
|
||||
SELECT camera_id, friendly_name, mac_address, created_at, updated_at
|
||||
FROM cameras WHERE camera_id = ?
|
||||
`, cameraID).Scan(
|
||||
&c.CameraID, &c.FriendlyName, &c.MacAddress,
|
||||
&c.CreatedAt, &c.UpdatedAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
respondJSON(w, http.StatusNotFound, map[string]string{"error": "camera not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Error querying camera: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get latest status
|
||||
var sl models.StatusLog
|
||||
err = database.QueryRowContext(r.Context(), `
|
||||
SELECT id, camera_id, recorded_at, battery_pct, video_remaining_sec,
|
||||
recording_state, mode, resolution, fps, online, raw_battery_pct
|
||||
FROM status_logs
|
||||
WHERE camera_id = ?
|
||||
ORDER BY recorded_at DESC
|
||||
LIMIT 1
|
||||
`, cameraID).Scan(
|
||||
&sl.ID, &sl.CameraID, &sl.RecordedAt,
|
||||
&sl.BatteryPct, &sl.VideoRemainingSec,
|
||||
&sl.RecordingState, &sl.Mode, &sl.Resolution, &sl.FPS,
|
||||
&sl.Online, &sl.RawBatteryPct,
|
||||
)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Printf("Error querying latest status: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get 24h history
|
||||
historyRows, err := database.QueryContext(r.Context(), `
|
||||
SELECT id, camera_id, recorded_at, battery_pct, video_remaining_sec,
|
||||
recording_state, mode, resolution, fps, online, raw_battery_pct
|
||||
FROM status_logs
|
||||
WHERE camera_id = ? AND recorded_at >= datetime('now', '-24 hours')
|
||||
ORDER BY recorded_at DESC
|
||||
LIMIT 100
|
||||
`, cameraID)
|
||||
if err != nil {
|
||||
log.Printf("Error querying history: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, map[string]string{"error": "database error"})
|
||||
return
|
||||
}
|
||||
defer historyRows.Close()
|
||||
|
||||
var history []models.StatusLog
|
||||
for historyRows.Next() {
|
||||
var h models.StatusLog
|
||||
if err := historyRows.Scan(
|
||||
&h.ID, &h.CameraID, &h.RecordedAt,
|
||||
&h.BatteryPct, &h.VideoRemainingSec,
|
||||
&h.RecordingState, &h.Mode, &h.Resolution, &h.FPS,
|
||||
&h.Online, &h.RawBatteryPct,
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
history = append(history, h)
|
||||
}
|
||||
if history == nil {
|
||||
history = []models.StatusLog{}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"camera": c,
|
||||
"last_status": sl,
|
||||
"history": history,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// respondJSON writes a JSON response with the given status code.
|
||||
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
Reference in New Issue
Block a user