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:
@@ -69,6 +69,37 @@ func (s *Server) handleCalendarColor(w http.ResponseWriter, r *http.Request) {
|
||||
_ = templates.ResourceCardView(card).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
// handleBirthdayColor updates the current user's display color for the
|
||||
// synthetic "Birthdays" calendar, mounted at /resources/birthdays/color.
|
||||
// It re-renders just that card (not the whole list), since the set of
|
||||
// cards doesn't change.
|
||||
func (s *Server) handleBirthdayColor(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", "POST")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
username := userFromContext(r.Context())
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "invalid form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
color := strings.TrimSpace(r.PostForm.Get("color"))
|
||||
if color != "" && !hexColorRe.MatchString(color) {
|
||||
http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := s.dbase.SetBirthdayCalendarColor(username, color); err != nil {
|
||||
s.logger.Error("setting birthday calendar color", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
card := templates.ResourceCard{Kind: "calendar", Name: "Birthdays", Color: s.birthdayCalendarColor(username), Virtual: true}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.ResourceCardView(card).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (s *Server) handleResource(w http.ResponseWriter, r *http.Request, kind string) {
|
||||
username := userFromContext(r.Context())
|
||||
|
||||
@@ -109,6 +140,10 @@ func (s *Server) handleResource(w http.ResponseWriter, r *http.Request, kind str
|
||||
http.Error(w, "already exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if err == db.ErrReservedName {
|
||||
http.Error(w, "this name is reserved for a computed calendar", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
s.logger.Error("creating resource", "kind", kind, "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user