package db import ( "encoding/json" "fmt" "sort" "strings" ) func (s *Store) ListSystemLogsPage(filters SystemLogFilters) (SystemLogPage, error) { page := filters.Page if page <= 0 { page = 1 } perPage := filters.PerPage if perPage <= 0 { perPage = 35 } if perPage > 100 { perPage = 100 } items, err := s.collectSystemLogs(filters) if err != nil { return SystemLogPage{}, err } sort.SliceStable(items, func(i, j int) bool { if items[i].CreatedAt == items[j].CreatedAt { return items[i].ID > items[j].ID } return items[i].CreatedAt > items[j].CreatedAt }) total := len(items) start := (page - 1) * perPage if start > total { start = total } end := start + perPage if end > total { end = total } return SystemLogPage{Items: items[start:end], Total: total, Page: page, PerPage: perPage}, nil } func (s *Store) collectSystemLogs(filters SystemLogFilters) ([]SystemLogItem, error) { items := []SystemLogItem{} appendItems := func(category string, next []SystemLogItem, err error) error { if err != nil { return err } for _, item := range next { if matchesSystemLog(filters, item, category) { items = append(items, item) } } return nil } logs, err := s.operationLogs() if err := appendItems("operation", logs, err); err != nil { return nil, err } logs, err = s.healthLogs() if err := appendItems("health", logs, err); err != nil { return nil, err } logs, err = s.clientCallLogs() if err := appendItems("client", logs, err); err != nil { return nil, err } logs, err = s.databaseSyncLogs() if err := appendItems("database_sync", logs, err); err != nil { return nil, err } logs, err = s.legacySyncLogs() if err := appendItems("legacy_sync", logs, err); err != nil { return nil, err } return items, nil } func (s *Store) operationLogs() ([]SystemLogItem, error) { rows, err := s.query(`SELECT id, actor, type, target, message, ip, user_agent, created_at FROM audit_logs ORDER BY id DESC LIMIT 500`) if err != nil { return nil, err } defer rows.Close() items := []SystemLogItem{} for rows.Next() { var item AuditLog if err := rows.Scan(&item.ID, &item.Actor, &item.Type, &item.Target, &item.Message, &item.IP, &item.UserAgent, &item.CreatedAt); err != nil { return nil, err } items = append(items, SystemLogItem{ ID: item.ID, Category: "operation", Type: item.Type, Target: item.Target, Status: firstNonEmpty(item.Actor, "system"), Message: item.Message, Detail: strings.TrimSpace(item.IP + " " + item.UserAgent), CreatedAt: item.CreatedAt, }) } return items, rows.Err() } func (s *Store) healthLogs() ([]SystemLogItem, error) { rows, err := s.query(`SELECT h.id, h.source_db_id, COALESCE(e.source_id, ''), COALESCE(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.id DESC LIMIT 500`) if err != nil { return nil, err } defer rows.Close() items := []SystemLogItem{} for rows.Next() { var id, sourceDBID int64 var sourceID, name, status, message, checkedAt string var latency int if err := rows.Scan(&id, &sourceDBID, &sourceID, &name, &status, &latency, &message, &checkedAt); err != nil { return nil, err } target := firstNonEmpty(sourceID, fmt.Sprintf("source#%d", sourceDBID)) items = append(items, SystemLogItem{ ID: id, Category: "health", Type: "endpoint.health", Target: target, Status: status, Message: firstNonEmpty(name, target), Detail: fmt.Sprintf("%dms %s", latency, message), CreatedAt: checkedAt, }) } return items, rows.Err() } func (s *Store) clientCallLogs() ([]SystemLogItem, error) { rows, err := s.query(`SELECT id, source_id, status, latency_ms, error, client, created_at FROM endpoint_call_logs ORDER BY id DESC LIMIT 500`) if err != nil { return nil, err } defer rows.Close() items := []SystemLogItem{} 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, SystemLogItem{ ID: id, Category: "client", Type: "endpoint.call", Target: sourceID, Status: status, Message: message, Detail: fmt.Sprintf("%dms %s", latency, client), CreatedAt: createdAt, }) } return items, rows.Err() } func (s *Store) databaseSyncLogs() ([]SystemLogItem, error) { rows, err := s.query(`SELECT id, direction, status, message, tables_json, started_at, finished_at FROM database_sync_jobs ORDER BY id DESC LIMIT 500`) if err != nil { return nil, err } defer rows.Close() items := []SystemLogItem{} for rows.Next() { job, err := scanDatabaseSyncJob(rows) if err != nil { return nil, err } detail, _ := json.Marshal(map[string]any{"tables": job.Tables, "warnings": job.Warnings, "errors": job.Errors}) items = append(items, SystemLogItem{ ID: job.ID, Category: "database_sync", Type: job.Direction, Target: directionLabel(job.Direction), Status: job.Status, Message: strings.Join(job.Output, "\n"), Detail: string(detail), CreatedAt: firstNonEmpty(job.FinishedAt, job.StartedAt), }) } return items, rows.Err() } func (s *Store) legacySyncLogs() ([]SystemLogItem, error) { rows, err := s.query(`SELECT id, status, summary, stats_json, started_at, finished_at FROM legacy_sync_jobs ORDER BY id DESC LIMIT 500`) if err != nil { return nil, err } defer rows.Close() items := []SystemLogItem{} for rows.Next() { var item LegacySyncJob if err := rows.Scan(&item.ID, &item.Status, &item.Summary, &item.StatsJSON, &item.StartedAt, &item.FinishedAt); err != nil { return nil, err } items = append(items, SystemLogItem{ ID: item.ID, Category: "legacy_sync", Type: "legacy.sync", Target: "legacy", Status: item.Status, Message: item.Summary, Detail: item.StatsJSON, CreatedAt: firstNonEmpty(item.FinishedAt, item.StartedAt), }) } return items, rows.Err() } func matchesSystemLog(filters SystemLogFilters, item SystemLogItem, category string) bool { if value := strings.TrimSpace(filters.Category); value != "" && value != category && value != item.Category { return false } if value := strings.ToLower(strings.TrimSpace(filters.Query)); value != "" { haystack := strings.ToLower(strings.Join([]string{item.Category, item.Type, item.Target, item.Status, item.Message, item.Detail, item.CreatedAt}, " ")) return strings.Contains(haystack, value) } return true }