Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion internal/models/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions internal/models/timer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
50 changes: 48 additions & 2 deletions internal/timer/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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]
Expand Down
42 changes: 41 additions & 1 deletion web/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@
<label class="block text-dark-muted text-sm font-bold mb-2" for="webhookTimeout">Webhook Timeout (sec)</label>
<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">
</div>
<div class="mb-4">
<label class="inline-flex relative items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" id="sleepTimeEnabled">
<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>
<span class="ml-3 text-dark-muted text-sm font-bold">Enable Sleep Time</span>
</label>
</div>
<div id="sleep-time-params" class="hidden mb-4 flex gap-2">
<div class="w-1/2">
<label class="block text-dark-muted text-sm font-bold mb-2" for="sleepTimeStart">Start (HH:MM)</label>
<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">
</div>
<div class="w-1/2">
<label class="block text-dark-muted text-sm font-bold mb-2" for="sleepTimeEnd">End (HH:MM)</label>
<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">
</div>
</div>
<div class="flex justify-end pt-2">
<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>
<button type="submit" class="px-4 bg-blue-600 p-3 rounded-lg text-white hover:bg-blue-500">Save</button>
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand All @@ -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,
Expand All @@ -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';
Expand Down
Loading