package alerts import ( "encoding/json" "os" "path/filepath" "sort" "strconv" "sync" "time" ) type Status string const ( StatusOpen Status = "open" StatusAcked Status = "acked" StatusClosed Status = "closed" ) type Alert struct { ID string `json:"id"` Title string `json:"title"` Severity string `json:"severity"` Status Status `json:"status"` Group string `json:"group"` Code string `json:"code"` Trace string `json:"trace"` Message string `json:"message"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Meta map[string]string `json:"meta,omitempty"` } type Store struct { path string mu sync.Mutex } func NewStore(path string) *Store { return &Store{path: path} } func (s *Store) Add(alert Alert) error { s.mu.Lock() defer s.mu.Unlock() alertsList, err := s.readLocked() if err != nil { return err } now := time.Now().UTC() if alert.ID == "" { alert.ID = strconv.FormatInt(now.UnixNano(), 10) } if alert.Status == "" { alert.Status = StatusOpen } if alert.CreatedAt.IsZero() { alert.CreatedAt = now } alert.UpdatedAt = now alertsList = append(alertsList, alert) return s.writeLocked(alertsList) } func (s *Store) List(limit int) ([]Alert, error) { s.mu.Lock() defer s.mu.Unlock() alertsList, err := s.readLocked() if err != nil { return nil, err } sort.Slice(alertsList, func(i, j int) bool { return alertsList[i].CreatedAt.After(alertsList[j].CreatedAt) }) if limit > 0 && len(alertsList) > limit { return alertsList[:limit], nil } return alertsList, nil } func (s *Store) SetStatus(ids []string, status Status) error { s.mu.Lock() defer s.mu.Unlock() alertsList, err := s.readLocked() if err != nil { return err } target := map[string]bool{} for _, id := range ids { target[id] = true } now := time.Now().UTC() for i := range alertsList { if target[alertsList[i].ID] { alertsList[i].Status = status alertsList[i].UpdatedAt = now } } return s.writeLocked(alertsList) } func (s *Store) Delete(ids []string) error { s.mu.Lock() defer s.mu.Unlock() alertsList, err := s.readLocked() if err != nil { return err } target := map[string]bool{} for _, id := range ids { target[id] = true } kept := make([]Alert, 0, len(alertsList)) for _, alert := range alertsList { if !target[alert.ID] { kept = append(kept, alert) } } return s.writeLocked(kept) } func (s *Store) readLocked() ([]Alert, error) { data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return []Alert{}, nil } return nil, err } if len(data) == 0 { return []Alert{}, nil } var alertsList []Alert if err := json.Unmarshal(data, &alertsList); err != nil { return []Alert{}, nil } return alertsList, nil } func (s *Store) writeLocked(alertsList []Alert) error { if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil { return err } data, err := json.MarshalIndent(alertsList, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0644) }