diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 660d0d5..ceba8fe 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -67,8 +67,8 @@ func (h *Handler) CreateTimer(w http.ResponseWriter, r *http.Request) { t.Type = "other" } - _, err := models.DB.Exec("INSERT INTO timers (id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, webhook_timeout, method, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - t.ID, t.Name, t.WebhookURL, t.Mode, t.FixedInterval, t.MinInterval, t.MaxInterval, t.Active, t.WebhookTimeout, t.Method, t.Type) + _, err := models.DB.Exec("INSERT INTO timers (id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, webhook_timeout, method, type, sleep_time_start, sleep_time_end) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + t.ID, t.Name, t.WebhookURL, t.Mode, t.FixedInterval, t.MinInterval, t.MaxInterval, t.Active, t.WebhookTimeout, t.Method, t.Type, t.SleepTimeStart, t.SleepTimeEnd) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -88,8 +88,8 @@ func (h *Handler) UpdateTimer(w http.ResponseWriter, r *http.Request) { } t.ID = id - _, err := models.DB.Exec("UPDATE timers SET name = ?, webhook_url = ?, mode = ?, fixed_interval = ?, min_interval = ?, max_interval = ?, active = ?, webhook_timeout = ?, method = ?, type = ? WHERE id = ?", - t.Name, t.WebhookURL, t.Mode, t.FixedInterval, t.MinInterval, t.MaxInterval, t.Active, t.WebhookTimeout, t.Method, t.Type, id) + _, err := models.DB.Exec("UPDATE timers SET name = ?, webhook_url = ?, mode = ?, fixed_interval = ?, min_interval = ?, max_interval = ?, active = ?, webhook_timeout = ?, method = ?, type = ?, sleep_time_start = ?, sleep_time_end = ? WHERE id = ?", + t.Name, t.WebhookURL, t.Mode, t.FixedInterval, t.MinInterval, t.MaxInterval, t.Active, t.WebhookTimeout, t.Method, t.Type, t.SleepTimeStart, t.SleepTimeEnd, id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -135,8 +135,8 @@ func (h *Handler) ToggleTimer(w http.ResponseWriter, r *http.Request) { // Fetch updated timer var t models.TimerEntry - row := models.DB.QueryRow("SELECT id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, last_execution, webhook_timeout, method, type FROM timers WHERE id = ?", id) - err = row.Scan(&t.ID, &t.Name, &t.WebhookURL, &t.Mode, &t.FixedInterval, &t.MinInterval, &t.MaxInterval, &t.Active, &t.LastExecution, &t.WebhookTimeout, &t.Method, &t.Type) + row := models.DB.QueryRow("SELECT id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, last_execution, webhook_timeout, method, type, sleep_time_start, sleep_time_end FROM timers WHERE id = ?", id) + err = row.Scan(&t.ID, &t.Name, &t.WebhookURL, &t.Mode, &t.FixedInterval, &t.MinInterval, &t.MaxInterval, &t.Active, &t.LastExecution, &t.WebhookTimeout, &t.Method, &t.Type, &t.SleepTimeStart, &t.SleepTimeEnd) if err == nil { h.Manager.UpdateTimer(&t) } diff --git a/internal/models/db.go b/internal/models/db.go index 6d587a4..33c4e1b 100644 --- a/internal/models/db.go +++ b/internal/models/db.go @@ -27,7 +27,9 @@ func InitDB(dataSourceName string) error { last_execution DATETIME, webhook_timeout INTEGER DEFAULT 5, method TEXT DEFAULT 'POST', - type TEXT DEFAULT 'other' + type TEXT DEFAULT 'other', + sleep_time_start TEXT, + sleep_time_end TEXT ); CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/internal/models/timer.go b/internal/models/timer.go index 6a902f6..b3daeba 100644 --- a/internal/models/timer.go +++ b/internal/models/timer.go @@ -18,6 +18,8 @@ type TimerEntry struct { LastExecution time.Time `json:"lastExecution"` WebhookTimeout int `json:"webhookTimeout"` NextExecution time.Time `json:"nextExecution"` // Only in RAM + SleepTimeStart string `json:"sleepTimeStart"` // HH:MM format, 24-hour + SleepTimeEnd string `json:"sleepTimeEnd"` // HH:MM format, 24-hour } type LogEntry struct { diff --git a/internal/timer/manager.go b/internal/timer/manager.go index 9b14c21..d662acf 100644 --- a/internal/timer/manager.go +++ b/internal/timer/manager.go @@ -31,7 +31,7 @@ func NewManager(db *sql.DB) *Manager { } func (m *Manager) StartAll() error { - rows, err := m.db.Query("SELECT id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, last_execution, webhook_timeout, method, type FROM timers") + rows, err := m.db.Query("SELECT id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, last_execution, webhook_timeout, method, type, sleep_time_start, sleep_time_end FROM timers") if err != nil { return err } @@ -40,7 +40,7 @@ func (m *Manager) StartAll() error { for rows.Next() { var t models.TimerEntry var lastExec sql.NullTime - err := rows.Scan(&t.ID, &t.Name, &t.WebhookURL, &t.Mode, &t.FixedInterval, &t.MinInterval, &t.MaxInterval, &t.Active, &lastExec, &t.WebhookTimeout, &t.Method, &t.Type) + err := rows.Scan(&t.ID, &t.Name, &t.WebhookURL, &t.Mode, &t.FixedInterval, &t.MinInterval, &t.MaxInterval, &t.Active, &lastExec, &t.WebhookTimeout, &t.Method, &t.Type, &t.SleepTimeStart, &t.SleepTimeEnd) if err != nil { return err } @@ -135,6 +135,10 @@ func (m *Manager) runTimer(ctx context.Context, id string) { case <-ctx.Done(): return case <-time.After(interval): + if m.isSleepTime(t) { + log.Printf("Skipping webhook for %s: within sleep time window", t.Name) + continue + } m.executeWebhook(t) if m.OnUpdate != nil { m.OnUpdate(id) @@ -159,6 +163,48 @@ func (m *Manager) calculateInterval(t *models.TimerEntry) time.Duration { return time.Duration(min+n.Int64()) * time.Second } +func (m *Manager) isSleepTime(t *models.TimerEntry) bool { + if t.SleepTimeStart == "" || t.SleepTimeEnd == "" { + return false + } + + loc, err := time.LoadLocation("Europe/Berlin") + if err != nil { + loc = time.Local + } + now := time.Now().In(loc) + + startH, startM, err := parseTime(t.SleepTimeStart) + if err != nil { + return false + } + endH, endM, err := parseTime(t.SleepTimeEnd) + if err != nil { + return false + } + + currentMinutes := now.Hour()*60 + now.Minute() + startMinutes := startH*60 + startM + endMinutes := endH*60 + endM + + // Handle sleep time that spans midnight (e.g., 23:00-06:00) + if startMinutes <= endMinutes { + // Normal case: 00:00-12:00 + return currentMinutes >= startMinutes && currentMinutes < endMinutes + } + // Spans midnight: 23:00-06:00 + return currentMinutes >= startMinutes || currentMinutes < endMinutes +} + +func parseTime(hhmm string) (int, int, error) { + var h, m int + _, err := fmt.Sscanf(hhmm, "%d:%d", &h, &m) + if err != nil { + return 0, 0, err + } + return h, m, nil +} + func (m *Manager) CallNow(id string) { m.mu.RLock() t, ok := m.timers[id] diff --git a/web/templates/index.html b/web/templates/index.html index fb672f6..4faccf1 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -126,6 +126,23 @@ +
+ +
+
@@ -270,6 +287,17 @@ document.getElementById('maxInterval').value = secondsToTime(timer.maxInterval); document.getElementById('webhookTimeout').value = timer.webhookTimeout; + // Sleep time + const sleepEnabled = timer.sleepTimeStart && timer.sleepTimeEnd; + document.getElementById('sleepTimeEnabled').checked = sleepEnabled; + if (sleepEnabled) { + document.getElementById('sleep-time-params').classList.remove('hidden'); + document.getElementById('sleepTimeStart').value = timer.sleepTimeStart; + document.getElementById('sleepTimeEnd').value = timer.sleepTimeEnd; + } else { + document.getElementById('sleep-time-params').classList.add('hidden'); + } + toggleModeParams(timer.mode); toggleTypeParams(timer.type || 'other'); document.getElementById('modal-title').innerText = 'Edit Timer'; @@ -300,11 +328,20 @@ document.getElementById('modal-title').innerText = 'New Timer'; toggleModeParams('fixed'); toggleTypeParams('other'); + document.getElementById('sleepTimeEnabled').checked = false; + document.getElementById('sleep-time-params').classList.add('hidden'); toggleModal(); }; document.getElementById('mode').onchange = (e) => toggleModeParams(e.target.value); document.getElementById('type').onchange = (e) => toggleTypeParams(e.target.value); + document.getElementById('sleepTimeEnabled').onchange = (e) => { + if (e.target.checked) { + document.getElementById('sleep-time-params').classList.remove('hidden'); + } else { + document.getElementById('sleep-time-params').classList.add('hidden'); + } + }; function toggleModeParams(mode) { if (mode === 'fixed') { @@ -327,6 +364,7 @@ document.getElementById('timer-form').onsubmit = (e) => { e.preventDefault(); const id = document.getElementById('timer-id').value; + const sleepTimeEnabled = document.getElementById('sleepTimeEnabled').checked; const data = { name: document.getElementById('name').value, webhookURL: document.getElementById('webhookURL').value, @@ -337,7 +375,9 @@ minInterval: timeToSeconds(document.getElementById('minInterval').value || '00:00:00'), maxInterval: timeToSeconds(document.getElementById('maxInterval').value || '00:00:00'), webhookTimeout: parseInt(document.getElementById('webhookTimeout').value), - active: true + active: true, + sleepTimeStart: sleepTimeEnabled ? document.getElementById('sleepTimeStart').value : '', + sleepTimeEnd: sleepTimeEnabled ? document.getElementById('sleepTimeEnd').value : '' }; const method = id ? 'PUT' : 'POST';