Files
woke/db/db.go
2026-02-02 19:38:02 +02:00

275 lines
6.7 KiB
Go

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
}