From 1b35bb3de4aff29055b065ef2e023adef261f52d Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 31 Oct 2026 10:22:36 +0100 Subject: [PATCH] fix(alarms): Kanban moves no longer re-arm due alarms of open tasks Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +++++- README.md | 2 +- frontend/package-lock.json | 4 +-- frontend/package.json | 2 +- internal/storage/recurrence_test.go | 38 +++++++++++++++++++++++++++++ internal/storage/thoughts.go | 7 ++++-- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7fd6e..6de2695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.17.3] - 2026-10-31 + +### Fixed +- Fälligkeits-Benachrichtigungen kamen nach jedem Verschieben im Kanban-Board erneut: Beim Verschieben wurde für alle offenen Aufgaben der betroffenen Spalten vermerkt, dass ihr Alarm noch aussteht. Jetzt bekommt nur eine Aufgabe, die von „Erledigt“ wieder geöffnet wird, ihren Alarm erneut. + ## [0.17.2] - 2026-10-31 ### Changed @@ -413,7 +418,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.2...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.3...dev +[0.17.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.2...v0.17.3 [0.17.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.1...v0.17.2 [0.17.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.0...v0.17.1 [0.17.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.1...v0.17.0 diff --git a/README.md b/README.md index 03da5d2..5f37d0e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.17.2-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.17.3-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d0a77e5..742075d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.17.2", + "version": "0.17.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.17.2", + "version": "0.17.3", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index d20c8e0..b37f1b3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.17.2", + "version": "0.17.3", "license": "Unlicense", "type": "module", "scripts": { diff --git a/internal/storage/recurrence_test.go b/internal/storage/recurrence_test.go index ca091eb..9926256 100644 --- a/internal/storage/recurrence_test.go +++ b/internal/storage/recurrence_test.go @@ -139,6 +139,44 @@ func TestAlarmResetsOnNewDueDateOrReopen(t *testing.T) { } } +// A Kanban move sends every task of the touched columns: only a task that goes from done back to open +// may get its due alarm again, the others must not be notified a second time +func TestKanbanMoveKeepsSentAlarms(t *testing.T) { + store, err := NewStore(filepath.Join(t.TempDir(), "kanban-alarm.db")) + if err != nil { + t.Fatalf("NewStore: %v", err) + } + defer store.Close() + u, _ := store.CreateUser("anna@example.com", "hash", "Anna", "member") + tl, _ := store.CreateTaskList(u.ID, "", "", "Haushalt", "🏠", "") + task, _ := store.CreateThoughtWithDetails(u.ID, "Heizung entlüften", "normal", "manual", "2026-10-31 09:00", tl.ID, "todo", "") + _, _ = store.UpdateThoughtTriage(u.ID, task.ID, "keep", "task", "", "", tl.ID) + sent := func() bool { + th, _ := store.GetThoughtByID(u.ID, task.ID) + return th.AlarmSent + } + move := func(status string) { + if err := store.ReorderTasks(tl.ID, []domain.ThoughtReorderItem{{ID: task.ID, SortOrder: 0, KanbanStatus: &status}}); err != nil { + t.Fatalf("ReorderTasks: %v", err) + } + } + + _ = store.MarkTaskAlarmSent(task.ID) + move("in_progress") + move("todo") + if !sent() { + t.Errorf("moving an open task between columns must not re-arm its alarm") + } + move("done") + if !sent() { + t.Errorf("completing keeps the flag") + } + move("todo") + if sent() { + t.Errorf("reopening via the board must re-arm the alarm") + } +} + func TestAlarmGivesUpAfterThreeFailedRuns(t *testing.T) { store, err := NewStore(filepath.Join(t.TempDir(), "alarmfail.db")) if err != nil { diff --git a/internal/storage/thoughts.go b/internal/storage/thoughts.go index 184cb22..2ec3b39 100644 --- a/internal/storage/thoughts.go +++ b/internal/storage/thoughts.go @@ -243,10 +243,13 @@ func (s *Store) ReorderTasks(taskListID string, items []domain.ThoughtReorderIte completed = append(completed, it.ID) } } + // Only a reopened task (done -> open) gets its due alarm again. A Kanban move sends every task + // of the touched columns, so resetting all open ones re-sent "due today" for each of them. + // (is_completed in the CASE is still the value before this UPDATE.) _, err = tx.Exec( `UPDATE thoughts SET sort_order = ?, kanban_status = ?, is_completed = ?, - alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END, - alarm_failures = CASE WHEN ? = 0 THEN 0 ELSE alarm_failures END WHERE id = ?`, + alarm_sent = CASE WHEN ? = 0 AND is_completed = 1 THEN 0 ELSE alarm_sent END, + alarm_failures = CASE WHEN ? = 0 AND is_completed = 1 THEN 0 ELSE alarm_failures END WHERE id = ?`, it.SortOrder, *it.KanbanStatus, isCompleted, isCompleted, isCompleted, it.ID, ) } else {