feat: add parse endpoint, refactor recurring tasks, and improve web task completion

Extract CreateRecurringTask into engine package for reuse by both CLI
and API. Add POST /tasks/parse endpoint for CLI-style input parsing.
Remove FK constraint on change_log to preserve history after task
deletion. Update web frontend to filter completed tasks from view and
add mock mode support for development.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 22:39:11 +01:00
parent 0352c22b4f
commit 78881e1b07
15 changed files with 2118 additions and 128 deletions
+3 -91
View File
@@ -4,10 +4,8 @@ import (
"fmt"
"os"
"strings"
"time"
"git.jnss.me/joakim/opal/internal/engine"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
@@ -101,99 +99,13 @@ func parseAddArgs(args []string) (string, []string, error) {
}
func addRecurringTask(description string, mod *engine.Modifier) error {
// Extract recurrence pattern
recurPattern := mod.SetAttributes["recur"]
if recurPattern == nil {
return fmt.Errorf("no recurrence pattern specified")
}
// Validate: recurring tasks must have due date
if mod.SetAttributes["due"] == nil {
return fmt.Errorf("recurring tasks require a due date (use due:YYYY-MM-DD or due:monday)")
}
duration, err := engine.ParseRecurrencePattern(*recurPattern)
instance, err := engine.CreateRecurringTask(description, mod)
if err != nil {
return fmt.Errorf("invalid recurrence pattern: %w", err)
return err
}
// Create template task (without saving yet)
now := time.Now()
template := &engine.Task{
UUID: uuid.New(),
Status: engine.StatusRecurring,
Description: description,
Priority: engine.PriorityDefault,
Created: now,
Modified: now,
RecurrenceDuration: &duration,
Tags: []string{},
}
// Create modifier without the recur attribute
tempMod := &engine.Modifier{
SetAttributes: make(map[string]*string),
AttributeOrder: []string{},
AddTags: mod.AddTags,
RemoveTags: mod.RemoveTags,
}
// Copy all attributes except recur
for _, key := range mod.AttributeOrder {
if key != "recur" {
val := mod.SetAttributes[key]
tempMod.SetAttributes[key] = val
tempMod.AttributeOrder = append(tempMod.AttributeOrder, key)
}
}
// Apply modifiers to template before first save
if err := tempMod.ApplyToNew(template); err != nil {
return fmt.Errorf("failed to apply modifiers to template: %w", err)
}
// Save template
if err := template.Save(); err != nil {
return fmt.Errorf("failed to save template: %w", err)
}
// Add tags to template (requires task.ID from save)
for _, tag := range mod.AddTags {
if err := template.AddTag(tag); err != nil {
return fmt.Errorf("failed to add tag to template: %w", err)
}
}
// Create first instance
instance := &engine.Task{
UUID: uuid.New(),
Status: engine.StatusPending,
Description: description,
Priority: template.Priority,
Created: now,
Modified: now,
ParentUUID: &template.UUID,
Due: template.Due,
Wait: template.Wait,
Scheduled: template.Scheduled,
Project: template.Project,
Tags: []string{},
}
if err := instance.Save(); err != nil {
return fmt.Errorf("failed to save first instance: %w", err)
}
// Copy tags to instance
for _, tag := range template.Tags {
if err := instance.AddTag(tag); err != nil {
return fmt.Errorf("failed to add tag to instance: %w", err)
}
}
fmt.Printf("Created recurring task %s\n", template.UUID)
fmt.Printf("Created recurring task %s\n", *instance.ParentUUID)
fmt.Printf("First instance: %s\n", instance.UUID)
fmt.Printf("Recurrence: %s\n", engine.FormatRecurrenceDuration(duration))
return nil
}