package web import ( "context" "fmt" "net/http" "github.com/yourusername/caldav-server/internal/db" "github.com/yourusername/caldav-server/internal/web/templates" ) func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { username := userFromContext(r.Context()) resources, err := s.resourceCards(username) if err != nil { s.logger.Error("listing resources", "error", err) http.Error(w, "internal error", http.StatusInternalServerError) return } var sharedWithMe []templates.SharedWithMeItem calShares, err := s.dbase.CalendarsSharedWith(username) if err != nil { s.logger.Warn("listing calendars shared with user", "error", err) } for _, sh := range calShares { sharedWithMe = append(sharedWithMe, templates.SharedWithMeItem{ Kind: "calendar", Owner: sh.Owner, Name: sh.CalendarName, Permission: string(sh.Permission), }) } bookShares, err := s.dbase.AddressBooksSharedWith(username) if err != nil { s.logger.Warn("listing address books shared with user", "error", err) } for _, sh := range bookShares { sharedWithMe = append(sharedWithMe, templates.SharedWithMeItem{ Kind: "addressbook", Owner: sh.Owner, Name: sh.AddressBookName, Permission: string(sh.Permission), }) } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = templates.Dashboard(username, resources, sharedWithMe).Render(context.Background(), w) } // resourceCards builds the full list of ResourceCards (calendars, then // address books) owned by username, each with its current shares — used // both for the initial dashboard render and to re-render the whole // #resources list after a create/delete (since the set of cards changes, // unlike a share update which only changes one card's contents). func (s *Server) resourceCards(username string) ([]templates.ResourceCard, error) { resources := []templates.ResourceCard{ {Kind: "calendar", Name: "Birthdays", Color: s.birthdayCalendarColor(username), Virtual: true}, } calNames, err := s.dbase.ListCalendars(username) if err != nil { return nil, fmt.Errorf("listing calendars: %w", err) } for _, cal := range calNames { card := templates.ResourceCard{Kind: "calendar", Name: cal.Name, Color: cal.Color} shares, err := s.dbase.SharesOfCalendar(username, cal.Name) if err != nil { s.logger.Warn("listing calendar shares", "error", err) } for _, sh := range shares { card.Shares = append(card.Shares, templates.ShareRow{ ResourceName: cal.Name, SharedWith: sh.SharedWith, Permission: string(sh.Permission), }) } resources = append(resources, card) } bookNames, err := s.dbase.ListAddressBooks(username) if err != nil { return nil, fmt.Errorf("listing address books: %w", err) } for _, bookName := range bookNames { card := templates.ResourceCard{Kind: "addressbook", Name: bookName} shares, err := s.dbase.SharesOfAddressBook(username, bookName) if err != nil { s.logger.Warn("listing address book shares", "error", err) } for _, sh := range shares { card.Shares = append(card.Shares, templates.ShareRow{ ResourceName: bookName, SharedWith: sh.SharedWith, Permission: string(sh.Permission), }) } resources = append(resources, card) } return resources, nil } // resourceCardFor rebuilds a single ResourceCard (used to re-render just // the card an htmx request just changed, for partial updates). func (s *Server) resourceCardFor(username, kind, name string) (templates.ResourceCard, error) { card := templates.ResourceCard{Kind: kind, Name: name} var shares []templates.ShareRow if kind == "calendar" { if color, err := s.dbase.GetCalendarColor(username, name); err == nil { card.Color = color } rows, err := s.dbase.SharesOfCalendar(username, name) if err != nil { return card, err } for _, sh := range rows { shares = append(shares, templates.ShareRow{ResourceName: name, SharedWith: sh.SharedWith, Permission: string(sh.Permission)}) } } else { rows, err := s.dbase.SharesOfAddressBook(username, name) if err != nil { return card, err } for _, sh := range rows { shares = append(shares, templates.ShareRow{ResourceName: name, SharedWith: sh.SharedWith, Permission: string(sh.Permission)}) } } card.Shares = shares return card, nil } func isValidPermission(p string) (db.Permission, bool) { switch db.Permission(p) { case db.PermRead, db.PermWrite: return db.Permission(p), true } return "", false }