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

@@ -399,3 +399,28 @@ table.bulk .chip-add input { padding: 1px 4px; font-size: 0.85em; width: 7em; }
}
.graph-canvas .fit-screen:hover { background: var(--accent); color: #fff; }
.graph-canvas.fit .graph-svg { width: 100%; height: auto; }
/* --- Dashboard Events card (Phase 3l) --- */
.dashboard .card-events .event-day { margin-bottom: 12px; }
.dashboard .card-events .event-day:last-child { margin-bottom: 0; }
.dashboard .card-events .event-day h3 {
font-size: 0.85em; margin: 8px 0 4px; color: var(--muted); font-weight: 500;
}
.dashboard .card-events .event-list { list-style: none; padding: 0; margin: 0; }
.dashboard .card-events .event-row {
display: flex; gap: 8px; align-items: baseline;
padding: 4px 0; border-bottom: 1px dotted var(--border); flex-wrap: wrap;
}
.dashboard .card-events .event-row:last-child { border-bottom: none; }
.dashboard .card-events .event-row .start {
font-family: ui-monospace, SFMono-Regular, monospace; font-size: 0.88em;
color: var(--muted); min-width: 4.5em;
}
.dashboard .card-events .event-row .proj {
font-family: ui-monospace, SFMono-Regular, monospace; font-size: 0.85em; color: var(--muted);
}
.dashboard .card-events .event-row .summary { flex: 1; min-width: 8em; }
.dashboard .card-events .event-row .loc { font-size: 0.85em; }
.dashboard .card-events .event-row .recurring {
font-size: 0.85em; color: var(--accent); cursor: help;
}