package db import ( "database/sql" "fmt" "os" "path/filepath" "time" _ "modernc.org/sqlite" ) type Alarm struct { ID int Name string Description string Time string // HH:MM format for simple alarms Trigger string // Cron expression or "once" SoundPath string Enabled bool CreatedAt time.Time UpdatedAt time.Time LastTriggered *time.Time SnoozeCount int } type AlarmHistory struct { ID int AlarmID int TriggeredAt time.Time DismissedAt *time.Time SnoozedCount int } type Store struct { db *sql.DB } func Open() (*Store, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("get home dir: %w", err) } dir := filepath.Join(home, ".config", "woke") if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create config dir: %w", err) } dbPath := filepath.Join(dir, "woke.db") db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("open database: %w", err) } store := &Store{db: db} if err := store.migrate(); err != nil { db.Close() return nil, fmt.Errorf("migrate: %w", err) } return store, nil } func (s *Store) Close() error { return s.db.Close() } func (s *Store) migrate() error { _, err := s.db.Exec(` CREATE TABLE IF NOT EXISTS alarms ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT DEFAULT '', time TEXT DEFAULT '', trigger TEXT NOT NULL, sound_path TEXT NOT NULL DEFAULT 'default', enabled BOOLEAN DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_triggered TIMESTAMP, snooze_count INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS alarm_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, alarm_id INTEGER, triggered_at TIMESTAMP, dismissed_at TIMESTAMP, snoozed_count INTEGER DEFAULT 0, FOREIGN KEY (alarm_id) REFERENCES alarms(id) ); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT ); `) return err } func (s *Store) ListAlarms() ([]Alarm, error) { rows, err := s.db.Query(` SELECT id, name, description, time, trigger, sound_path, enabled, created_at, updated_at, last_triggered, snooze_count FROM alarms ORDER BY time ASC, name ASC `) if err != nil { return nil, err } defer rows.Close() var alarms []Alarm for rows.Next() { var a Alarm var lastTriggered sql.NullTime err := rows.Scan(&a.ID, &a.Name, &a.Description, &a.Time, &a.Trigger, &a.SoundPath, &a.Enabled, &a.CreatedAt, &a.UpdatedAt, &lastTriggered, &a.SnoozeCount) if err != nil { return nil, err } if lastTriggered.Valid { a.LastTriggered = &lastTriggered.Time } alarms = append(alarms, a) } return alarms, rows.Err() } func (s *Store) GetAlarm(id int) (Alarm, error) { var a Alarm var lastTriggered sql.NullTime err := s.db.QueryRow(` SELECT id, name, description, time, trigger, sound_path, enabled, created_at, updated_at, last_triggered, snooze_count FROM alarms WHERE id = ? `, id).Scan(&a.ID, &a.Name, &a.Description, &a.Time, &a.Trigger, &a.SoundPath, &a.Enabled, &a.CreatedAt, &a.UpdatedAt, &lastTriggered, &a.SnoozeCount) if lastTriggered.Valid { a.LastTriggered = &lastTriggered.Time } return a, err } func (s *Store) CreateAlarm(a Alarm) (int, error) { result, err := s.db.Exec(` INSERT INTO alarms (name, description, time, trigger, sound_path, enabled) VALUES (?, ?, ?, ?, ?, ?) `, a.Name, a.Description, a.Time, a.Trigger, a.SoundPath, a.Enabled) if err != nil { return 0, err } id, err := result.LastInsertId() return int(id), err } func (s *Store) UpdateAlarm(a Alarm) error { _, err := s.db.Exec(` UPDATE alarms SET name=?, description=?, time=?, trigger=?, sound_path=?, enabled=?, updated_at=CURRENT_TIMESTAMP WHERE id=? `, a.Name, a.Description, a.Time, a.Trigger, a.SoundPath, a.Enabled, a.ID) return err } func (s *Store) DeleteAlarm(id int) error { _, err := s.db.Exec(`DELETE FROM alarms WHERE id=?`, id) return err } func (s *Store) ToggleAlarm(id int) error { _, err := s.db.Exec(` UPDATE alarms SET enabled = NOT enabled, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, id) return err } func (s *Store) MarkTriggered(id int) error { now := time.Now() _, err := s.db.Exec(` UPDATE alarms SET last_triggered = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, now, id) if err != nil { return err } _, err = s.db.Exec(` INSERT INTO alarm_history (alarm_id, triggered_at) VALUES (?, ?) `, id, now) return err } // Settings holds all configurable app settings. type Settings struct { SnoozeMinutes int TimeoutMinutes int DefaultSound string BlinkOnMs int BlinkOffMs int ColorClock string ColorAlarm string } func DefaultSettings() Settings { return Settings{ SnoozeMinutes: 5, TimeoutMinutes: 5, DefaultSound: "default", BlinkOnMs: 1000, BlinkOffMs: 500, ColorClock: "#00FF88", ColorAlarm: "#FF4444", } } func (s *Store) LoadSettings() Settings { cfg := DefaultSettings() cfg.SnoozeMinutes = s.getSettingInt("snooze_minutes", cfg.SnoozeMinutes) cfg.TimeoutMinutes = s.getSettingInt("timeout_minutes", cfg.TimeoutMinutes) cfg.DefaultSound = s.GetSetting("default_sound", cfg.DefaultSound) cfg.BlinkOnMs = s.getSettingInt("blink_on_ms", cfg.BlinkOnMs) cfg.BlinkOffMs = s.getSettingInt("blink_off_ms", cfg.BlinkOffMs) cfg.ColorClock = s.GetSetting("color_clock", cfg.ColorClock) cfg.ColorAlarm = s.GetSetting("color_alarm", cfg.ColorAlarm) return cfg } func (s *Store) SaveSettings(cfg Settings) error { pairs := map[string]string{ "snooze_minutes": fmt.Sprintf("%d", cfg.SnoozeMinutes), "timeout_minutes": fmt.Sprintf("%d", cfg.TimeoutMinutes), "default_sound": cfg.DefaultSound, "blink_on_ms": fmt.Sprintf("%d", cfg.BlinkOnMs), "blink_off_ms": fmt.Sprintf("%d", cfg.BlinkOffMs), "color_clock": cfg.ColorClock, "color_alarm": cfg.ColorAlarm, } for k, v := range pairs { if err := s.SetSetting(k, v); err != nil { return err } } return nil } func (s *Store) getSettingInt(key string, defaultVal int) int { val := s.GetSetting(key, "") if val == "" { return defaultVal } var n int if _, err := fmt.Sscanf(val, "%d", &n); err != nil { return defaultVal } return n } func (s *Store) GetSetting(key, defaultVal string) string { var val string err := s.db.QueryRow(`SELECT value FROM settings WHERE key = ?`, key).Scan(&val) if err != nil { return defaultVal } return val } func (s *Store) SetSetting(key, value string) error { _, err := s.db.Exec(` INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value `, key, value) return err }