package service import ( "context" "net/http" "sort" "sync" "time" "cmr-backend/internal/apperr" "cmr-backend/internal/store/postgres" ) type DevService struct { appEnv string store *postgres.Store mu sync.Mutex logSeq int64 logs []ClientDebugLogEntry } type ClientDebugLogEntry struct { ID int64 `json:"id"` Source string `json:"source"` Level string `json:"level"` Category string `json:"category,omitempty"` Message string `json:"message"` EventID string `json:"eventId,omitempty"` ReleaseID string `json:"releaseId,omitempty"` SessionID string `json:"sessionId,omitempty"` ManifestURL string `json:"manifestUrl,omitempty"` Route string `json:"route,omitempty"` OccurredAt time.Time `json:"occurredAt"` ReceivedAt time.Time `json:"receivedAt"` Details map[string]any `json:"details,omitempty"` } type CreateClientDebugLogInput struct { Source string `json:"source"` Level string `json:"level"` Category string `json:"category"` Message string `json:"message"` EventID string `json:"eventId"` ReleaseID string `json:"releaseId"` SessionID string `json:"sessionId"` ManifestURL string `json:"manifestUrl"` Route string `json:"route"` OccurredAt string `json:"occurredAt"` Details map[string]any `json:"details"` } func NewDevService(appEnv string, store *postgres.Store) *DevService { return &DevService{ appEnv: appEnv, store: store, } } func (s *DevService) Enabled() bool { return s.appEnv != "production" } func (s *DevService) BootstrapDemo(ctx context.Context) (*postgres.DemoBootstrapSummary, error) { if !s.Enabled() { return nil, apperr.New(http.StatusNotFound, "not_found", "dev bootstrap is disabled") } return s.store.EnsureDemoData(ctx) } func (s *DevService) AddClientDebugLog(_ context.Context, input CreateClientDebugLogInput) (*ClientDebugLogEntry, error) { if !s.Enabled() { return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled") } if input.Message == "" { return nil, apperr.New(http.StatusBadRequest, "invalid_request", "message is required") } if input.Source == "" { input.Source = "unknown" } if input.Level == "" { input.Level = "info" } occurredAt := time.Now().UTC() if input.OccurredAt != "" { parsed, err := time.Parse(time.RFC3339, input.OccurredAt) if err != nil { return nil, apperr.New(http.StatusBadRequest, "invalid_request", "occurredAt must be RFC3339") } occurredAt = parsed.UTC() } entry := ClientDebugLogEntry{ Source: input.Source, Level: input.Level, Category: input.Category, Message: input.Message, EventID: input.EventID, ReleaseID: input.ReleaseID, SessionID: input.SessionID, ManifestURL: input.ManifestURL, Route: input.Route, OccurredAt: occurredAt, ReceivedAt: time.Now().UTC(), Details: input.Details, } s.mu.Lock() defer s.mu.Unlock() s.logSeq++ entry.ID = s.logSeq s.logs = append(s.logs, entry) if len(s.logs) > 200 { s.logs = append([]ClientDebugLogEntry(nil), s.logs[len(s.logs)-200:]...) } copyEntry := entry return ©Entry, nil } func (s *DevService) ListClientDebugLogs(_ context.Context, limit int) ([]ClientDebugLogEntry, error) { if !s.Enabled() { return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled") } if limit <= 0 || limit > 200 { limit = 50 } s.mu.Lock() defer s.mu.Unlock() items := append([]ClientDebugLogEntry(nil), s.logs...) sort.Slice(items, func(i, j int) bool { return items[i].ID > items[j].ID }) if len(items) > limit { items = items[:limit] } return items, nil } func (s *DevService) ClearClientDebugLogs(_ context.Context) error { if !s.Enabled() { return apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled") } s.mu.Lock() defer s.mu.Unlock() s.logs = nil return nil }