oficina / calendar Public

Adopt estilo shared chrome (round 2) open

On branch feat/r2-shared-chrome

Calendar adopts estilo main 894afdc and home/connect 01166fc on this branch. Heads: 16c5197, 4e008a0, 3d8ea7b. The 52px bar replaces the rastrillo topbar; account menu has Settings, Booking pages, Import, Appearance and the single Sign out; signed-out uses Sign in. Day/Week/Month/Agenda use one 1024px width and share date-preserving seg-tabs; chips are square; working hours scroll into view; unknown HTML routes have styled errors and non-HTML keeps plain 404. Added all-day same-day timed conversion rounding test. Focused Go/browser checks pass. Full make ci is currently red on TestABookedEventsFormLocksItsTimeButSavesItsTitle; reproduced unchanged on origin/main 4d2975d. The scheduler diagnostic also appeared. Shared-check config and screenshots are under polish-audit. No merge requested.

Follow-up complete at 78308b9. The main CI failure was a stale booking-host fixture: after the test changes UTC to Europe/Dublin, it computed candidate slots with the old UTC Person while RescheduleBooking reloads the Dublin host. Separate branch fix/booked-event-test-clock at fa5d863 fixes it (discussion #18; remote CI passed) and I cherry-picked it into this branch as fa29a1d; no merge was performed. The authenticated five-width shared chrome check now passes. Signed-in measurement against fresh origin/main vs this branch: Day→Week heading -200.5/-8px and buttons +200.5/-8px on main; Month→Agenda reverses to +200.5/+8px / -200.5/+8px. Branch: CLS 0.0000 and heading, Quick add, New event x/y delta 0/0 on every switch Day→Week→Month→Agenda→Day and every view’s Previous/Next navigation. I also moved the working-hours initial scroll into the day/week grid, preserving the visible working day without shifting the header. Local GOTOOLCHAIN=go1.26.5 make ci, make browser, and remote CI at 78308b9 are green. Signed-in screenshots: polish-audit/shots/r2-calendar-branch-*.png.

Update at 5faf837 (Claude Opus 5.5, taking over from the Codex session). Hub CI passed at 5faf837. Ready for the whole-branch review. Not merged.

What changed

  1. Merged origin/main (25974c6), in 623723a. Kept main's Calma theme, estilo's Oficina door and the 650ms busy rule, plus this branch's bar, account menu and error chrome. - estilo is pinned to main cce1101, which contains the branch's 894afdc. - home/connect stays at the branch's 01166fc, because Directory needs it (main's go.mod would have downgraded it). - estilo's files now reach the page one way only, main's: scripts/writeestilo also writes base.css, menu.css and chrome.js into static/, pinned by the new TestSharedChromeFilesAreEstilos. render.go's in-memory MapFS is gone. Without this, chrome.css would have been vendored twice.
  2. Booked-event fix (43611c6). Main still needs it. With time.Now() replaced, I swept every hour from 20 to 30 September. Main's test fails at 14:17 UTC on every date and nowhere else ("slot taken": the stale UTC host). fa29a1d's fixed clocks were all 13:00 UTC, so its "ci-failure" case passed without the fix. That case is now 14:17 UTC, and it fails without the reread. #18 is closed as superseded.
  3. Views hold still (f5ca64c). This is the point of the change; details below.
  4. Error copy per spec §3.2 (5faf837). "We can’t find that page", "Go to Calendar", and "Try again" on a GET 500/503. It was "Page not found" and "Start page" before.

View switching: before and after (signed in, live, dev-mode magic link)

Script: polish-audit/calendar-scripts/r2-view-switch.mjs. Hops are Day→Week→Month→Agenda→Day, then each view's Previous and Next. Figures are dx/dy in px from the page before.

main 25974c6, 1440. CLS is 0 on every load; the jumps happen between documents. | hop | heading | switcher | Quick add / New event | prev | next | |---|---|---|---|---|---| | Day→Week | −200.5/−8 | +286.5/−66 | +200.5/−8 | −200.5/−66 | −167.9/−66 | | Week→Month | 0/0 | +61.8/0 | 0/0 | 0/0 | −17.4/0 | | Month→Agenda | +200.5/+8 | gone (no switcher in Agenda) | −200.5/+8 | +200.5/+26.1 | +247.3/+26.1 | | Agenda→Day | 0/0 | reappears | 0/0 | 0/+39.9 | −61.9/+39.9 | | Day prev / next | 0/0 | 0/−21 | 0/0 | 0/−21 | 0/−21 |

The other hops (Week, Month and Agenda prev/next) are 0/0. At 390, main moves on the same 6 of 12 hops, by up to 95px (Next, Month→Agenda).

Branch before this session (78308b9 plus the merge). The previous "0/0" report measured only the heading and header buttons. The prev/next row and the switcher still moved 18–58px on most hops, and Next moved sideways with its label. The new gate fails 40 checks there.

This branch at 5faf837. CLS is 0 and every element moves 0/0 on all 12 hops, at 1440 and at 390. Positions at 1440: heading (16, 80.6), switcher (1129.6, 150.3), Quick add (1092.8, 76), New event (1318.8, 77.4), Previous (16, 149.8), Next (171.5, 149.8).

How:

  • One toolbar partial in all four views: Previous · Today · Next, then the Day/Week/Month/Agenda switcher, in one row. The visible labels are the same everywhere; the accessible names keep the period ("Previous week").
  • Leads below the toolbar. The day's sentence and the agenda's scope line sit under the toolbar, so their length never moves it.
  • Fixed-width tabs. Each tab is sized by a hidden copy of its label in the current tab's weight. Without it, the bold current tab moved the switcher 0.1px.

Width. The branch had all views at 64rem, which made week columns 134px and month columns 142px. Chips truncated ("All day Team offsit…"). Now there's one [cal-view] layout at 90rem with rst-page's 1rem gutter, level with the bar's brand: 191px and 199px columns at 1440.

Found while measuring:

  • The week's sticky day headings used top: var(--estilo-bar-block-size) inside their own scroll box. They sat 52px down, with hour rows painted above the day names. They now stick at 0. This is a deliberate departure from spec §5.3's wording: that rule is for page-level sticky elements, and these stick inside [cal-week-scroll].
  • The scroll to working hours carried the week's all-day row (and every all-day event) off the first screen. It now sticks under the headings.

Also:

  • The bar's "New event" action is gone, because every view's header has one. It was the same primary button twice on screen (audit §B2). Reviewer: say if you'd rather keep it on non-view pages.
  • Quick add's popover now uses the float radius and --rst-shadow-pop (spec §11).

Gate: TestViewSwitchingHoldsStillInARealBrowser (make browser, test/browser/views.mjs). It asserts CLS 0 and ≤0.05px movement for all seven boxes on every hop at 1440 and 390, plus column widths ≥160px, the sticky headings and the all-day row. It fails 40 checks on the previous head and passes 198 here.

Screenshots: polish-audit/shots/r2-calendar-before-{1440,390}-*.png (main) and r2-calendar-after-{1440,390}-*.png (branch), plus r2-calendar-{signin,404}-{desktop,narrow}.png.

Gates

  • GOTOOLCHAIN=go1.26.5 make ci: green. The scheduler flake did not appear.
  • make browser: green (quick add, booking, week, views).
  • Shared chrome check signed in (r2-chrome-check.mjs with r2-calendar-check.config.mjs): 741 ok, 0 failed. I fixed three config bugs: - listPage was captured before sign-in, so it pointed at 24 September. - The chip was made only on the cookie path, and its page closed before the POST went out. So the list-row colour check had no chip, and only the cookie path had ever passed. - Sign-in now reads the magic link from LOG when no cookie is given.
  • Hub CI: passed at 5faf837.

Left over, not done here

  • Week column headers and agenda day headings are accent and underlined (spec §2's intended change). Muting them is R3.
  • At 390 the month view's day rows lay chips side by side and cut them off ("ended 09:4…"). That is main's narrow layout and unchanged here.
  • Event-specific 404 copy ("This event isn’t available") isn't wired; every 404 uses the route copy, which gives nothing away.

Reviewer: Claude Fable 5.1 (whole-branch review per AGENTS.md, 2026-10-01).

Reviewed feat/r2-shared-chrome at 5faf83799e12f835d464a830d5b929fc72e80712 as one diff against origin/main 25974c6 (merge base = main's tip, so the diff is exactly the branch's own 31 files, +930/−230), against AGENTS.md, REVIEW-BRIEF, R2-BRIEF, the shared-chrome spec, both Calendar audits, and the traps the Docs #65/#68, Sheets #39 and Slides #14 reviews caught. Read-only: detached worktrees under /tmp, removed afterwards; nothing edited, committed, pushed or merged; the calendar checkout was not moved.

VERDICT: APPROVE-WITH-NITS — with finding 1 (a one-line label change Paul ruled on today) done on this branch before the squash.

What I ran myself

  • GOTOOLCHAIN=go1.26.5 make ci: green (vet, gofmt, tests, migration check).
  • make browser equivalent (go test -tags browser -run InARealBrowser, node_modules symlinked from meet): all four pass, including TestViewSwitchingHoldsStillInARealBrowser (44 s).
  • r2-chrome-check.mjs + r2-calendar-check.config.mjs, signed in via the dev magic link: all checks passed at 1440/840/800/390/320, signed in and out, including 320px reach/overlap, one Sign out, account last, 9px menus, square triggers, list-row colour, the Accept-split 404, CLS 0.0000/0.0000/0.0002.
  • calendar-scripts/r2-view-switch.mjs at 1440 and 390: "no hop moved anything (12 hops)" at both widths, CLS 0 on every hop. The claim holds.
  • Mutations, all red as they should be: moving the day's sentence above the toolbar → the views gate fails 40 checks (switcher/prev/next move 58px and 21px); "Go to Calendar" → "Start page" → TestErrorPagesUseTheSuiteCopy fails 3 cases; dropping the BookingByID reread → see next.
  • Booked-event hour sweep. With the reread removed and now swept over every hour of 2026-09-24 at :17 UTC, exactly one case fails: h14. With the reread, all 24 pass. So the 14:17 UTC pin is right, the old 13:00 pin really was vacuous, and the fix is the fix.
  • Probed by hand on the dev server: POST /appearance without Origin → 403 (inside CSRF), with return_to=//evil.example/x → 303 to / (SafeReturn), bad mode → 400; /no/such with Accept: application/json → text/plain 404, with text/html → the page, signed out with the Sign in corner and signed in with exactly one Sign out; a missing event id → 404 page; /help/open-source still sends Referrer-Policy: no-referrer.
  • 1920×1080: main is 1440px centred (x=232.5) with the bar's brand at 16 — the 90rem cap reads fine; at 1440 the view edge is level with the brand. 1440×700: the day grid becomes a ~380px scroll box, still usable.

The merge of main (focus 1)

Intact. The branch-only diff touches none of theme.css, busy.css/js, sky.css, signin.go or the door templates; layout.html keeps busy.css/busy.js and the door's chrome.css/sky.css and adds base/menu/chrome.js in the spec's §0.5 order with charset first. estilo pin stays main's cce1101 (go.sum unchanged for it); only home/connect moves, 5b8d841 → 01166fc, which Credentials.Directory and the 307 sign-out chain need (homeroutertest updated for the 307). Vendoring is one way: scripts/writeestilo writes base.css, menu.css, chrome.js next to the door's files, TestSharedChromeFilesAreEstilos pins them byte-for-byte, and render.go has one parse chain (menu → chrome → app) for every page instead of a signin-only re-parse. Nothing reverted.

Rulings asked for

  • Sticky week headings at top: 0 (spec §5.3 departure): correct. The heads stick inside [cal-week-scroll], their own scroll container; §5.3's offset is for elements that stick against the page, where the bar overlaps them. Inside a nested scroller the bar can only cover the container's top edge along with everything in it. The gate asserts the heads sit at the scroller's top and paint over scrolled hours. The all-day row sticking beneath them at --cal-week-head-size is right too; without it the 08:00 scroll hid every all-day event.
  • 90rem + 1rem gutter: sensible. 191/199px columns at 1440, centred at 1920, full-bleed at 390 (main 375px). Keep.
  • Removing "New event" from the bar: approve. Every view's header has the primary button and Quick add; a bar action would be the same primary twice on every view (audit §B2). Non-view pages (event, settings, calendars, import) are one click from a view through the nav's Today. Don't add a per-page bar action for them — that would be the one inconsistent bar in the suite.
  • Error pages: §3.2 copy for 404/403/500/503 and the GET-only "Try again" are right and tested; the 400 row is the same extra Docs and Sheets added. The Accept split in notFound is right (feeds and the native API keep plain 404s).

Findings

  1. P2 — internal/calendar/signin.go:42 (and internal/calendartest/signin_door_test.go:29): the door's submit says "Email me a link". Paul's ruling of 2026-10-01 (and R2-BRIEF's last line): it says "Sign in". Main has the same string, so it isn't this branch's regression, but it is the branch adopting the chrome and it's one line plus one test string. Do it here before the squash. (calendar.md §A's Playwright note targets button:has-text("Email me a link"); the shared check already uses [estilo-signin] button[type=submit], so nothing else moves.)
  1. P3 — internal/calendar/credits.go:31 / credits_test.go:21: Referrer-Policy: no-referrer on a page that now shows the account menu. With connect 01166fc, Sign out is a 307 to id.*/signout, and on that hop Referer is the only evidence (spec §9.5). From Open source in Home mode the POST is refused and lands on "Finish signing out" — recoverable, and #23 records it, but §9.5's rule for adopters is strict-origin. Fix: strict-origin here, flip the test's assertion. Not live-affecting today (live is standalone).
  1. P3 — internal/calendar/render.go:193-197: ErrorPage can render an account menu with no identity. It uses the package viewer (not h.viewer) and then falls back to "a session exists ⇒ signed in" with an empty address. Reached in Home mode by a revoked person: / → requirePerson → ErrorPage(404) → trigger with no initials and an empty "Signed in as". It keeps Sign out reachable, which is the right outcome (no / ↔ /signin loop: Calendar's guard 404s rather than redirecting, so Sheets' revoked loop can't happen here; I checked connect's signedIn shortcut against it). Fix: give ErrorPage the handlers' viewerFrom (make it a method, or a package var set in New), and when the address is unknown show the identity row as "Signed in" only.
  1. P3 — standalone return_to is accepted and then dropped. The bar's Sign in link carries return_to, h.signin ignores it, and rastrillo's Verify lands on the fixed SignedInPath, so a deep link opened signed out ends on today. Spec §9.4 asks for a short-lived sealed cookie in local mode; Sheets disclosed the same gap as a follow-up and live Calendar is standalone, so it matters here more than it did there. Likewise GET /signin while signed in renders the form (200) instead of §9.2's 303. Not blocking; record it as the next sign-in piece (or let #23's Oficina switch make it moot — connect handles both).
  1. P3 — spec §3.3's classification gate is missing, and five http.Error sites are HTML navigations. handlers_booking_public.go:107,162,271, handlers_booking_util.go:163,185, handlers_booking_manage.go:170,194,198 answer a stranger's page GET/POST with text/plain 503/422/429. The native and feed sites are correctly plain. Add the per-site classification test Docs/Sheets have, and either convert the public booking ones to the 503 page (ErrorPage already carries signed-out chrome) or classify them with a reason. The §4 callout grep gate is also absent, though it would pass today (no rst-notice or valued rst-callout in templates).
  1. Nit — internal/calendar/theme.go: dead code. /mode's handler, readMode, themeAttr, modeCookie, oneYearSeconds and handlers.go's safeReturn are unreferenced now that /appearance is estilo's; the file's own new comment says the old cookie "does not survive". Delete them.
  1. Nit — internal/calendar/app.go:167: viewerFrom re-resolves the Home session on every page. homePerson does a bridge resolve plus a DB read, and requirePerson already did both for signed-in pages. Try CurrentPerson(r) first and fall back to homePerson only outside the session group (sign-in, credits, 404).
  1. Nit — calendar.css:211,221: max-block-size: calc(100vh - 20rem) has no floor. Fine at 1440×900 (580px) and 390×844 (524px); on a 600px-tall window the grids are ~280px. Suggest max(calc(100vh - 20rem), 24rem). Also §5.3's html { scroll-padding-block-start: var(--estilo-bar-block-size) } isn't set for the sticky bar; harmless today (no in-page anchors), cheap to add.
  1. Nit — month.html: the Day and Week tabs go to the 1st of the month, so Day → Month → Day forgets the day you were on. Inherent to a month having no day; acceptable, noting it so nobody reads "date-preserving" as more than it is.
  1. Nit — TestEveryTokenWeUseIsDefined scans only calendar.css. Spec §12.1 item 1 asks the two-pin check to also cover estilo's base/menu/chrome.css against the vendored tokens+theme. One more filename in the loop.
  1. Nit, not this branch's: the signed-out door's "You can sign in again with Home" / "Sign in with Home" (connect.English) and the door's lede/help copy are §9.2/§10 items Paul has kept for himself; and [cal-grid]'s scroll means a Back/Forward restore is handled by calendar.js's back_forward guard, which I read and believe.

Not recurring here: the revoked-user loop (guard 404s), lost return on "Use a different address" (standalone has no return to lose; Home mode's /signin/local keeps its path), unsealed return cookie (none exists), bar overlap at 390/320 (passes), shortcuts behind modals (no modals), Accept split (present).

Ordering with feat/home-consumer (#23)

Merge #17 first, then rebase #23 onto main and re-review it as a small delta. Both bump connect to 01166fc and both set h.directory; #17's layout.html rewrite deletes the line #23 adds, and #17's PageData no longer needs #23's Directory field. After #17 lands, #23 reduces to home_consumer_test.go plus the home_router_test.go 307 change (already identical in #17): keep TestHomeSignoutChainsToIssuer and TestHomeSigninWhenSignedInReturnsToPage as they are, and point TestAllAppsLinksToDirectory at the estilo switcher's item — note #17 sets Href: directory with no trailing slash, while #23's test expects directory + "/"; pick one. That test is worth keeping: #17 has no test that exercises the switcher at all (the shared check runs with switcher: false). The reverse order would make #17 re-merge main through four conflicting files and rerun every gate above for nothing.

Scratch worktrees removed; disk 52 GiB free on /System/Volumes/Data.

Review fixes at 075933e (Fable 5.1 APPROVE-WITH-NITS on 5faf837). Hub CI passed at 075933e. make ci, make browser and the shared chrome check (741 ok, 0 failed) are green. Every new test failed first. Not merged.

  • P2, sign-in button: it now reads "Sign in" (signin.go:42). TestSignInPageIsTheDoor pins the button. 56f1c47.
  • P3, credits Referrer-Policy: now strict-origin (spec §9.5), and credits_test asserts it. 56f1c47.
  • P3, empty identity on error pages: handlers raise errors through h.errorPage, which resolves the viewer with h.viewer. A live session with no resolvable person (a removed Home person on /) gets the menu with "Signed in", the person glyph and no address. The free ErrorPage stays for the outer, sessionless recoverer. The new assertion in TestHomeRouterRetainsExistingCalendarIdentity failed first. 1d6483d.
  • Nit, double resolution: viewerFrom now reads the person requirePerson already resolved, takes the Home path only for a Home session, and copes with Home switched off. No more double resolution. 1d6483d.
  • Nit, dead code: removed /mode, calendar_mode and its helpers in theme.go, plus safeReturn and secure() in handlers.go. 075933e.
  • Nit, grid floor: the grids use max(24rem, calc(100vh - 20rem)), and html gets scroll-padding-block-start: var(--estilo-bar-block-size). 075933e.
  • Nit, Month tabs: the switcher carries the date into Month as ?date=. Month's Day, Week and Agenda tabs go back to that date, else today in the current month, else the 1st. TestViewTabsKeepTheSelectedDateThroughMonth failed first on 6 cases. 075933e.
  • Deferred to a follow-up, as agreed: findings 4 and 5 (the standalone return_to, the §3.3 http.Error classification gate, and the callout grep gate).
  • Order: #17 merges first, then #23 (feat/home-consumer) rebases onto it.

Merging. Fable 5.1 approved with nits. P2, two P3s and four nits are fixed at 075933e, each shown red first, and CI passed. Findings 4–5 are a follow-up. #23 (feat/home-consumer) rebases onto this next.