Polish Calendar views and shared error pages
7 files changed,
+50
−14
internal/calendar/app.go+6 −1internal/calendar/handlers.go+4 −1internal/calendar/handlers_events.go+1 −0internal/calendar/render.go+36 −7internal/calendar/templates/errors.html+1 −1internal/calendar/templates/event.html+1 −3internal/calendar/templates/layout.html+1 −1
CI failed — run details
CI log
=== vet ===
go vet ./...
=== fmt ===
=== test ===
go test ./...
? amadan.net/oficina/calendar/cmd/calendar [no test files]
--- FAIL: TestHomeRouterRetainsExistingCalendarIdentity (0.02s)
home_router_test.go:148: redirect status 307:
FAIL
FAIL amadan.net/oficina/calendar/internal/calendar 4.538s
? amadan.net/oficina/calendar/internal/calendar/icons [no test files]
? amadan.net/oficina/calendar/internal/calendar/seal [no test files]
--- FAIL: TestABookedEventsFormLocksItsTimeButSavesItsTitle (0.02s)
booking_settings_test.go:150: reschedule: changed=false err=calendar: slot taken
2026/09/24 14:33:11 INFO migrate applied="[sessions/0001_init auth/0001_init auth/0002_sessions_backfill calendar/0001_create_people calendar/0002_alter_people calendar/0003_alter_reminders calendar/0004_create_attendees calendar/0005_alter_people calendar/0006_alter_reminder_queues]" skipped=0 adopted=false
2026/09/24 14:33:11 ERROR calendar: scheduled work registration failed stage=schedule.register err="calendar: this platform does not support safe scheduled work; update the platform before starting Calendar"
FAIL
FAIL amadan.net/oficina/calendar/internal/calendartest 2.196s
--- FAIL: TestNoFileExceedsTheLineCap (0.00s)
guards_test.go:91: internal/calendar/render.go is 306 lines, cap is 300 — split it
--- FAIL: TestOurCSSDoesNotClaimAnotherPrefix (0.00s)
guards_test.go:226: calendar.css defines a selector for another namespace: "[rst-" — this app's prefix is cal-
FAIL
FAIL amadan.net/oficina/calendar/internal/guardstest 0.044s
FAIL
make: *** [Makefile:85: test] Error 1
amadan: step test failed: exit status 2
amadan: job failed at step test — its output is above, not at the tail of this log
diff --git a/internal/calendar/app.go b/internal/calendar/app.go| index 9a5d607..b006a91 100644 |
| --- a/internal/calendar/app.go |
| +++ b/internal/calendar/app.go |
| @@ -154,7 +154,7 @@ func New(cfg Config) (*App, error) { |
| mailRecorder: recorder, |
| auth: authPlugin, |
| } |
| - h := &handlers{store: store, logger: logger, origin: origin, mailer: mailer, gate: newGate()} |
| + h := &handlers{store: store, logger: logger, origin: origin, mailer: mailer, gate: newGate(), viewerFrom: authPlugin.SessionFrom} |
| a.h = h |
| if homeSettings != nil { |
| a.home, err = a.newHome(homeSettings, list) |
| @@ -163,6 +163,11 @@ func New(cfg Config) (*App, error) { |
| return nil, err |
| } |
| h.homeEnabled = true |
| + h.directory = homeSettings.Directory |
| + h.viewerFrom = func(r *http.Request) (rauth.Identity, bool) { |
| + p, err := a.homePerson(r) |
| + return rauth.Identity{Address: p.Address}, err == nil |
| + } |
| } |
| // Native wiring comes before the scheduler starts, so a native |
| // failure leaves no timer goroutine behind to leak. |
diff --git a/internal/calendar/handlers.go b/internal/calendar/handlers.go| index 8ff2dd6..c08fd34 100644 |
| --- a/internal/calendar/handlers.go |
| +++ b/internal/calendar/handlers.go |
| @@ -5,6 +5,7 @@ import ( |
| "net/http" |
| "strings" |
| + rauth "github.com/carlosframework/rastrillo/auth" |
| "github.com/carlosframework/rastrillo/mail" |
| ) |
| @@ -12,6 +13,7 @@ import ( |
| type handlers struct { |
| scheduled *scheduledWork |
| homeEnabled bool |
| + directory string |
| store *Store |
| logger *slog.Logger |
| @@ -26,7 +28,8 @@ type handlers struct { |
| // gate is the per-IP token bucket every public booking route |
| // passes through. One per app; memory only. |
| - gate *gate |
| + gate *gate |
| + viewerFrom func(*http.Request) (rauth.Identity, bool) |
| } |
| // secure reports whether cookies this app sets should carry Secure. It |
diff --git a/internal/calendar/handlers_events.go b/internal/calendar/handlers_events.go| index a924d5b..4699e90 100644 |
| --- a/internal/calendar/handlers_events.go |
| +++ b/internal/calendar/handlers_events.go |
| @@ -151,6 +151,7 @@ func (h *handlers) showEvent(w http.ResponseWriter, r *http.Request) { |
| "Attendees": attendees, |
| "Zone": p.Zone, |
| "RuleText": DescribeRule(e.RRule), |
| + "When": spanWords(e.StartUTC, e.EndUTC, p.Zone), |
| } |
| render(w, "event", h.page(r, e.Title, data)) |
| } |
diff --git a/internal/calendar/render.go b/internal/calendar/render.go| index 75895b1..ba2b566 100644 |
| --- a/internal/calendar/render.go |
| +++ b/internal/calendar/render.go |
| @@ -14,6 +14,8 @@ import ( |
| "amadan.net/oficina/estilo.git/chrome" |
| estilomenu "amadan.net/oficina/estilo.git/menu" |
| "github.com/carlosframework/rastrillo" |
| + rauth "github.com/carlosframework/rastrillo/auth" |
| + "github.com/carlosframework/rastrillo/sessions" |
| "github.com/carlosframework/rastrillo/ui" |
| "amadan.net/oficina/calendar/internal/calendar/icons" |
| @@ -177,18 +179,25 @@ func (h *handlers) page(r *http.Request, title string, data any) PageData { |
| QuickPane: emptyPane(), |
| Data: data, |
| } |
| - if id, ok := viewer(r); ok { |
| + if id, ok := h.viewer(r); ok { |
| pd.SignedIn = true |
| pd.Address = id.Address |
| } |
| - pd.Chrome = calendarChrome(r, pd.SignedIn, pd.Address, mode, title) |
| + pd.Chrome = calendarChrome(r, pd.SignedIn, pd.Address, mode, title, h.directory) |
| if r.URL.Path == "/signin" || r.URL.Path == "/signin/local" || r.URL.Path == "/signed-out" { |
| pd.Chrome.Account, pd.Chrome.SignIn = nil, nil |
| } |
| return pd |
| } |
| -func calendarChrome(r *http.Request, signedIn bool, address string, mode chrome.Mode, title string) chrome.Bar { |
| +func (h *handlers) viewer(r *http.Request) (rauth.Identity, bool) { |
| + if h.viewerFrom != nil { |
| + return h.viewerFrom(r) |
| + } |
| + return viewer(r) |
| +} |
| + |
| +func calendarChrome(r *http.Request, signedIn bool, address string, mode chrome.Mode, title, directory string) chrome.Bar { |
| current := func(prefix string) bool { |
| return r.URL.Path == prefix || (prefix != "/" && len(r.URL.Path) > len(prefix) && r.URL.Path[:len(prefix)] == prefix) |
| } |
| @@ -207,6 +216,9 @@ func calendarChrome(r *http.Request, signedIn bool, address string, mode chrome. |
| bar.SignIn = &chrome.Link{Href: "/signin?return_to=" + template.URLQueryEscaper(r.URL.RequestURI()), Label: "Sign in"} |
| return bar |
| } |
| + if directory != "" { |
| + bar.Switcher = &chrome.Switcher{Label: "Oficina apps", AllApps: estilomenu.Item{Href: directory, Label: "All apps"}} |
| + } |
| bar.Account = &chrome.Account{ |
| TriggerLabel: "Account: " + address, Initials: chrome.Initials("", address), Address: address, |
| Labels: chrome.AccountLabels{SignedInAs: "Signed in as", Appearance: "Appearance", Help: "Help"}, |
| @@ -243,17 +255,34 @@ func render(w http.ResponseWriter, name string, data any) { |
| // would. |
| func ErrorPage(w http.ResponseWriter, r *http.Request, status int, ref string) { |
| mode := chrome.Appearance(r) |
| + errPage := errorData(status, ref) |
| + signed, address := false, "" |
| + if id, ok := viewer(r); ok { |
| + signed, address = true, id.Address |
| + } else if _, ok := sessions.Current(r); ok { |
| + signed = true |
| + } |
| w.WriteHeader(status) |
| render(w, "errors", PageData{ |
| - Title: "Something went wrong", |
| + Title: errPage.Title, |
| ThemeAttr: chrome.ThemeAttr(mode), |
| Mode: string(mode), |
| Path: r.URL.Path, |
| - Chrome: calendarChrome(r, false, "", mode, "Something went wrong"), |
| - Data: map[string]any{"Status": status, "Ref": ref}, |
| + SignedIn: signed, |
| + Address: address, |
| + Chrome: calendarChrome(r, signed, address, mode, errPage.Title, ""), |
| + Data: errPage, |
| }) |
| } |
| +func errorData(status int, ref string) chrome.ErrorPage { |
| + title, body := "Something went wrong", "Something went wrong on our side. Try again in a moment." |
| + if status == http.StatusNotFound { |
| + title, body = "Page not found", "The link may be out of date, or the page may have moved." |
| + } |
| + return chrome.ErrorPage{Status: status, Title: title, Body: body, Home: chrome.Link{Href: "/", Label: "Start page"}, Ref: ref, RefLabel: "Reference:"} |
| +} |
| + |
| // notFound has an HTML page only for navigation. API/feed clients retain the |
| // ordinary compact 404, which is both the useful representation and prevents |
| // an HTML error page from being mistaken for a successful feed response. |
| @@ -263,7 +292,7 @@ func (h *handlers) notFound(w http.ResponseWriter, r *http.Request) { |
| return |
| } |
| w.WriteHeader(http.StatusNotFound) |
| - render(w, "errors", h.page(r, "Page not found", map[string]any{"Status": http.StatusNotFound})) |
| + render(w, "errors", h.page(r, "Page not found", errorData(http.StatusNotFound, ""))) |
| } |
| func containsHTML(accept string) bool { |
diff --git a/internal/calendar/templates/errors.html b/internal/calendar/templates/errors.html| index 7fa0354..b83354b 100644 |
| --- a/internal/calendar/templates/errors.html |
| +++ b/internal/calendar/templates/errors.html |
| @@ -7,4 +7,4 @@ |
| override the partial's own catalog defaults; see |
| ui/partials/error-page.html for what it accepts. */}} |
| {{define "title"}}{{.Title}}{{end}} |
| -{{define "content"}}{{template "error-page" dict "Status" .Data.Status "Ref" .Data.Ref}}{{end}} |
| +{{define "content"}}{{template "estilo-error" .Data}}{{end}} |
diff --git a/internal/calendar/templates/event.html b/internal/calendar/templates/event.html| index 00bb875..cfdde13 100644 |
| --- a/internal/calendar/templates/event.html |
| +++ b/internal/calendar/templates/event.html |
| @@ -26,9 +26,7 @@ |
| {{end}} |
| {{$items := list |
| - (dict "Label" "When" "Value" .Data.Chip.TimeLabel) |
| - (dict "Label" "Starts" "Value" .Data.Event.StartLocal) |
| - (dict "Label" "Ends" "Value" .Data.Event.EndLocal)}} |
| + (dict "Label" "When" "Value" .Data.When)}} |
| {{template "detail-list" dict "Items" $items}} |
| {{/* The authored zone is a whisper, never the headline: the time above |
diff --git a/internal/calendar/templates/layout.html b/internal/calendar/templates/layout.html| index f30d90b..9dac19d 100644 |
| --- a/internal/calendar/templates/layout.html |
| +++ b/internal/calendar/templates/layout.html |
| @@ -1,5 +1,5 @@ |
| {{define "layout"}}<!doctype html> |
| -<html lang="{{block "lang" .}}en{{end}}" dir="{{block "dir" .}}ltr{{end}}"{{if .ThemeAttr}} data-theme="{{.ThemeAttr}}"{{end}}> |
| +<html lang="{{block "lang" .}}en-GB{{end}}" dir="{{block "dir" .}}ltr{{end}}"{{if .ThemeAttr}} data-theme="{{.ThemeAttr}}"{{end}}> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |