package db import ( "fmt" "time" ) func (s *Store) DashboardOverview(limit int) (map[string]any, error) { if limit <= 0 || limit > 200 { limit = 80 } feedbackTotal, _ := s.countTable("feedback_tickets") feedbackToday, _ := s.countWhere("feedback_tickets", "created_at LIKE ?", time.Now().UTC().Format("2006-01-02")+"%") sourceTotal, _ := s.countTable("source_endpoints") sourceVisible, _ := s.countWhere("source_endpoints", "enabled = 1 AND client_visible = 1") releaseTotal, _ := s.countTable("release_notices") mailFailed, _ := s.countWhere("mail_records", "status = ?", "failed") statusCounts, _ := s.groupCounts("feedback_tickets", "status") healthCounts, _ := s.groupCounts("source_endpoints", "last_status") recentChecks, _ := s.RecentSourceChecks(limit) recentCalls, _ := s.RecentSourceCalls(limit) audit, _ := s.ListAuditLogs(10) return map[string]any{ "ok": true, "kpis": map[string]any{ "feedbackTotal": feedbackTotal, "feedbackToday": feedbackToday, "sourceTotal": sourceTotal, "sourceVisible": sourceVisible, "releaseNotices": releaseTotal, "mailFailed": mailFailed, }, "feedbackStatus": statusCounts, "sourceHealth": healthCounts, "heartbeats": recentChecks, "clientCalls": recentCalls, "database": s.Status(), "audit": audit, }, nil } func (s *Store) RecentSourceChecks(limit int) ([]map[string]any, error) { rows, err := s.query(`SELECT h.id, e.source_id, e.name, h.status, h.latency_ms, h.error, h.checked_at FROM endpoint_health_checks h LEFT JOIN source_endpoints e ON e.id = h.source_db_id ORDER BY h.checked_at DESC, h.id DESC LIMIT ?`, limit) if err != nil { return nil, err } defer rows.Close() items := []map[string]any{} for rows.Next() { var id int64 var sourceID, name, status, message, checkedAt string var latency int if err := rows.Scan(&id, &sourceID, &name, &status, &latency, &message, &checkedAt); err != nil { return nil, err } items = append(items, map[string]any{"id": id, "sourceId": sourceID, "name": name, "status": status, "latencyMs": latency, "error": message, "checkedAt": checkedAt}) } return items, rows.Err() } func (s *Store) RecentSourceCalls(limit int) ([]map[string]any, error) { rows, err := s.query(`SELECT id, source_id, status, latency_ms, error, client, created_at FROM endpoint_call_logs ORDER BY created_at DESC, id DESC LIMIT ?`, limit) if err != nil { return nil, err } defer rows.Close() items := []map[string]any{} for rows.Next() { var id int64 var sourceID, status, message, client, createdAt string var latency int if err := rows.Scan(&id, &sourceID, &status, &latency, &message, &client, &createdAt); err != nil { return nil, err } items = append(items, map[string]any{"id": id, "sourceId": sourceID, "status": status, "latencyMs": latency, "error": message, "client": client, "createdAt": createdAt}) } return items, rows.Err() } func (s *Store) InsertAudit(log AuditLog) error { if log.CreatedAt == "" { log.CreatedAt = Now() } _, err := s.exec(`INSERT INTO audit_logs (actor, type, target, message, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, sanitize(log.Actor), sanitize(log.Type), sanitize(log.Target), sanitize(log.Message), sanitize(log.IP), sanitize(log.UserAgent), log.CreatedAt) return err } func (s *Store) ListAuditLogs(limit int) ([]AuditLog, error) { if limit <= 0 || limit > 200 { limit = 100 } rows, err := s.query(`SELECT id, actor, type, target, message, ip, user_agent, created_at FROM audit_logs ORDER BY id DESC LIMIT ?`, limit) if err != nil { return nil, err } defer rows.Close() return scanAuditRows(rows) } func (s *Store) ListAuditLogsForTarget(target string, limit int) ([]AuditLog, error) { if limit <= 0 || limit > 200 { limit = 100 } rows, err := s.query(`SELECT id, actor, type, target, message, ip, user_agent, created_at FROM audit_logs WHERE target = ? ORDER BY id DESC LIMIT ?`, target, limit) if err != nil { return nil, err } defer rows.Close() return scanAuditRows(rows) } func (s *Store) countTable(table string) (int, error) { if !validStatsTable(table) { return 0, fmt.Errorf("invalid table %q", table) } var total int err := s.queryRow(`SELECT COUNT(*) FROM ` + table).Scan(&total) return total, err } func (s *Store) countWhere(table, where string, args ...any) (int, error) { if !validStatsTable(table) { return 0, fmt.Errorf("invalid table %q", table) } var total int err := s.queryRow(`SELECT COUNT(*) FROM `+table+` WHERE `+where, args...).Scan(&total) return total, err } func (s *Store) groupCounts(table, column string) (map[string]int, error) { if !validStatsColumn(table, column) { return nil, fmt.Errorf("invalid group %s.%s", table, column) } rows, err := s.query(`SELECT ` + column + `, COUNT(*) FROM ` + table + ` GROUP BY ` + column) if err != nil { return nil, err } defer rows.Close() out := map[string]int{} for rows.Next() { var key string var count int if err := rows.Scan(&key, &count); err != nil { return nil, err } if key == "" { key = "unknown" } out[key] = count } return out, rows.Err() } func validStatsTable(table string) bool { switch table { case "feedback_tickets", "source_endpoints", "release_notices", "mail_records": return true default: return false } } func validStatsColumn(table, column string) bool { switch table + "." + column { case "feedback_tickets.status", "source_endpoints.last_status": return true default: return false } }