package httpapi import ( "bufio" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "time" "rk3588sys/agent/internal/files" ) type alarmRecord struct { ID string `json:"id"` Timestamp string `json:"timestamp"` Channel string `json:"channel"` RuleName string `json:"rule_name"` RuleType string `json:"rule_type"` ObjectLabel string `json:"object_label,omitempty"` Confidence float64 `json:"confidence,omitempty"` SnapshotURL string `json:"snapshot_url,omitempty"` ClipURL string `json:"clip_url,omitempty"` DurationMs int64 `json:"duration_ms,omitempty"` } func (s *Server) alarmsPath() string { return filepath.Join(s.baseDir, "logs", "alarms.jsonl") } // handleAlarmReport receives alarm notifications from media-server. // POST /v1/alarms/report func (s *Server) handleAlarmReport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, true) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } maxBytes := int64(1 << 20) // 1MB max r.Body = http.MaxBytesReader(w, r.Body, maxBytes) var alarm alarmRecord if err := json.NewDecoder(r.Body).Decode(&alarm); err != nil { errorJSON(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } if strings.TrimSpace(alarm.Channel) == "" { errorJSON(w, http.StatusBadRequest, "channel is required") return } if strings.TrimSpace(alarm.RuleName) == "" { errorJSON(w, http.StatusBadRequest, "rule_name is required") return } if alarm.ID == "" { alarm.ID = fmt.Sprintf("alarm_%s_%s", time.Now().Format("20060102_150405"), randomHex(6)) } if alarm.Timestamp == "" { alarm.Timestamp = time.Now().Format(time.RFC3339) } path := s.alarmsPath() dir := filepath.Dir(path) if err := files.EnsureDir(dir, 0o755); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } line, err := json.Marshal(alarm) if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } defer f.Close() if _, err := fmt.Fprintf(f, "%s\n", string(line)); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } s.recordAudit(r, "alarm.report", true, alarm.ID) writeJSON(w, http.StatusOK, map[string]any{"ok": true, "id": alarm.ID}) } // handleAlarmsRecent returns recent alarm records. // GET /v1/alarms/recent?limit=50&since=2026-01-01T00:00:00Z func (s *Server) handleAlarmsRecent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } limit := 50 if v := strings.TrimSpace(r.URL.Query().Get("limit")); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 500 { limit = n } } alarms, err := s.readRecentAlarms(limit) if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } if alarms == nil { alarms = make([]alarmRecord, 0) } writeJSON(w, http.StatusOK, map[string]any{"alarms": alarms}) } func (s *Server) readRecentAlarms(limit int) ([]alarmRecord, error) { path := s.alarmsPath() f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } defer f.Close() // Read all lines and keep last N var lines []string scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 1<<20), 1<<20) // 1MB max line for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == "" { continue } lines = append(lines, line) // Keep only last 2*limit lines in memory if len(lines) > 2*limit { lines = lines[limit:] } } if err := scanner.Err(); err != nil && err != io.EOF { return nil, err } // Take last `limit` lines start := 0 if len(lines) > limit { start = len(lines) - limit } alarms := make([]alarmRecord, 0, limit) for i := start; i < len(lines); i++ { var alarm alarmRecord if err := json.Unmarshal([]byte(lines[i]), &alarm); err != nil { continue // skip malformed lines } alarms = append(alarms, alarm) } return alarms, nil } func randomHex(n int) string { b := make([]byte, n) rand.Read(b) return hex.EncodeToString(b)[:n] }