From 588c679c6a0a822b64da4cccfc6a648554b3a011 Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 07:23:09 +0100 Subject: [PATCH] feat(tasks): repeating tasks (daily, weekly, monthly, yearly) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recurrence on tasks, set in the detail dialog; 🔁 on rows and Kanban cards - Completing a repeating task (detail, list, Kanban drag to done, note checkbox) creates the next one: title, tags, priority, notes, list, assignees and rule copied, subtasks reset, first open column, due date one interval later (month end clamped); pushed live as TASK_CREATED - Each task spawns at most once (re-open + complete makes no duplicate) - Export/import keep the rule (Obsidian Tasks "🔁 every week", JSON field) Co-Authored-By: Claude Opus 5.5 --- Doku/api/rest.md | 2 +- Doku/modules/tasks.md | 9 ++ frontend/src/features/help/HelpView.tsx | 3 + .../src/features/triage/TaskDetailModal.tsx | 31 ++++- frontend/src/features/triage/TasksView.tsx | 23 +++- frontend/src/lib/api.ts | 1 + frontend/src/lib/types.ts | 11 ++ internal/api/server.go | 12 ++ internal/domain/models.go | 3 +- internal/storage/docsync.go | 2 +- internal/storage/export_markdown.go | 6 + internal/storage/import_markdown.go | 17 ++- internal/storage/import_markdown_test.go | 4 +- internal/storage/recurrence.go | 129 ++++++++++++++++++ internal/storage/recurrence_test.go | 97 +++++++++++++ internal/storage/sqlite.go | 45 ++++-- 16 files changed, 376 insertions(+), 19 deletions(-) create mode 100644 internal/storage/recurrence.go create mode 100644 internal/storage/recurrence_test.go diff --git a/Doku/api/rest.md b/Doku/api/rest.md index 992321f..74fab3b 100644 --- a/Doku/api/rest.md +++ b/Doku/api/rest.md @@ -71,7 +71,7 @@ Führt eine Volltextsuche über SQLite FTS5 mit BM25-Relevanz-Ranking aus. |---|---|---| | POST | `/inbox` | Schnellerfassung. Body: `content`, `priority`, `source`, `dueDate`, `target` (`inbox`/`task`/`list_item`/`document`), `targetListId`, `taskListId` (snake_case-Varianten werden akzeptiert) | | GET | `/thoughts?status=` | Gedanken (`inbox`, `kept`, `converted`, `discarded`, `all`) | -| GET/PATCH/DELETE | `/thoughts/{id}` | Einzelabruf; Teiländerung (`content`, `isCompleted`, `dueDate`, `priority`, `taskListId`, `kanbanStatus`, `notes`, `subtasks`, `tags`, `assignees`); Löschen. `assignees` ersetzt die Zuweisungen; nur Personen mit Zugriff auf die Liste, sonst `403` | +| GET/PATCH/DELETE | `/thoughts/{id}` | Einzelabruf; Teiländerung (`content`, `isCompleted`, `dueDate`, `priority`, `taskListId`, `kanbanStatus`, `notes`, `subtasks`, `tags`, `assignees`, `recurrence` = `daily`/`weekly`/`monthly`/`yearly`/leer); Löschen. `assignees` ersetzt die Zuweisungen; nur Personen mit Zugriff auf die Liste, sonst `403` | | DELETE | `/thoughts/discarded/purge` | Papierkorb leeren | | GET | `/triage/deck` | Posteingang (max. 100) | | PATCH | `/triage/{id}` | `action`: `keep`/`discard`/`restore`; mit `convertTo`: `task`/`document`/`list_item` | diff --git a/Doku/modules/tasks.md b/Doku/modules/tasks.md index c39914c..bcbbc15 100644 --- a/Doku/modules/tasks.md +++ b/Doku/modules/tasks.md @@ -27,6 +27,15 @@ Eine Aufgabe ist ein `thought` ([database.md](../database.md)) mit `kanbanStatus - Wer sehen und zuweisen kann, hängt außerdem von der Sichtbarkeit ab: Verliert jemand sie (z. B. Wechsel der Organisation), fallen Freigabe und Zuweisungen weg ([sharing-and-permissions.md](../sharing-and-permissions.md)). - Bekannte Lücke: [FLAW-03](../flaws.md) (Umsortieren per ID). +## Wiederkehrende Aufgaben + +- Im Detail-Fenster unter der Fälligkeit: *Keine Wiederholung*, täglich, wöchentlich, monatlich oder jährlich (`thoughts.recurrence`, `PATCH /thoughts/{id}` mit `recurrence`). Zeilen und Kanban-Karten zeigen dann ein 🔁-Symbol. +- **Abhaken erzeugt die nächste Aufgabe** ([recurrence.go](../../internal/storage/recurrence.go)): egal ob im Detail, in der Liste, per Ziehen in „Erledigt“ im Kanban oder über die Checkbox in einer Notiz. Die erledigte Aufgabe bleibt erledigt (Historie); die neue übernimmt Titel, Tags, Priorität, Notizen, Liste, Zuweisungen und die Regel, Unteraufgaben kommen unerledigt mit, sie steht in der ersten offenen Spalte. +- **Fälligkeit:** das bisherige Datum plus ein Intervall (ohne Datum ab heute). Monate und Jahre behalten den Tag oder rutschen auf das Monatsende (31.1. → 28./29.2.). +- Jede Aufgabe erzeugt höchstens eine Nachfolgerin (`recurrence_spawned`): Wieder öffnen und erneut abhaken legt keine zweite an. +- Die neue Aufgabe geht per `TASK_CREATED` an alle Beteiligten, sie erscheint ohne Neuladen. +- Export/Import: im Markdown-Archiv als `🔁 every week` (Format des Obsidian-Tasks-Plugins), im JSON-Backup als Feld; beides wird beim Import wiederhergestellt. + ## Alarme Der Ticker läuft jede Minute ([architecture.md](../architecture.md)). Für offene Aufgaben mit `due_date <= heute` und `alarm_sent = 0` sendet er an die Kanäle des Aufgaben-**Eigentümers**: diff --git a/frontend/src/features/help/HelpView.tsx b/frontend/src/features/help/HelpView.tsx index 2ea0e3f..7061b91 100644 --- a/frontend/src/features/help/HelpView.tsx +++ b/frontend/src/features/help/HelpView.tsx @@ -558,6 +558,9 @@ export const HelpView: React.FC = ({ onBack }) => {
  • Zuweisen: In geteilten Aufgabenlisten weist du eine Aufgabe einer oder mehreren Personen mit Zugriff zu (Namen tippen, Enter). Die Zugewiesenen erscheinen als kleine Profilbilder an der Aufgabe; wer den Zugriff auf die Liste verliert, wird automatisch entfernt.
  • +
  • + Wiederkehrende Aufgaben: Im Detail-Fenster eine Wiederholung wählen (täglich bis jährlich). Beim Abhaken entsteht automatisch die nächste Aufgabe mit fortgeschriebenem Datum; erkennbar am 🔁-Symbol. +
  • Automatische Alarme: Ist ein Fälligkeitsdatum erreicht, kann MindSift dich automatisch per E-Mail, Discord-Webhook oder individuellem HTTP-Webhook benachrichtigen. Konfigurierbar in deinen Benutzereinstellungen.
  • diff --git a/frontend/src/features/triage/TaskDetailModal.tsx b/frontend/src/features/triage/TaskDetailModal.tsx index c514e1a..8cd00ea 100644 --- a/frontend/src/features/triage/TaskDetailModal.tsx +++ b/frontend/src/features/triage/TaskDetailModal.tsx @@ -17,8 +17,9 @@ import { Webhook, Lock, UserPlus, + Repeat, } from 'lucide-react'; -import { Thought, SubTask, TaskList, TaskListMember, KanbanColumn, User } from '../../lib/types'; +import { Thought, SubTask, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types'; import { UserAvatar } from '../../components/UserAvatar'; import { UserAutocomplete } from '../UserAutocomplete'; import { triggerHaptic } from '../../lib/api'; @@ -34,6 +35,7 @@ interface TaskDetailModalProps { onUpdateStatus: (task: Thought, newStatus: string) => Promise; onUpdatePriority: (task: Thought, newPriority: string) => Promise; onUpdateDueDate: (task: Thought, newDueDate: string) => Promise; + onUpdateRecurrence?: (task: Thought, recurrence: Recurrence) => Promise; onUpdateTaskList: (task: Thought, newListId: string) => Promise; onUpdateNotes: (task: Thought, newNotes: string) => Promise; onUpdateTags?: (task: Thought, newTags: string[]) => Promise; @@ -58,6 +60,7 @@ export const TaskDetailModal: React.FC = ({ onUpdateStatus, onUpdatePriority, onUpdateDueDate, + onUpdateRecurrence, onUpdateTaskList, onUpdateNotes, onUpdateTags, @@ -509,6 +512,32 @@ export const TaskDetailModal: React.FC = ({ +1 Woche + {onUpdateRecurrence && ( +
    + + + {task.recurrence && ( +

    + Beim Erledigen entsteht automatisch die nächste Aufgabe, fällig{' '} + {task.dueDate ? 'ein Intervall nach diesem Datum' : 'ein Intervall nach dem Erledigen'}. +

    + )} +
    + )} {/* Task List assignment */} diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index 786ec43..519c041 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -25,8 +25,9 @@ import { Folder, Filter, Webhook, + Repeat, } from 'lucide-react'; -import { TaskList, Thought, SubTask, Team, User, TaskListShare, TaskListMember, KanbanColumn, PriorityLevel } from '../../lib/types'; +import { TaskList, Thought, SubTask, Team, User, TaskListShare, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { TaskDetailModal } from './TaskDetailModal'; @@ -504,6 +505,15 @@ export const TasksView: React.FC = ({ await api.updateThought(task.id, { notes: newNotes }); }; + const handleUpdateTaskRecurrence = async (task: Thought, recurrence: Recurrence) => { + setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, recurrence } : t))); + try { + await api.updateThought(task.id, { recurrence }); + } catch { + setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, recurrence: task.recurrence } : t))); + } + }; + const handleUpdateTaskAssignees = async (task: Thought, assignees: string[]) => { triggerHaptic(10); setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, assignees } : t))); @@ -1306,6 +1316,11 @@ export const TasksView: React.FC = ({ {task.dueDate} )} + {task.recurrence && ( + + + + )} {/* Hashtag badges */} {task.tags?.map((tag) => ( @@ -2131,6 +2146,7 @@ export const TasksView: React.FC = ({ onUpdateStatus={handleSetKanbanStatus} onUpdatePriority={handleSetPriority} onUpdateDueDate={handleUpdateTaskDueDate} + onUpdateRecurrence={handleUpdateTaskRecurrence} onUpdateTaskList={handleUpdateTaskTaskList} onUpdateNotes={handleUpdateTaskNotes} onUpdateTags={handleUpdateTaskTags} @@ -2324,6 +2340,11 @@ export const TasksView: React.FC = ({ {task.dueDate} )} + {task.recurrence && ( + + + + )} {task.tags?.map((tag) => (