feat(tasks): repeating tasks (daily, weekly, monthly, yearly)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m28s

- 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 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 07:23:09 +01:00
1 parent 0e9fd1b3ad
commit 588c679c6a
16 files changed
+376 -19

No files matched your search

+13 -4
View File
@@ -60,9 +60,9 @@ type importDoc struct {
}
type importTask struct {
content, priority, due, notes string
done bool
subtasks []domain.SubTask
content, priority, due, notes, recurrence string
done bool
subtasks []domain.SubTask
}
type importTaskList struct {
@@ -393,7 +393,7 @@ func PlanMindSiftJSONImport(data []byte) (*ImportPlan, error) {
plan.TaskLists = append(plan.TaskLists, importTaskList{title: tl.Title, icon: tl.Icon})
}
for _, t := range exp.Thoughts {
task := importTask{content: t.Content, priority: t.Priority, due: t.DueDate, notes: t.Notes, done: t.IsCompleted, subtasks: t.Subtasks}
task := importTask{content: t.Content, priority: t.Priority, due: t.DueDate, notes: t.Notes, done: t.IsCompleted, subtasks: t.Subtasks, recurrence: t.Recurrence}
switch {
case t.Status == domain.StatusInbox:
plan.Inbox = append(plan.Inbox, task)
@@ -506,6 +506,10 @@ func (s *Store) importTaskInto(userID, listID string, t importTask, asTask bool)
if _, err := s.UpdateThoughtTriage(userID, th.ID, "keep", "task", "", t.due, listID); err != nil {
return fmt.Errorf("Aufgabe %q: %w", content, err)
}
if validRecurrence(t.recurrence) && t.recurrence != "" {
// Imported as already handled: completing a restored task must not create a second successor
_, _ = s.db.Exec(`UPDATE thoughts SET recurrence = ?, recurrence_spawned = ? WHERE id = ?`, t.recurrence, boolInt(t.done), th.ID)
}
if t.done || len(t.subtasks) > 0 {
var done *bool
if t.done {
@@ -754,6 +758,7 @@ func MarkdownToBlocks(md string) []domain.DocumentBlock {
}
var (
recurRe = regexp.MustCompile(`(?i)[ \t]*🔁[ \t]*every[ \t]+(day|week|month|year)\b`)
dueRe = regexp.MustCompile(`[ \t]*📅[ \t]*(\d{4}-\d{2}-\d{2})`)
priorityRe = regexp.MustCompile(`[ \t]*(🔺|⏫|🔼|🔽|⏬)`)
)
@@ -790,6 +795,10 @@ func parseTaskFile(body string) []importTask {
if c := checkboxRe.FindStringSubmatch(m[2]); c != nil {
t.done, t.content = c[1] != " ", c[2]
}
if r := recurRe.FindStringSubmatch(t.content); r != nil {
t.recurrence = map[string]string{"day": "daily", "week": "weekly", "month": "monthly", "year": "yearly"}[strings.ToLower(r[1])]
t.content = recurRe.ReplaceAllString(t.content, "")
}
if d := dueRe.FindStringSubmatch(t.content); d != nil {
t.due = d[1]
t.content = dueRe.ReplaceAllString(t.content, "")