m's ask: per-item CalDAV linking should support existing lists, not
just create-new. Athena's design update extended it: also tag VTODOs
on create so multiple projax items can SHARE one CalDAV list, with
projax doing tag-based slicing on read.
Three layers, one branch:
## 1. Link-existing picker (the original ask)
- New POST /i/{path}/caldav/link-existing handler validates the
submitted calendar_url is in the discoverable PROPFIND set (defence
against crafted forms pointing at arbitrary HTTP servers), then
inserts the item_link row with display_name + color metadata
preserved from the discovery payload.
- handleDetail + renderTasksSection pre-load
availableCalendarsForItem(ctx, links) — calendars from
s.CalDAV.Client.ListCalendars MINUS the ones already linked to this
item. Errors degrade to an empty picker (non-fatal).
- tasks_section.tmpl gains a .caldav-actions block rendering the
picker (<select> of available calendars) when AvailableCalendars
is non-empty AND the Create-new button (when the item has no
linked list yet). Same surface serves both the "first link" flow
and the "+ link another" flow per athena's brief.
## 2. Tag-on-create (CATEGORIES carries projax:<path>)
- caldav package gains Categories []string on Todo + the same on
VTodoEdit. BuildVTodoICS emits a CATEGORIES line when non-empty;
parseVTodos parses CATEGORIES comma-list into the slice with per-
entry unescape per RFC 5545.
- handleCalDAVTodoAction action="todo-create" passes
`Categories: []{ProjaxCategoryFor(it.PrimaryPath())}` into
VTodoEdit so every per-item Add submits a tagged VTODO.
- ApplyVTodoEdit intentionally ignores the Categories field —
edit/complete/delete paths preserve existing CATEGORIES via the
unknown-property pass-through that's been tested since Phase 5
(TestApplyVTodoEditPreservesUnknown).
## 3. Per-item filter (managed-vs-legacy)
- detailTodos now calls caldav.AnyTodoHasProjaxTag(todos) to decide
whether the linked list is projax-managed (any projax: tag
anywhere) or legacy/unmanaged (zero projax: tags).
- Managed → filter to VTODOs whose CATEGORIES include this
item's projax:<path>. Multiple projax: tags are AND-of-OR — a
VTODO with two projax tags appears on both items per athena's
multi-tag contract.
- Legacy → show every VTODO untouched. Existing pre-5j users with
untagged lists keep seeing everything; the detail page doesn't
suddenly hide their tasks.
## Helpers (caldav package, exported)
- ProjaxCategoryFor(primaryPath) → "projax:<path>" string
- HasProjaxTag(t) bool → any projax: prefix
- HasProjaxTagFor(t, primaryPath) bool → exact projax:<path>
- AnyTodoHasProjaxTag(todos) bool → list-level signal
## Tests
caldav unit (caldav/projax_tags_test.go):
- TestProjaxCategoryFor / TestHasProjaxTagAndFor /
TestAnyTodoHasProjaxTag / TestBuildVTodoICSEmitsCategories /
TestParseVTodosMultiCategory.
web integration (web/caldav_link_existing_test.go) — single fake
CalDAV server (httptest) answering PROPFIND + REPORT + PUT, then
four end-to-end probes:
- TestDetailLinkExistingCalendar — three calendars discoverable,
picker renders, POST link-existing creates the link, second GET
drops the linked URL from the picker.
- TestVTodoCreateAttachesProjaxCategory — Add-task POST writes a
VTODO whose CATEGORIES contains projax:<path>.
- TestDetailFilterByProjaxCategory — one calendar shared between
Trip A and Trip B with three tagged VTODOs; A sees A+shared,
B sees B+shared, neither sees the other's tagged-only VTODO.
- TestDetailUntaggedListShowsAll — linked list with zero projax
tags renders ALL VTODOs (legacy fallback).
Full web + caldav suites green. Pre-existing
db/TestBackfillTagsFromArea failure unchanged.
Net: +795 / -14.
caldav package:
- Event struct: UID, Summary, Start, End, AllDay, Location, Description,
Recurring, URL — read-only, no writeback
- ListEvents(ctx, calendarURL, ListEventsOpts{TimeMin, TimeMax}) issues
REPORT calendar-query with server-side <c:time-range> filter
- parseVEvents handles DATE vs DATE-TIME (via hasDateOnlyParam since
splitLine strips ;VALUE=DATE), RRULE-present → Recurring=true with NO
expansion (literal DTSTART only)
- 2 unit tests: full parse (DATE-TIME, all-day, recurring), hasDateOnlyParam
web dashboard:
- dashboardEvent / dashboardEventGroup types
- collectEvents fans out 4-worker pool across every caldav-list link,
fixed 7-day window from now, sort start-asc, cap 50, group by day
- dayLabelFor: Today / Tomorrow / weekday-day-month
- Events card on /dashboard between Tasks and Issues, with empty-collapse
- 2 integration tests with stubbed CalDAV: surfaces upcoming + DATE/RRULE
rendering; empty-collapse with no links
design.md §5 (CalDAV) + §Dashboard updated; mgmt-teardown plan's one
blocking gap is now closed.
caldav package:
- Todo carries URL, ETag, Raw so ListTodos rows can be PUT/DELETEd in place
- BuildVTodoICS for new VTODOs, ApplyVTodoEdit for in-place edits that
preserve unknown properties (DESCRIPTION, CATEGORIES, X-*)
- PutTodo/DeleteTodo with If-Match optimistic concurrency
- ErrPreconditionFailed/ErrNotFound for 412/404
- RFC 5545 fold-at-75 + CRLF + text escape, hand-rolled UUID v4
- httptest round-trip (create -> list -> complete -> delete) plus 412 path
web:
- POST /i/{path}/caldav/todo/{complete,reopen,edit,delete,todo-create}
- Re-fetches the live ETag before each PUT/DELETE so ordinary use never
trips 412; on actual 412 the section reloads with a banner
- Calendar URL must already be linked to the item (anti-forgery guard)
- tasks_section partial drives both the initial page render and HTMX
swaps; detail.tmpl reduces to a one-liner template call
docs/design.md §5: rewrite for full read/write semantics + ETag concurrency.
m's CalDAV server (dav.msbls.de, SabreDAV) now feeds projax via a thin
read-only-plus-create-on-demand integration. No background sync; tasks
fetched live on detail-page render.
New caldav/ package
- ListCalendars (PROPFIND Depth: 1, filters non-calendar collections)
- ListTodos (REPORT calendar-query for VTODO; hand-rolled iCalendar
parser for UID/SUMMARY/STATUS/DUE/PRIORITY/LAST-MODIFIED — RFC 5545
line-folding aware)
- CreateCalendar (MKCALENDAR, 405 → ErrCalendarExists for the "link
instead" branch)
- httptest-stubbed tests cover all four paths.
Store
- ItemLink shape + LinksByType / LinksByRefType / AddLink / DeleteLink.
AddLink upserts on (item_id, ref_type, ref_id, rel) so re-linking the
same calendar is idempotent.
Web
- GET /admin/caldav — discovery + auto-suggested matches + manual
linker. Suggestion = lowercased displayname == projax slug or title.
- POST /admin/caldav/link — insert item_links row.
- POST /admin/caldav/unlink — delete by link id.
- POST /i/{path}/caldav/create — MKCALENDAR at <base>/<slug>/, then
AddLink. On 405 (already exists), fall back to link-only.
- Detail page Tasks section: per-calendar block with open VTODOs +
collapsed completed (30d window). Errors per calendar logged and
skipped, so one bad calendar does not blank the page.
- nav adds /admin/caldav link.
main.go
- DAV_URL + DAV_USER + DAV_PASSWORD optional. Missing DAV_URL → CalDAV
off (admin page renders "not configured" notice). DAV_URL set but
user/pass missing → fail fast at boot.
docs/design.md gains §5 documenting the integration shape.
deploy/dokploy.yaml lists the two new secrets + the env var.
Phase 2.b (writeback / two-way / background sync) is parked.