Keep Calendar chrome consistent on errors
5 files changed,
+50
−45
internal/calendar/chrome_calendar.go+42 −0internal/calendar/home_router_test.go+5 −1internal/calendar/render.go+0 −41internal/calendar/static/calendar.css+1 −1internal/calendar/templates/layout.html+2 −2
CI failed — run details
CI log
=== vet ===
go vet ./...
go: downloading amadan.net/oficina/home/connect v0.0.0-20260923190447-01166fcfc0a9
go: downloading amadan.net/oficina/estilo.git v0.0.0-20260924071126-894afdcb71a9
go: downloading amadan.net/rastrillo/native/server v0.0.0-20260923173546-2a5d07b2c8d3
=== fmt ===
=== test ===
go test ./...
? amadan.net/oficina/calendar/cmd/calendar [no test files]
ok amadan.net/oficina/calendar/internal/calendar 4.503s
? 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:36:18 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:36:18 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.112s
ok amadan.net/oficina/calendar/internal/guardstest 0.041s
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/chrome_calendar.go b/internal/calendar/chrome_calendar.go| new file mode 100644 |
| index 0000000..ee5571e |
| --- /dev/null |
| +++ b/internal/calendar/chrome_calendar.go |
| @@ -0,0 +1,42 @@ |
| +package calendar |
| + |
| +import ( |
| + "html/template" |
| + "net/http" |
| + |
| + "amadan.net/oficina/estilo.git/chrome" |
| + estilomenu "amadan.net/oficina/estilo.git/menu" |
| + rauth "github.com/carlosframework/rastrillo/auth" |
| +) |
| + |
| +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) |
| + } |
| + bar := chrome.Bar{ |
| + App: chrome.App{Name: "Calendar", Slug: "calendar", Home: "/", Icon: assets.Path("static/brand/icon.svg")}, Sticky: true, |
| + Labels: chrome.BarLabels{Skip: "Skip to content", Menu: "Menu"}, NavLabel: "Calendar", |
| + Nav: []chrome.Link{{Href: "/today", Label: "Today", Current: current("/today") || current("/day/") || current("/week/") || current("/month/")}, {Href: "/agenda", Label: "Agenda", Current: current("/agenda")}, {Href: "/calendars", Label: "Calendars", Current: current("/calendars")}}, |
| + Actions: []chrome.Action{{Item: estilomenu.Item{Href: "/events/new", Label: "New event"}, Primary: true}}, |
| + } |
| + if !signedIn { |
| + bar.Nav, bar.Actions = nil, nil |
| + 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"}, |
| + Items: []estilomenu.Item{{Href: "/settings", Label: "Settings"}, {Href: "/settings/booking", Label: "Booking pages"}, {Href: "/import", Label: "Import a calendar"}}, |
| + Appearance: &chrome.AppearanceMenu{Action: "/appearance", Return: r.URL.RequestURI(), Options: chrome.AppearanceOptions(mode, "Light", "Dark", "System")}, Help: []estilomenu.Item{{Href: "/help/open-source", Label: "Open source"}}, SignOut: chrome.SignOut{Action: "/signout", Label: "Sign out"}} |
| + return bar |
| +} |
diff --git a/internal/calendar/home_router_test.go b/internal/calendar/home_router_test.go| index a18ab48..94db8f7 100644 |
| --- a/internal/calendar/home_router_test.go |
| +++ b/internal/calendar/home_router_test.go |
| @@ -145,7 +145,11 @@ func TestHomeRouterRetainsExistingCalendarIdentity(t *testing.T) { |
| t.Fatal("proof replaced session") |
| } |
| homeRedirect(t, f.do("POST", "/account/home/confirm", "")) |
| - homeRedirect(t, f.do("POST", "/signout", "")) |
| + // connect now preserves this POST across the app → id.* sign-out hop; |
| + // a 303 would turn it into a GET and leave the directory session alive. |
| + if out := f.do("POST", "/signout", ""); out.Code != http.StatusTemporaryRedirect || out.Header().Get("Location") != homeTestIssuer+"/signout" { |
| + t.Fatalf("sign-out = %d %q, want 307 to issuer", out.Code, out.Header().Get("Location")) |
| + } |
| start = homeRedirect(t, f.do("GET", "/signin", "")) |
| target := homeRedirect(t, f.exchange(t, start, "opaque-home-alice")) |
| if target != "/" { |
diff --git a/internal/calendar/render.go b/internal/calendar/render.go| index ba2b566..7152289 100644 |
| --- a/internal/calendar/render.go |
| +++ b/internal/calendar/render.go |
| @@ -14,7 +14,6 @@ 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" |
| @@ -190,46 +189,6 @@ func (h *handlers) page(r *http.Request, title string, data any) PageData { |
| return pd |
| } |
| -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) |
| - } |
| - bar := chrome.Bar{ |
| - App: chrome.App{Name: "Calendar", Slug: "calendar", Home: "/", Icon: assets.Path("static/brand/icon.svg")}, |
| - Sticky: true, Labels: chrome.BarLabels{Skip: "Skip to content", Menu: "Menu"}, |
| - NavLabel: "Calendar", Nav: []chrome.Link{ |
| - {Href: "/today", Label: "Today", Current: current("/today") || current("/day/") || current("/week/") || current("/month/")}, |
| - {Href: "/agenda", Label: "Agenda", Current: current("/agenda")}, |
| - {Href: "/calendars", Label: "Calendars", Current: current("/calendars")}, |
| - }, |
| - Actions: []chrome.Action{{Item: estilomenu.Item{Href: "/events/new", Label: "New event"}, Primary: true}}, |
| - } |
| - if !signedIn { |
| - bar.Nav, bar.Actions = nil, nil |
| - 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"}, |
| - Items: []estilomenu.Item{{Href: "/settings", Label: "Settings"}, {Href: "/settings/booking", Label: "Booking pages"}, {Href: "/import", Label: "Import a calendar"}}, |
| - Appearance: &chrome.AppearanceMenu{Action: "/appearance", Return: r.URL.RequestURI(), Options: chrome.AppearanceOptions(mode, "Light", "Dark", "System")}, |
| - Help: []estilomenu.Item{{Href: "/help/open-source", Label: "Open source"}}, |
| - SignOut: chrome.SignOut{Action: "/signout", Label: "Sign out"}, |
| - } |
| - return bar |
| -} |
| - |
| // render executes a page into a buffer before anything touches the |
| // wire: a template error becomes a clean 500 instead of garbage |
| // appended to a half-written page, and headers stay settable until the |
diff --git a/internal/calendar/static/calendar.css b/internal/calendar/static/calendar.css| index b4e934a..bc39c8d 100644 |
| --- a/internal/calendar/static/calendar.css |
| +++ b/internal/calendar/static/calendar.css |
| @@ -25,7 +25,7 @@ |
| margin-block-end: var(--rst-sp-4); |
| } |
| -[cal-bar] + [rst-seg-tabs] { margin-block: 0 var(--rst-sp-4); } |
| +[cal-view-tabs] { margin-block: 0 var(--rst-sp-4); } |
| [cal-bar-spacer] { flex: 1 1 auto; } |
diff --git a/internal/calendar/templates/layout.html b/internal/calendar/templates/layout.html| index 9dac19d..e8eb1eb 100644 |
| --- a/internal/calendar/templates/layout.html |
| +++ b/internal/calendar/templates/layout.html |
| @@ -34,9 +34,9 @@ |
| </body> |
| </html>{{end}} |
| -{{define "calendar-view-tabs"}}<nav rst-seg-tabs aria-label="Calendar view"> |
| +{{define "calendar-view-tabs"}}<div cal-view-tabs><nav rst-seg-tabs aria-label="Calendar view"> |
| <a href="{{.Day}}"{{if eq .View "day"}} aria-current="page"{{end}}>Day</a> |
| <a href="{{.Week}}"{{if eq .View "week"}} aria-current="page"{{end}}>Week</a> |
| <a href="{{.Month}}"{{if eq .View "month"}} aria-current="page"{{end}}>Month</a> |
| <a href="{{.Agenda}}"{{if eq .View "agenda"}} aria-current="page"{{end}}>Agenda</a> |
| -</nav>{{end}} |
| +</nav></div>{{end}} |