package store import ( "database/sql" "time" ) // ICSFeed is a subscribed iCalendar feed. type ICSFeed struct { ID int64 Name string URL string Enabled bool LastFetchedAt *time.Time LastError string EventCount int CreatedAt time.Time } // AddICSFeed stores a feed. Re-adding an existing URL updates its name and // re-enables it rather than failing on the unique constraint — "add it again" // is the obvious way to try to fix a feed that was turned off. func (s *Store) AddICSFeed(name, url string) error { _, err := s.db.Exec(` INSERT INTO ics_feeds (name, url, enabled) VALUES (?, ?, 1) ON CONFLICT(url) DO UPDATE SET name = excluded.name, enabled = 1 `, name, url) return err } // GetICSFeeds returns every feed, enabled or not, newest name-order first. func (s *Store) GetICSFeeds() ([]ICSFeed, error) { return s.queryICSFeeds(` SELECT id, name, url, enabled, last_fetched_at, COALESCE(last_error, ''), event_count, created_at FROM ics_feeds ORDER BY name`) } // GetEnabledICSFeeds returns only the feeds that should be fetched. func (s *Store) GetEnabledICSFeeds() ([]ICSFeed, error) { return s.queryICSFeeds(` SELECT id, name, url, enabled, last_fetched_at, COALESCE(last_error, ''), event_count, created_at FROM ics_feeds WHERE enabled = 1 ORDER BY name`) } func (s *Store) queryICSFeeds(query string) ([]ICSFeed, error) { rows, err := s.db.Query(query) if err != nil { return nil, err } defer func() { _ = rows.Close() }() var feeds []ICSFeed for rows.Next() { var f ICSFeed var lastFetched sql.NullTime if err := rows.Scan(&f.ID, &f.Name, &f.URL, &f.Enabled, &lastFetched, &f.LastError, &f.EventCount, &f.CreatedAt); err != nil { return nil, err } if lastFetched.Valid { t := lastFetched.Time f.LastFetchedAt = &t } feeds = append(feeds, f) } return feeds, rows.Err() } // SetICSFeedEnabled turns a feed on or off without losing it. func (s *Store) SetICSFeedEnabled(id int64, enabled bool) error { _, err := s.db.Exec(`UPDATE ics_feeds SET enabled = ? WHERE id = ?`, enabled, id) return err } // DeleteICSFeed removes a feed entirely. func (s *Store) DeleteICSFeed(id int64) error { _, err := s.db.Exec(`DELETE FROM ics_feeds WHERE id = ?`, id) return err } // RecordICSFeedResult stores the outcome of a fetch. A successful fetch clears // any previous error, so a feed that recovers stops being reported as broken. func (s *Store) RecordICSFeedResult(id int64, eventCount int, fetchErr error) error { errText := "" if fetchErr != nil { errText = fetchErr.Error() } _, err := s.db.Exec(` UPDATE ics_feeds SET last_fetched_at = CURRENT_TIMESTAMP, last_error = ?, event_count = ? WHERE id = ? `, errText, eventCount, id) return err }