Skip to content

Commit 697e422

Browse files
committed
feat: add optional sleep time feature to webhooktimer
Add optional sleep time feature - a time period during which webhooks won't be triggered even if timer expires. Changes: - Add SleepTimeStart and SleepTimeEnd fields to TimerEntry model (HH:MM format) - Update SQLite database schema with new columns - Add isSleepTime() helper to check if current time falls within sleep window - Handle sleep times that span midnight (e.g., 23:00-06:00) - Use Europe/Berlin timezone for time calculations - Update all SQL queries (Create, Update, Toggle, StartAll) - Add UI in timer modal with checkbox toggle and time inputs - Update JavaScript to handle sleep time in form submission and editing
1 parent f534249 commit 697e422

5 files changed

Lines changed: 100 additions & 10 deletions

File tree

internal/handlers/handlers.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ func (h *Handler) CreateTimer(w http.ResponseWriter, r *http.Request) {
6767
t.Type = "other"
6868
}
6969

70-
_, err := models.DB.Exec("INSERT INTO timers (id, name, webhook_url, mode, fixed_interval, min_interval, max_interval, active, webhook_timeout, method, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
71-
t.ID, t.Name, t.WebhookURL, t.Mode, t.FixedInterval, t.MinInterval, t.MaxInterval, t.Active, t.WebhookTimeout, t.Method, t.Type)
70+
_, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
71+
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)
7272
if err != nil {
7373
http.Error(w, err.Error(), http.StatusInternalServerError)
7474
return
@@ -88,8 +88,8 @@ func (h *Handler) UpdateTimer(w http.ResponseWriter, r *http.Request) {
8888
}
8989
t.ID = id
9090

91-
_, err := models.DB.Exec("UPDATE timers SET name = ?, webhook_url = ?, mode = ?, fixed_interval = ?, min_interval = ?, max_interval = ?, active = ?, webhook_timeout = ?, method = ?, type = ? WHERE id = ?",
92-
t.Name, t.WebhookURL, t.Mode, t.FixedInterval, t.MinInterval, t.MaxInterval, t.Active, t.WebhookTimeout, t.Method, t.Type, id)
91+
_, 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 = ?",
92+
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)
9393
if err != nil {
9494
http.Error(w, err.Error(), http.StatusInternalServerError)
9595
return
@@ -135,8 +135,8 @@ func (h *Handler) ToggleTimer(w http.ResponseWriter, r *http.Request) {
135135

136136
// Fetch updated timer
137137
var t models.TimerEntry
138-
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)
139-
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)
138+
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)
139+
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)
140140
if err == nil {
141141
h.Manager.UpdateTimer(&t)
142142
}

internal/models/db.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ func InitDB(dataSourceName string) error {
2727
last_execution DATETIME,
2828
webhook_timeout INTEGER DEFAULT 5,
2929
method TEXT DEFAULT 'POST',
30-
type TEXT DEFAULT 'other'
30+
type TEXT DEFAULT 'other',
31+
sleep_time_start TEXT,
32+
sleep_time_end TEXT
3133
);
3234
CREATE TABLE IF NOT EXISTS logs (
3335
id INTEGER PRIMARY KEY AUTOINCREMENT,

internal/models/timer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type TimerEntry struct {
1818
LastExecution time.Time `json:"lastExecution"`
1919
WebhookTimeout int `json:"webhookTimeout"`
2020
NextExecution time.Time `json:"nextExecution"` // Only in RAM
21+
SleepTimeStart string `json:"sleepTimeStart"` // HH:MM format, 24-hour
22+
SleepTimeEnd string `json:"sleepTimeEnd"` // HH:MM format, 24-hour
2123
}
2224

2325
type LogEntry struct {

internal/timer/manager.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func NewManager(db *sql.DB) *Manager {
3131
}
3232

3333
func (m *Manager) StartAll() error {
34-
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")
34+
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")
3535
if err != nil {
3636
return err
3737
}
@@ -40,7 +40,7 @@ func (m *Manager) StartAll() error {
4040
for rows.Next() {
4141
var t models.TimerEntry
4242
var lastExec sql.NullTime
43-
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)
43+
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)
4444
if err != nil {
4545
return err
4646
}
@@ -135,6 +135,10 @@ func (m *Manager) runTimer(ctx context.Context, id string) {
135135
case <-ctx.Done():
136136
return
137137
case <-time.After(interval):
138+
if m.isSleepTime(t) {
139+
log.Printf("Skipping webhook for %s: within sleep time window", t.Name)
140+
continue
141+
}
138142
m.executeWebhook(t)
139143
if m.OnUpdate != nil {
140144
m.OnUpdate(id)
@@ -159,6 +163,48 @@ func (m *Manager) calculateInterval(t *models.TimerEntry) time.Duration {
159163
return time.Duration(min+n.Int64()) * time.Second
160164
}
161165

166+
func (m *Manager) isSleepTime(t *models.TimerEntry) bool {
167+
if t.SleepTimeStart == "" || t.SleepTimeEnd == "" {
168+
return false
169+
}
170+
171+
loc, err := time.LoadLocation("Europe/Berlin")
172+
if err != nil {
173+
loc = time.Local
174+
}
175+
now := time.Now().In(loc)
176+
177+
startH, startM, err := parseTime(t.SleepTimeStart)
178+
if err != nil {
179+
return false
180+
}
181+
endH, endM, err := parseTime(t.SleepTimeEnd)
182+
if err != nil {
183+
return false
184+
}
185+
186+
currentMinutes := now.Hour()*60 + now.Minute()
187+
startMinutes := startH*60 + startM
188+
endMinutes := endH*60 + endM
189+
190+
// Handle sleep time that spans midnight (e.g., 23:00-06:00)
191+
if startMinutes <= endMinutes {
192+
// Normal case: 00:00-12:00
193+
return currentMinutes >= startMinutes && currentMinutes < endMinutes
194+
}
195+
// Spans midnight: 23:00-06:00
196+
return currentMinutes >= startMinutes || currentMinutes < endMinutes
197+
}
198+
199+
func parseTime(hhmm string) (int, int, error) {
200+
var h, m int
201+
_, err := fmt.Sscanf(hhmm, "%d:%d", &h, &m)
202+
if err != nil {
203+
return 0, 0, err
204+
}
205+
return h, m, nil
206+
}
207+
162208
func (m *Manager) CallNow(id string) {
163209
m.mu.RLock()
164210
t, ok := m.timers[id]

web/templates/index.html

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,23 @@
126126
<label class="block text-dark-muted text-sm font-bold mb-2" for="webhookTimeout">Webhook Timeout (sec)</label>
127127
<input class="bg-dark-bg border border-dark-border rounded w-full py-2 px-3 text-dark-text leading-tight focus:outline-none focus:border-blue-500" id="webhookTimeout" type="number" value="5" min="1" max="60">
128128
</div>
129+
<div class="mb-4">
130+
<label class="inline-flex relative items-center cursor-pointer">
131+
<input type="checkbox" class="sr-only peer" id="sleepTimeEnabled">
132+
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
133+
<span class="ml-3 text-dark-muted text-sm font-bold">Enable Sleep Time</span>
134+
</label>
135+
</div>
136+
<div id="sleep-time-params" class="hidden mb-4 flex gap-2">
137+
<div class="w-1/2">
138+
<label class="block text-dark-muted text-sm font-bold mb-2" for="sleepTimeStart">Start (HH:MM)</label>
139+
<input class="bg-dark-bg border border-dark-border rounded w-full py-2 px-3 text-dark-text leading-tight focus:outline-none focus:border-blue-500" id="sleepTimeStart" type="time" value="23:00">
140+
</div>
141+
<div class="w-1/2">
142+
<label class="block text-dark-muted text-sm font-bold mb-2" for="sleepTimeEnd">End (HH:MM)</label>
143+
<input class="bg-dark-bg border border-dark-border rounded w-full py-2 px-3 text-dark-text leading-tight focus:outline-none focus:border-blue-500" id="sleepTimeEnd" type="time" value="06:00">
144+
</div>
145+
</div>
129146
<div class="flex justify-end pt-2">
130147
<button type="button" class="px-4 bg-transparent p-3 rounded-lg text-blue-500 hover:bg-dark-bg hover:text-blue-400 mr-2 modal-close">Cancel</button>
131148
<button type="submit" class="px-4 bg-blue-600 p-3 rounded-lg text-white hover:bg-blue-500">Save</button>
@@ -270,6 +287,17 @@
270287
document.getElementById('maxInterval').value = secondsToTime(timer.maxInterval);
271288
document.getElementById('webhookTimeout').value = timer.webhookTimeout;
272289

290+
// Sleep time
291+
const sleepEnabled = timer.sleepTimeStart && timer.sleepTimeEnd;
292+
document.getElementById('sleepTimeEnabled').checked = sleepEnabled;
293+
if (sleepEnabled) {
294+
document.getElementById('sleep-time-params').classList.remove('hidden');
295+
document.getElementById('sleepTimeStart').value = timer.sleepTimeStart;
296+
document.getElementById('sleepTimeEnd').value = timer.sleepTimeEnd;
297+
} else {
298+
document.getElementById('sleep-time-params').classList.add('hidden');
299+
}
300+
273301
toggleModeParams(timer.mode);
274302
toggleTypeParams(timer.type || 'other');
275303
document.getElementById('modal-title').innerText = 'Edit Timer';
@@ -300,11 +328,20 @@
300328
document.getElementById('modal-title').innerText = 'New Timer';
301329
toggleModeParams('fixed');
302330
toggleTypeParams('other');
331+
document.getElementById('sleepTimeEnabled').checked = false;
332+
document.getElementById('sleep-time-params').classList.add('hidden');
303333
toggleModal();
304334
};
305335

306336
document.getElementById('mode').onchange = (e) => toggleModeParams(e.target.value);
307337
document.getElementById('type').onchange = (e) => toggleTypeParams(e.target.value);
338+
document.getElementById('sleepTimeEnabled').onchange = (e) => {
339+
if (e.target.checked) {
340+
document.getElementById('sleep-time-params').classList.remove('hidden');
341+
} else {
342+
document.getElementById('sleep-time-params').classList.add('hidden');
343+
}
344+
};
308345

309346
function toggleModeParams(mode) {
310347
if (mode === 'fixed') {
@@ -327,6 +364,7 @@
327364
document.getElementById('timer-form').onsubmit = (e) => {
328365
e.preventDefault();
329366
const id = document.getElementById('timer-id').value;
367+
const sleepTimeEnabled = document.getElementById('sleepTimeEnabled').checked;
330368
const data = {
331369
name: document.getElementById('name').value,
332370
webhookURL: document.getElementById('webhookURL').value,
@@ -337,7 +375,9 @@
337375
minInterval: timeToSeconds(document.getElementById('minInterval').value || '00:00:00'),
338376
maxInterval: timeToSeconds(document.getElementById('maxInterval').value || '00:00:00'),
339377
webhookTimeout: parseInt(document.getElementById('webhookTimeout').value),
340-
active: true
378+
active: true,
379+
sleepTimeStart: sleepTimeEnabled ? document.getElementById('sleepTimeStart').value : '',
380+
sleepTimeEnd: sleepTimeEnabled ? document.getElementById('sleepTimeEnd').value : ''
341381
};
342382

343383
const method = id ? 'PUT' : 'POST';

0 commit comments

Comments
 (0)