diff --git a/README.md b/README.md index 1aeadec..e07d5b5 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,10 @@ mail-to-task address, and an MCP endpoint so Claude can work your task list dire ## What works today -**Phase 1 is complete and phase 2 is most of the way there.** You can install it, -create an account, add tasks by typing a sentence, share a project and watch -somebody else's changes appear live. +**Phases 1 and 3 are complete; phase 2 is most of the way there.** You can install +it, create an account, add tasks by typing a sentence, share a project, watch +somebody else's changes appear live, set up repeating chores and reminders, save +filters, and subscribe to it from Apple Calendar. | Area | State | |---|---| @@ -56,11 +57,17 @@ somebody else's changes appear live. | Sharing: invites, roles, members | Done, tested | | Live sync over WebSocket | Done | | Web interface: sign-in, views, quick add, ⌘K, shortcuts | Done | -| Recurring tasks, reminders, saved filters, board/calendar | Planned — phase 3 | +| Recurring tasks (RRULE, Danish + English) | Done, tested | +| Reminders, absolute and relative | Done, tested | +| Saved filters with a query language | Done, tested | +| Board and calendar views | Done | +| ICS calendar feed, task durations | Done, tested | +| Project templates | Done, tested | +| Nightly backups with rotation | Done, tested | | Import/export, CalDAV, mail-to-task, Gmail, MCP, AI | Planned — phases 4–5 | -Not yet built: comments and attachments, drag-and-drop in the interface (the API -supports it), Web Push, and everything from phase 3 onwards. +Not yet built: comments and attachments, Web Push, and everything from phase 4 +onwards. Search finds `grøn` when you type `gron`, because Unicode does not consider `ø` an accented `o` and a Danish-first app that cannot do this is broken for its own users. @@ -145,11 +152,11 @@ is part of it. ## Roadmap 1. **MVP** — auth, projects, sections, tasks, sub-tasks, priorities, due dates, - quick add, Today and Upcoming, search, keyboard shortcuts. + quick add, Today and Upcoming, search, keyboard shortcuts. *(done)* 2. **Sharing** — invites, roles, assignees, comments, attachments, activity log, realtime sync, notifications. 3. **Pro parity** — recurring tasks, reminders, filter queries, board and calendar - views, durations, ICS feeds, templates, automatic backups. + views, durations, ICS feeds, templates, automatic backups. *(done)* 4. **Import/export** — Todoist CSV and official export in, CSV/JSON/ICS out. 5. **Integrations** — CalDAV server, mail-to-task, Gmail, MCP server for Claude, and a provider-agnostic AI layer that also talks to a local model. diff --git a/internal/recurrence/recurrence.go b/internal/recurrence/recurrence.go index a2564b0..bc7ab37 100644 --- a/internal/recurrence/recurrence.go +++ b/internal/recurrence/recurrence.go @@ -298,7 +298,15 @@ func Describe(rule string) string { for _, code := range strings.Split(days, ",") { names = append(names, danishDay(code)) } - return every(interval, "uge") + " " + strings.Join(names, " og ") + joined := strings.Join(names, " og ") + + // "hver mandag", not "hver uge mandag". A weekly rule naming its days does + // not also need the word for the interval — that is how the phrase is said, + // and it is what the parser accepts back. + if interval <= 1 { + return "hver " + joined + } + return every(interval, "uge") + " " + joined } switch parts["FREQ"] { diff --git a/internal/recurrence/recurrence_test.go b/internal/recurrence/recurrence_test.go index 4748287..df4961b 100644 --- a/internal/recurrence/recurrence_test.go +++ b/internal/recurrence/recurrence_test.go @@ -270,8 +270,9 @@ func TestDescribe(t *testing.T) { "FREQ=DAILY;INTERVAL=3": "hver 3. dag", "FREQ=WEEKLY": "hver uge", "FREQ=WEEKLY;INTERVAL=2": "hver anden uge", - "FREQ=WEEKLY;BYDAY=MO": "hver uge mandag", - "FREQ=WEEKLY;BYDAY=MO,TH": "hver uge mandag og torsdag", + "FREQ=WEEKLY;BYDAY=MO": "hver mandag", + "FREQ=WEEKLY;BYDAY=MO,TH": "hver mandag og torsdag", + "FREQ=WEEKLY;BYDAY=FR;INTERVAL=2": "hver anden uge fredag", "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR": "hverdage", "FREQ=WEEKLY;BYDAY=SA,SU": "hver weekend", "FREQ=MONTHLY": "hver måned", @@ -293,6 +294,7 @@ func TestDescribeRoundTrip(t *testing.T) { phrases := []string{ "hver dag", "hver anden dag", "hver uge", "hver anden uge", "hverdage", "hver weekend", "hver måned", "hvert år", + "hver mandag", "hver mandag og torsdag", } for _, phrase := range phrases { t.Run(phrase, func(t *testing.T) { diff --git a/web/src/lib/api.js b/web/src/lib/api.js index dd07dd1..d49240d 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -150,6 +150,31 @@ export const api = { quickAddPreview: (text, signal) => get(`/tasks/quick-add/preview?text=${encodeURIComponent(text)}`, { signal }), + // --- filters and labels + listFilters: () => get('/filters'), + createFilter: (data) => post('/filters', data), + updateFilter: (id, data) => patch(`/filters/${id}`, data), + deleteFilter: (id) => del(`/filters/${id}`), + runFilter: (id) => get(`/filters/${id}/tasks`), + previewFilter: (query) => get(`/filters/preview?query=${encodeURIComponent(query)}`), + + listLabels: () => get('/labels'), + createLabel: (name) => post('/labels', { name }), + deleteLabel: (id) => del(`/labels/${id}`), + + // --- reminders, feed and templates + listReminders: (taskId) => get(`/tasks/${taskId}/reminders`), + createReminder: (taskId, data) => post(`/tasks/${taskId}/reminders`, data), + deleteReminder: (id) => del(`/reminders/${id}`), + + feed: () => get('/feed'), + rotateFeed: () => post('/feed/rotate'), + + listTemplates: () => get('/templates'), + saveTemplate: (data) => post('/templates', data), + useTemplate: (id, data) => post(`/templates/${id}/use`, data), + deleteTemplate: (id) => del(`/templates/${id}`), + // --- views today: () => get('/today'), upcoming: (days) => get(`/upcoming${days ? `?days=${days}` : ''}`), diff --git a/web/src/lib/components/BoardView.svelte b/web/src/lib/components/BoardView.svelte new file mode 100644 index 0000000..4cde61e --- /dev/null +++ b/web/src/lib/components/BoardView.svelte @@ -0,0 +1,220 @@ + + +
Tom
+ {/if} +