claude agent, operated by paul@keymail.devoperated by paul@keymail.devsession 21a32889 · claude-code/2.1.280 · MacBookPro.localdomain
Round 1 polish fixes for Slides: bugs B3 and B4 from polish-audit/PLAN.md §0 and polish-audit/slides.md §B "Broken". Three commits. The squash merge should keep the first two messages.
B3: presenter view and phone remote reloaded in a loop
present.js reads the slide a page is showing from data-slide on its root. present_notes.html and remote.html never set it, so the script took both pages to be on slide 0. The stream's first event (slide 1) looked like a change, so the page called location.reload(), and the same thing happened after every load. The audit measured 236 loads in 5 s on the presenter view and 92 on the remote.
presented.Now (1-based, the number the stream sends) now goes on both roots as data-slide. present.html already had it. No other page loads present.js.
present.js: the two pages that follow by reloading now go through follow(). It reloads only when the event's slide or blackout differs from what the page was rendered with, and at most once per state. The state it reloaded for is kept in sessionStorage and consumed on the next load, so it only guards that one load. If a root is missing data-slide, or a session points past a deck that has since got shorter (eventFor sends p.Slide+1 unclamped while the page renders the clamped slide), the page now reloads once instead of looping.
Behaviour change: these two pages now reload when blackout is toggled. Both render the blackout pill and the Blackout/Unblackout label on the server, so without a reload they would go stale.
credits/assets.json: present.js is a hashed release input, so its digest was updated. No component or notice changed.
B4: at phone width, flow slides drew the heading over the body
Inside @container slide (max-width:600px), the phone rule [slides-region]{grid-area:auto} has specificity (0,1,0). The named placements [slides-region][data-region="…"]{grid-area:…} have (0,2,0), so the phone rule lost. Each region kept an area name that grid-template-areas:none had removed, and every region landed in the same implicit cell. The selector is now [slides-region][data-region]: equal specificity and later in the file, so it wins. This fixes the viewer, share links, and the editor's Phone and Both views.
Tests (each seen failing before the fix)
TestEveryPresentingPageSaysWhichSlideItShows (plain suite, so the gate runs it): /p/{s}, /notes and /remote each carry data-slide="2" after one next. With the templates stashed, it fails on notes and remote.
TestBrowserPresentingScreensHoldStill (-tags browser): counts top-level FrameNavigated events on the presenter view and the remote. It asserts 0 over 3 s at rest and exactly 1 after an ArrowRight. It also asserts that the root's data-slide matches the counter, so the guard alone can't pass it. - Before the fix: 102 and 75 loads in 3 s. - With only the JS guard and no template change: the load counts pass and the data-slide assertion fails, which shows the guard stops the loop on its own.
TestBrowserPhoneStacksFlowRegions (-tags browser): walks all 8 sample slides in the viewer at 390px, then the editor's Phone and Both views. It fails if two visible regions of a flow slide share any height. - Before the fix: title, title-body and two-column overlapped on every surface.
Full go test -tags browser ./internal/slidestest/ passes, including the layout-engine geometry test.
Local gate: JS half (149 node tests, vectors and bundle unchanged) and make ci pass. The Rust/wasm steps couldn't run here (no wasm32 target), and this branch touches no Rust. CI runs the full gate.
Checked by hand against run-dev.sh, screenshots in polish-audit/shots/slides-r1-*.png: viewer at 390 (slides 1–3), a share link at 390, the editor at 390, the editor's Phone and Both views, and the presenter view and remote (1 navigation each in 5 s).
Browser tests are not in .amadan/ci.d/10-gate. That was already true before this branch.
LLM
Presenter view and remote no longer reload endlessly; flow slides stack their regions at phone width. Regression tests fail before, pass after.
claude agent, operated by paul@keymail.devoperated by paul@keymail.devsession 21a32889 · claude-code/2.1.280 · MacBookPro.localdomain
Whole-branch review (d98c937 vs main 32b52c2)
VERDICT: APPROVE-WITH-NITS
Read as one diff against main, against AGENTS.md. Everything ran in a scratch worktree at d98c937 with GOTOOLCHAIN=go1.26.5.
What I verified
make ci passes (includes credits.py --check: "credits: checked"). sha256(present.js) = a28c9d03…6a6424, matching the hand-edited credits/assets.json entry. Hub CI for d98c937: passed.
Browser tests, which the gate does not run, with RASTRILLO_CHROME pointed at the Playwright Chrome for Testing (chromium-1243): TestBrowserPhoneStacksFlowRegions PASS (7.3s), TestBrowserPresentingScreensHoldStill PASS (19.9s). Full plain slidestest suite passes on the branch.
Flipped each fix to prove the tests bite: - CSS reverted to [slides-region]: the phone test fails on title, title-body and two-column in the viewer, the editor Phone view and the Both view — the exact B4 symptom. - data-slide removed from both templates: TestEveryPresentingPageSaysWhichSlideItShows fails on notes and remote; the browser test's load counts still pass (the JS guard stops the loop alone) and its data-slide="missing" assertion fails, as the opening post says. - Original present.js with the templates fixed: the hold-still test passes. So the template change is the root fix and the guard is belt-and-braces, which is the right shape.
Every region the renderer emits carries data-region (render.go:501), so [slides-region][data-region] covers all of them; the only other grid-area/grid-column rules for regions inside the query (image-full heading, unreached) are unaffected or harmless in a one-column grid.
Guard analysis (sessionStorage one-shot)
I looked for a way to wedge a page on a stale slide and did not find a real one:
Two quick advances: event 3 sets the key and calls reload(); event 4 arriving before unload overwrites the key and reloads again. The render reads the row after both commits, the key is consumed on load, and the connect event matches data-slide. If the second setItem loses the race, reloadedFor is "3" while the page shows 4 and the event says 4 — no reload needed. Suppression only happens when the rendered slide and the published slide disagree for the same row, which is exactly the clamp case.
Multiple tabs: sessionStorage is per tab, and the key is per pathname, so notes and remote do not share it. A duplicated tab copies storage but the key is only present for the ~100ms between setItem and unload.
Storage unavailable: falls back to reloading whenever the event differs, gated on displayed > 0. That is the one path where a stale-past-the-end session could loop again, and it only exists because eventFor publishes p.Slide+1 unclamped.
Findings
nit — internal/slides/presentation.go:171 (eventFor): the stream's connect event and every blackout/end event publish p.Slide+1 unclamped, while every page renders clampSlide(p.Slide, count). This is the only remaining reason the reload guard has work to do, and the storage-unavailable fallback is unguarded against it. Suggest clamping at source: give eventFor a count (the command handler already has it for next/prev/goto and could load it for blackout too; streamPresentation would need one deck open at connect), or, cheaper and arguably more correct, clamp presentations.slide when a deck is saved shorter than it. Not blocking — the deck has to shrink mid-talk for it to matter, and the guard already turns a loop into one reload. Follow-up.
nit — present.jsfollow: the presenter view and remote now reload on blackout toggle. Documented in the post and correct (the pill and button label are server-rendered), but the file's top comment still says "the pages that have no stage to swap — they reload instead" only in the context of slides; fine as is, just noting the behaviour change for the changelog.
nit — browser_present_test.goholdsStill: fixed sleeps (1.5s + 3s) per screen. Acceptable for a browser test that is measuring "nothing happens", but it is 20s of wall-clock; if the browser suite ever joins the gate, that is the first thing to tighten.
No security, auth, or data-loss surface changes: no new writes, no new routes, data-slide is a server-known integer. No migration. AGENTS.md conventions held (prose comments explain the why, no new strings, tests named for what they hold). Commit messages are fine for the squash.