Make birthdays calendar color configurable and expose it via CalDAV

- Add a per-user birthday_calendars table (color, cascade-deletes with
  the user) and GetBirthdayCalendarColor/SetBirthdayCalendarColor in
  internal/db, plus a reservedCalendarNames guard ("birthdays") in
  CreateCalendarWithColor so no real calendar can collide with the
  synthetic one, whether created via the web UI, nidusctl, or CalDAV
  MKCALENDAR.
- Add a "Birthdays" virtual resource card to the dashboard (color
  picker only, no delete/share controls) backed by a new
  ResourceCard.Virtual flag and POST /web/resources/birthdays/color
  handler.
- Extract the birthday-parsing/generation logic shared by the web
  calendar view and CalDAV into internal/birthdays (ParseBirthday,
  Collect, OccurrenceDate, Summary) instead of duplicating it.
- Expose the Birthdays calendar over real CalDAV in
  internal/caldav/backend.go + birthdays.go: it's always listed for
  every user, generates one VEVENT per (contact, year) for a rolling
  window (current year -2..+8) with "🎂 Name (Age)" titles, is
  read-only (Put/Delete/DeleteCalendar all return 403), and its
  Apple/DAVx5 calendar-color is injected from the same per-user
  setting used by the dashboard/web view.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-20 20:09:59 +02:00
co-authored by Copilot
parent cd96b365d0
commit b451f1a76e
12 changed files with 753 additions and 353 deletions
+8 -2
View File
@@ -209,8 +209,14 @@ func TestPropFindEmitsCalendarColor(t *testing.T) {
// Find personal's response block boundaries loosely by looking for the
// nearest calendar-color occurrence and ensuring it isn't right next to
// the personal href (colors are per-block, checked via count instead).
if strings.Count(body, "<calendar-color ") != 1 {
t.Fatalf("expected exactly one calendar-color element (only for work), got: %s", body)
// Two calendar-color elements are expected: one for "work" (explicit
// color) and one for the always-present synthetic "birthdays" calendar
// (default color, since it wasn't explicitly set here).
if strings.Count(body, "<calendar-color ") != 2 {
t.Fatalf("expected exactly two calendar-color elements (work + birthdays default), got: %s", body)
}
if !strings.Contains(body, `<calendar-color xmlns="http://apple.com/ns/ical/">`+defaultBirthdayColor+`FF</calendar-color>`) {
t.Fatalf("expected default birthdays calendar-color, got: %s", body)
}
}