feat(phase 3l vevents): VEVENT support on dashboard — closes mgmt-parity gap

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.
This commit is contained in:
mAi
2026-05-16 00:57:52 +02:00
parent 67f2e992e3
commit d49ad219a4
8 changed files with 727 additions and 1 deletions

View File

@@ -252,7 +252,8 @@ m's CalDAV server lives at `dav.msbls.de/dav/calendars/m/` (SabreDAV, Basic auth
- **ICS round-trip**: writes that modify an existing task call `ApplyVTodoEdit` against the server's raw ICS so unknown properties (DESCRIPTION, CATEGORIES, X-extensions, …) survive the round-trip. Only the keys projax knows about (SUMMARY, STATUS, COMPLETED, DUE, PRIORITY, LAST-MODIFIED, DTSTAMP) get rewritten. New tasks go through `BuildVTodoICS` which emits a minimal but valid VCALENDAR wrapper with RFC 5545 folding at 75 octets and CRLF terminators.
- **Multi-parent items** keep ONE list per item — the URL is derived from the slug, not the path. `paliad` gets `/dav/calendars/m/paliad/` whether it lives at `work.paliad`, `dev.paliad`, or both.
- **Authorisation**: writeback handlers reject calendar URLs not currently linked to the item, so a crafted form can't route writes to arbitrary collections.
- **Out of scope (still parked)**: RRULE / recurring VTODOs (rendered as single occurrences until m needs more), background sync, multi-calendar drag-and-drop. Phase 2.c may add a TTL'd `cached_tasks` table if live REPORT-querying gets slow at m's scale.
- **VEVENT reading (Phase 3l)** — read-only event listing parallel to VTODO support, closing the mgmt-parity gap before teardown. `caldav.ListEvents(calendarURL, ListEventsOpts{TimeMin, TimeMax})` issues a REPORT calendar-query with a server-side `<c:time-range>` filter and parses VEVENT blocks into an `Event{UID, Summary, Start, End, AllDay, Location, Description, Recurring, URL}` struct. DATE-only DTSTART values are detected at the raw-line level (the param strip in `splitLine` would otherwise lose `VALUE=DATE`); `hasDateOnlyParam` flips `AllDay=true`. RRULE-bearing events surface with `Recurring=true` and only the literal DTSTART instance — projax does NOT expand RRULE at v1; m clicks through to his calendar app for the recurring picture.
- **Out of scope (still parked)**: RRULE expansion, VEVENT writeback (create/edit/delete events from projax — calendar app handles), iCal export of projax-managed events, recurring VTODOs, background sync, multi-calendar drag-and-drop. Phase 2.c may add a TTL'd `cached_tasks` table if live REPORT-querying gets slow at m's scale.
Env contract: `DAV_URL` (default `https://dav.msbls.de/dav/calendars/m/`), `DAV_USER`, `DAV_PASSWORD`. All three live in Dokploy secrets; missing → `/admin/caldav` renders a "not configured" notice and the detail page hides the Tasks section.
@@ -393,6 +394,10 @@ A single landing surface at `/dashboard` that aggregates open work and recent ac
6. **Force-refresh button**`↻ refresh` link that adds `?refresh=1` to the current URL. The handler invalidates the matching cache key and re-runs the full aggregation. HTMX swaps the section in-place.
7. **Empty-card collapse** — when no filter is active AND a card has zero rows, render a one-line `No open tasks.` / `No open issues.` / `No recent documents.` note instead of the full empty-state block. With a filter active the card chrome stays so m can distinguish "filter hid the data" from "no data".
**Phase 3l addition — Events card (closes the mgmt-parity gap):**
8. **Events** — every VEVENT in the next 7 days across every `caldav-list` item_link, fanned out via the same 4-worker pool. Time-range filter is server-side (RFC 4791 `<c:time-range>`), so the DAV server returns only what the window contains. Grouped by day with German "Today" / "Tomorrow" / weekday labels lifted from the mgmt cockpit's wording. Sort within day: start asc, summary asc as tiebreaker. Cap 50. Each row: start time (or "ganztägig" for all-day DATE events), project path link, summary, location, and a `↻` badge when the source VEVENT has an RRULE (the dashboard never expands recurrences — only the literal DTSTART instance). Empty-collapse: with no filter and zero events, the card renders "No upcoming events." inline.
## Graph view (Phase 3f)
A read-only top-down DAG render of every projax item at `/graph`, server-rendered inline SVG — no client-side layout library, no Excalidraw file. Trade-offs: m gets a single page that prints, downloads, and reflows in a regular browser; no drag-to-rearrange (read-only is enough for the daily glance).