feat(tasks): due dates with optional time; alarms at the right moment
Due dates can carry a time (YYYY-MM-DDTHH:MM). Alarms fire at that time,
or 09:00 for all-day tasks, minus the user's lead time, in TZ (default
Europe/Berlin, tzdata embedded). A new due date or reopening a task
re-arms the alarm. Recurrence keeps the time; Markdown export/import
carry it. The UI shows relative labels ("Heute, 14:30"), marks overdue
tasks to the minute and groups by local day instead of the UTC date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
300fd6a7be
commit
344f5e4ab2
21 files changed
+391
-50
No files matched your search
@@ -17,6 +17,8 @@ func TestNextDueDate(t *testing.T) {
|
||||
{"2026-03-15", "monthly", "2026-04-15"},
|
||||
{"2028-02-29", "yearly", "2029-02-28"},
|
||||
{"", "weekly", "2026-11-05"},
|
||||
{"2026-01-31T07:15", "monthly", "2026-02-28T07:15"},
|
||||
{"2026-10-30T23:30", "daily", "2026-10-31T23:30"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := NextDueDate(c.due, c.rule, today); got != c.want {
|
||||
@@ -95,3 +97,44 @@ func TestRepeatingTaskSpawnsItsSuccessorOnce(t *testing.T) {
|
||||
t.Errorf("non-repeating task must not spawn")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlarmResetsOnNewDueDateOrReopen(t *testing.T) {
|
||||
store, err := NewStore(filepath.Join(t.TempDir(), "alarm.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
u, _ := store.CreateUser("anna@example.com", "hash", "Anna", "member")
|
||||
task, _ := store.CreateThoughtWithDetails(u.ID, "Zahnarzt", "normal", "manual", "2026-10-30 14:30", "", "todo", "")
|
||||
_, _ = store.UpdateThoughtTriage(u.ID, task.ID, "keep", "task", "", "", "")
|
||||
sent := func() bool {
|
||||
th, _ := store.GetThoughtByID(u.ID, task.ID)
|
||||
return th.AlarmSent
|
||||
}
|
||||
if th, _ := store.GetThoughtByID(u.ID, task.ID); th.DueDate != "2026-10-30T14:30" {
|
||||
t.Fatalf("due date not normalized: %q", th.DueDate)
|
||||
}
|
||||
|
||||
_ = store.MarkTaskAlarmSent(task.ID)
|
||||
same := "2026-10-30T14:30"
|
||||
_, _ = store.UpdateThoughtDetails(u.ID, task.ID, nil, nil, &same, nil, nil, nil, nil, nil, nil)
|
||||
if !sent() {
|
||||
t.Errorf("unchanged due date must not re-arm the alarm")
|
||||
}
|
||||
later := "2026-10-31T08:00"
|
||||
_, _ = store.UpdateThoughtDetails(u.ID, task.ID, nil, nil, &later, nil, nil, nil, nil, nil, nil)
|
||||
if sent() {
|
||||
t.Errorf("new due date must re-arm the alarm")
|
||||
}
|
||||
|
||||
_ = store.MarkTaskAlarmSent(task.ID)
|
||||
done, open := true, false
|
||||
_, _ = store.UpdateThoughtDetails(u.ID, task.ID, nil, &done, nil, nil, nil, nil, nil, nil, nil)
|
||||
if !sent() {
|
||||
t.Errorf("completing keeps the flag")
|
||||
}
|
||||
_, _ = store.UpdateThoughtDetails(u.ID, task.ID, nil, &open, nil, nil, nil, nil, nil, nil, nil)
|
||||
if sent() {
|
||||
t.Errorf("reopening must re-arm the alarm")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user