Repository navigation
Conversation
checkBeforePublish reads a draft's fields and slots and returns the
first problem the code can prove, blocking ones first: no slots, no
fields, or every slot empty (only spaces count as empty) block; a slot
value still holding TBD, TBC or TBA as a word, in any case, is a
warning. The warning carries the slots, each word they hold, once and
in order, the field it's in on the first slot, and that slot named by
its other values as the page prints them ("Sat, Nov 14 · 1:00 PM").
It reads only a field's ref, label and type and a slot's id and
values, never the title or description.
publishCheckCopy gives the words the panel shows: "Nothing to sign up
for yet" with Fill it in (or Add a slot; none without fields) and Not
now, or "Before people see it": "<slot> still says “TBD”." or "3 slots
still say “TBD” or “TBA”." and that a published signup can't go back
to being a draft, with Name it first and Publish anyway.
publishCheckTarget says where the fix goes: the slot to open (or a
new one) and the field to focus. PublishCheckAnswer is what Publish
answers with: the check, its copy and its target.
The placeholder regex is the one from the closed draft-notices
branch, near-misses included ("TBDX", "Tbacco", "ÉTBA").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
useBuildState gains flushPendingSaves(): it sends each edit still waiting out its 800 ms debounce at once, clearing its timer so it can't be sent again, then waits until no save is waiting or in flight, going round again for one a save sets off as it ends (adding a list choice, then the cell that uses it). It resolves to whether every save since it began succeeded. Until now a debounced save stopped being tracked once its timer fired, and adds, deletes, moves, group renames and settings saves were never tracked. Saves are tracked where they run (withSave, the move path, the group rename, and a settings save from the moment it's queued), and a failure is counted as it happens. The unmount flush now drops the timers it fires, so a flush after unmount (Publish on another tab) waits for those saves without sending them twice. addRow resolves to the new slot's id, or null when it's refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Narrows #503. Publish and share now saves what the Build tab has pending, then checks the draft as stored, and publishes only when nothing is wrong or the organizer chose Publish anyway. - publishAction reads the signup fresh (the non-cached read, then the write guard, so a viewer still gets "You don’t have edit access." before anything is checked) and, while it's a draft, runs checkBeforePublish. No slots, no fields or every slot empty answer { check, copy, target } and publish nothing; so does a slot still saying TBD, TBC or TBA, unless the form says acknowledge=placeholder. The form can't wave a blocking problem through. The read takes no lock, so an edit landing between the check and the publish isn't checked; that window is narrow. A signup that isn't a draft goes straight to the service, which asks for a reload. The service, the REST endpoint and the MCP tool are unchanged, and the service's "Add a slot before publishing." stays for a race that reaches it. - PublishButton replaces both Publish and share buttons (the computer header's and the phone bar's). Pressing it starts loading the share panel, waits for the Build tab's saves through BuildBridge, then runs the action in a transition. If a save failed it stops there and PublishRefusal says "A change didn’t save. Fix it, then publish." where Publish's refusals go, so the share panel never covers the save's error; a save that hasn't answered in 10 s no longer holds it up. "Publishing…" covers the wait; the button stays focusable (aria-disabled). - PublishCheckPanel opens by the button: a non-modal popover on a computer ([Not now|Publish anyway] [fix]), a sheet over the bar on a phone (fix first, both 48px), named by its title and described by its text, focus on the fix (Not now when there's none). Escape, Not now and the sheet's ✕ put focus back on Publish and share, and on a computer pressing it again closes the popover. The live region says nothing. Popover and BottomSheet take a description id for this. - BuildBridge, a client context in the [id] layout, lets the header reach the Build tab, which registers flushPendingSaves and openSlot. Fill it in and Name it first open the slot's editor with focus in the field to change, scrolled into view; Add a slot adds one and opens it. From another tab the bridge keeps the request and goes to Build's own address; the Build tab takes it once as it registers. Nothing goes in the address, so Back, Forward or a bookmark never opens it, or adds a slot, again. A Build tab that unmounts hands the bridge its last saves, and the next Publish waits for them. A slot the Build tab hasn't loaded (added elsewhere) opens nothing, and focus goes back to Publish and share. - Tests: the order (the action waits for the flush), a failed save, the 10 s cap, the panel on a computer and a phone, each fix with and without the Build tab, a slot it can't open, Publish anyway's form; BuildBridge's request taken once and the saves a tab leaves behind; BuildWysiwyg opening a slot through the bridge, as a new slot and after a remount; DB tests for each kind, Publish anyway, a blocking problem the form can't wave through, a viewer and a non-draft. The existing viewer tests' signup gets a filled slot so its publish still goes through. - e2e: at a computer's and a phone's size, an empty slot stops Publish, Escape returns focus, and Fill it in opens the slot; a TBD slot warns from the Responses tab, Name it first lands on Build with the slot open, and Publish anyway publishes. On a computer: Add a slot from another tab adds one, and Back and Forward add no more; a save the server refuses stops Publish with the line; an edit typed just before going to another tab is what Publish checks. The existing publish smoke fills its slot first (createDraft helper). - CHANGELOG. Budgets, measured against 344cd6c: every signup tab's first load grows by 1,311 bytes gzip (PublishButton, PublishCheckPanel, BuildBridge; the publish module is 1.8 kB with the Send icon it took from PhoneBar), and Build by 2,140 (the flush and the slot opening as well). Over budget at 128/128/127/127/155 kB, so: [id] and edit 127 -> 128 (127.8), settings 128 -> 129 (128.8), responses 128 -> 130 (129.2), build 155 -> 157 (156.9). Lazy-loading the panel saved only 213 bytes on these tabs and added 21 to every page's webpack runtime, which put / and /s/[slug] over theirs, so the panel stays in the first load. Nothing stored or sent changes, so the legal pages stay as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
"Create and publish your first signup" gets a note under the Publish step: when every slot is empty, Nothing to sign up for yet, and the signup isn't published; Fill it in opens the first slot. When a slot still says "TBD", Before people see it, with Name it first or Publish anyway, and people see "TBD" until it's changed. The step then says the share panel shows once it's published. The UI list gains the two headings and three buttons. The phone walkthrough's new draft has its one slot empty, so it now goes through the check: Publish and share, Nothing to sign up for yet, Fill it in (focus in What), "TBD", Publish and share again, Before people see it, Name it first (focus back on the TBD), "Fruit and water", then publishes. The computer walkthrough no longer waits for the copied slot's date to save before publishing: Publish waits for it now, so the share message's dates check that it does. No picture shows these screens, so none is re-taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Conflicts: - CHANGELOG: PR 3's #499 and #390 lines as they now read, the #503 entry first, and #390 without "Add a slot before publishing.", which a signup with no slots now gets as the check. - budgets.json: this branch's numbers, re-measured after the merge. - ShareButton.test: ShareScope's new signupId, with the real Publish and share button. PublishButton.test gets the signupId too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
#525 counts refused publishes as signup.publish_refused, written by publishSignup. The check now stops a draft with no slots before the service sees it, so from the header that row was never written again. recordPublishCheck (services/signups.ts) writes it for every check that stops a publish, after reading the signup and running the write guard, best effort and outside any transaction, as #525's own write is: { code: 'conflict', reason } with no_slots as before (the same payload the service writes) and now no_fields and all_blank, and { reason: 'placeholder', warning: true } for the warning, so a warning shown and a later Publish anyway (which writes signup.published and nothing more) can be told apart. The publish action calls it before answering with the check. A viewer is refused before the check and writes nothing; the MCP tool and the REST API still get the service's own rows. docs/telemetry.md's row says all of this; the privacy page already covers "a publish that didn't go through". BuildBridge, from the review's optional items: - A fix's pending request is dropped when the path settles on any tab but Build, so an abandoned Add a slot can't add an empty slot on a later visit. - A departed Build tab's save result stops counting once a new Build tab registers; the next Publish still waits for those saves. The CHANGELOG now says the warning names each slot's first such word (once each), which is what words holds, and that "Add a slot before publishing." still shows if the last slot goes in the moment between the check and the publish. Tests: a row per check kind with its payload, none after Publish anyway, none for a viewer, and the service's own row for a publish the check didn't see; the bridge drops a request when the organizer goes elsewhere, and drops a departed tab's failure once a Build tab shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
No conflicts. docs/telemetry.md keeps both changes: PR 2's signup.link_shared lines and this branch's signup.publish_refused row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Measured against PR 3 at 6ce99a6, after merging main's #525 and #526: every signup tab's first load grows by 1,362 bytes gzip and Build by 2,216 (the publish button and its check panel, the bridge to the Build tab, and on Build the flush and the slot opening). That is 51 bytes more than last round, for the bridge's path watch and the dropped result of a departed tab's saves, and #525's base moved too, so four pages are just over what this branch had set: - /app/signups/[id] and edit: 128.06 kB, 128 -> 129 (PR 3: 127) - settings: 129.02 kB, 129 -> 130 (PR 3: 128) - responses: 129.48 kB, stays 130 (PR 3: 129) - build: 157.17 kB, 157 -> 158 (PR 3: 155) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Tophat: passedI ran The head has moved on since. It now also has the catch-up merges with #578 and
What changes for organizers: Publish and share first saves anything still being typed, then checks the draft:
Looked off, but not a failure:
Screenshots are in the session linked below. https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H Generated by Claude Code |
Review finding (blocking). A fix pressed on another tab leaves its slot for the Build tab to open as it mounts, and that request was dropped only when the path settled on a page that isn't Build. When the push to Build was overtaken by a navigation to the same page (the tab they were on, chosen again, or Settings' Save coming back to Settings), the path never changed, the request stayed, and a later visit to Build added an empty slot. The bridge now has dropPending(), and the request is dropped, besides the path watch, when the organizer chooses any tab but Build (TabsNav, the current tab included), goes Back or Forward (popstate), or sends a form (a submit anywhere, which covers Settings' Save). The doc comment lists exactly these. Tests: a click on the current tab, another tab, Back and a form submit each drop it; a plain re-render on the same page and a click on Build keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review finding (blocking). docs/telemetry.md said a warning and a
Publish anyway were told apart by whether signup.published followed,
but Name it first, then Publish and share again, gives the same two
rows, and the Publish anyway path recorded nothing about the choice.
publishSignup takes an optional { acknowledged: 'placeholder' }, which
goes into the signup.published row it already writes. The publish
action passes it only when the form said Publish anyway and the check
it ran still found a placeholder; the MCP tool and the REST API call
publishSignup without it. docs/telemetry.md now says: after the
warning row, a signup.published with acknowledged: 'placeholder' means
Publish anyway, and one without it means the slots were fixed first;
real refusals are the signup.publish_refused rows whose payload has
code. Nothing personal is stored, so the privacy page is unchanged.
DB tests: Publish anyway writes the flag; naming the slot first and
publishing writes none; nor does a Publish anyway that reaches the
server after the slot was named.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review finding (optional). recordPublishCheck read the signup outside the best-effort write, so a database error there threw out of the publish action instead of showing the check, though docs/telemetry.md says a failed telemetry write never fails the request. The read is caught now: it logs a warning and records nothing. Test: with a database that won't answer, it resolves ok rather than throwing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review finding (optional): the test helper read actor_id and dropped it. Each check kind's row is now asserted to be the organizer's who pressed Publish and share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Conflicts, both sides kept: - useBuildState: addRow still resolves to the new slot's id (for the check's Add a slot), now with #568's capacity of 1 and no seed capacity; #568's duplicateRow (the copy placed after the original, resolving to its id) saves through withSave, so the flush tracks it. A hook test now has the flush wait for a duplicate in flight, as when Duplicate is pressed just before Publish. - BuildWysiwyg: #568's focusRowId for a fresh duplicate's editor and this branch's focus request for the check's fix, side by side; the fix opens its slot through #568's expandRow, so a stale duplicate focus can't win. - BuildWysiwyg.test: #568's duplicate tests and this branch's bridge tests. - The publish walkthrough: #568's steps for the copy (opening below the original, its date's own save waited for before the reload check). - budgets.json: PR 3's numbers, re-measured in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Measured against PR 3 at 31f97ec (which carries main's #568): every signup tab's first load grows by 1,472 bytes gzip and Build by 2,446. That is the Publish and share button and its check panel, and the bridge to the Build tab, now also dropped by TabsNav, Back and Forward and a form submit; on Build, also the flush and the slot opening. PR 3 moved [id], edit and build up with #568, so those three go over: - /app/signups/[id] and edit: 126.70 -> 128.17 kB, budget 127 -> 129 - build: 155.28 -> 157.73 kB, budget 156 -> 158 Responses (129.59 kB) and settings (129.13 kB) stay at the 130 this branch already set (PR 3: 129 and 128). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
The #503 entry still gave round 3's rule, that a Publish anyway after the warning writes nothing more, which c71d757 made false. It now says the signup.published after the warning carries acknowledged: 'placeholder' when the organizer chose Publish anyway, and not when the slots were fixed first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review finding. A Ctrl, Cmd, Shift or middle click on a tab opens it in a new browser tab, but TabsNav dropped the pending fix all the same, so this window still went on to Build and the fix silently did nothing. It now drops the request only on a plain primary click. Tests: a Ctrl-, Cmd- and Shift-click and a middle click keep it; a plain click still drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review finding. docs/telemetry.md said a signup.published without acknowledged: 'placeholder' after a warning means the slots were fixed first, but a publish through POST /api/signups/[id]/publish, or a connected AI assistant's publish_signup, has no flag either and never checked. It now says so, and that the assistant's row also carries viaClientId (the REST route's cookie session doesn't). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
richshaw
left a comment
There was a problem hiding this comment.
Review: passed (rounds 1–5)
This PR delivers the narrowed part of #503 within the redesign. Publish and share first sends any edits still being saved, then checks the draft on a fresh read:
- Nothing to sign up for (no slots, no fields, or every slot empty): it won't publish. Fill it in or Add a slot opens the slot in Build, from any tab.
- A slot still says TBD, TBC or TBA: it warns, with Name it first or Publish anyway.
- Nothing wrong: it publishes at once, as before.
The REST and MCP publish paths are unchanged. Rounds 1 to 4 were each an independent agent that didn't write the change: code-review at high effort as a starting list, each item checked by hand, and from round 3 a running-app check on its own dev server at 1280 and 390. Round 5 is three small commits (29703de, 5dcfaac, 9c4281e) that fix round 4's findings. I read those myself, and a final independent pass over the whole stack will look at them again.
Blocking (0). Earlier rounds had four, all fixed:
- Round 1: the fix's open-slot request lived in the address (
?slot=new), so Back, Forward or a bookmark replayed it and added another slot. It now sits in the layout'sBuildBridge, and the Build tab takes it once. e2e checks Back and Forward. - Round 3: a pending Add a slot survived when the organizer stayed on the tab they pressed it on (clicked it again, or Settings' Save came back to the same path), so a later Build visit added an empty slot. The bridge now drops the request on a plain click on any tab but Build, on Back and Forward, and on any form submit. Round 4 re-ran the exact scenario with Build's load delayed 5 s, at both sizes: no slot.
- Round 3:
docs/telemetry.mdsaid a warning and a Publish anyway could be told apart by whether asignup.publishedfollows, but fixing first then publishing gives the same rows. Publish anyway now writesacknowledged: 'placeholder'on thesignup.publishedrow, and the doc says so. Round 4 checked the rows for Publish anyway, a fix then publish, and a slot named in another tab, and that REST and MCP can't set the flag. - Round 4: the CHANGELOG still gave round 3's old rule. Fixed in
29703de.
Fixed from optional findings (each marked with the round that found it):
- Flush gaps (round 1): a save sent as a Build tab unmounts, saves a departed tab left in flight, and a failed save are all waited for and counted, and a failed save stops Publish with "A change didn’t save. Fix it, then publish." A 10 s cap keeps a stuck save from holding Publish.
- A pending fix never expired (round 2): it's dropped when the path settles anywhere but Build, and on the triggers above.
- Check outcomes recorded (round 3): the check stops before the service, where #525 records refused publishes, so it writes the same
signup.publish_refusedrow itself (recordPublishCheck, outside any transaction, after the write guard). A read failure there is caught and logged, so it never fails the publish. Tests assert the actor on each row. The doc says to count real refusals as rows whose payload hascode, since warnings are rows too. - A Ctrl/Cmd or middle click on a tab (round 4) dropped the fix while this window still went to Build. Only a plain primary click drops it now (
5dcfaac, tested). - Wording: "each slot's first such word", the "Add a slot before publishing." CHANGELOG line, the bridge's comments, and the doc's note that an API publish carries no flag either.
Optional, left as is (3):
- A Duplicate whose save takes longer than Publish's 10 s wait can take focus from a fix's slot when it lands (round 4, reproduced with the save delayed 12 s). The root is #568's code, which opens the copy whenever its save returns, whatever was opened since.
mainhas the same steal when another slot is opened meanwhile. One follow-up should fix it there. - A departed Build tab's failed save is dropped once a new Build tab registers. The new tab shows the stored values, so nothing wrong is shown, but the failure isn't mentioned again.
- A save slower than 10 s can leave the check looking at the stored values, so it may say a slot is empty that was just typed into. That's the cap working as designed.
The merges: PR 3's catch-ups with main (#525, #526, then #568) are merged in. Round 4 checked the #568 merge: a duplicate made just before Publish is waited for, and the check sees it. Duplicate still opens the copy right after the original with focus in What. A fix's focus wins over a duplicate's that saved within the wait.
Earlier threads: none.
Checks:
- CI is green on this SHA (
9c4281e), and was one8150c0. - The tophat passed on
3952591(comment above). Rounds 3 and 4 re-ran its flows on later heads, at 1280 and 390, with no console errors. - Run locally on
e8150c0:pnpm lint && pnpm typecheck && pnpm knip && pnpm test(1595),pnpm test:db(562),pnpm build && pnpm budgets, and the full chromium e2e suite withCI=1(72). On9c4281e: lint, typecheck, knip and unit tests (1599),pnpm build && pnpm budgets, and the chromium e2e fororganizer.spec.tsand the publish walkthrough (20).
gate=owner: this is UX. It adds a check panel to Publish and share, opens slots from other tabs, and records each check's outcome.
https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Generated by Claude Code
Only a plain primary click on a tab other than Build drops the fix on its way to Build; a Ctrl, Cmd, Shift or Alt click keeps it, and a middle click never reaches onClick. The comments in TabsNav and BuildBridge now say so. The test for a middle click fired a click event browsers never send; it now fires auxclick, and an Alt-click case joins the modified clicks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Drafts lost their copyable link, so the signup.link_copied row no longer says what a draft's link shows; only rows written before then can carry draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
|
Final independent check: passed. An agent that didn't write the change checked round 5's three commits, which the review above says I had only read myself:
It found three small things, now fixed:
I read both, and on The smoke test of the whole stack at the top (#581), at 1280 and 390, covered this PR's part:
Left as is, as the review says: a Duplicate still saving after a fix can take focus when it lands. The root is #568's code on Nothing else is left to do here; it's waiting on review. Generated by Claude Code |
Brings in PR 2's share message by way of PR 3: spots open rather than a count of slots, and "Sign up here, no account needed:" as its last line, with the help article, its walkthrough, share-panel.png and the #505 CHANGELOG entry to match. It merged without conflicts: PR 4's check steps and note in the article and walkthrough sit beside the new message sentence and regex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings PR 2's 4a490fb through 487b2db: the share message's rules as their own sentences in the #505 entry, and one repeated test gone. Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
|
Tophat: two things in the publish check I tophatted the stack locally and ran a UX walkthrough of Publish and share, on a computer and a phone. Both of these are on 1. The spinner flashes before "Nothing to sign up for yet" opensOn a draft with nothing in it, pressing Publish and share turns the button into its spinner and "Publishing…", and almost at once the "Nothing to sign up for yet" panel opens. The spinner shows for a split second, so it looks like a glitch. Why: in Suggested: show the spinner only when the answer is slow. Wait about 300 ms before showing it, and once it shows, keep it for at least about 400 ms so it never flickers. Nothing moves when the answer is instant, and a slow publish still gets a spinner. Also, "Publishing…" isn't right when the answer is a check and nothing gets published. The delayed spinner hides that in the fast case. If a check can be slow, the label could fit both outcomes. 2. When the TBD is in the slot's name, the warning names the slot only by its dateA signup with the rows "Cashier TBD · Sat, Nov 14" and "Grill · Sat, Nov 14". Publish and share says:
It reads as if the date is TBD. Both rows have that date, so it doesn't say which one. (Name it first does open the right row, and the row's TBD tag marks it.) Why: Suggested: leave a field out of Generated by Claude Code |
Pressing Publish and share on a signup with nothing to save showed the spinner and "Publishing…" for the moment the check took, then the check's panel. The button now looks busy only once it has been for 300 ms, and then for 400 ms at least, through a small useDelayedFlag hook, so an answer that comes at once flashes nothing and one just slower doesn't flicker. It is still busy at once, so a second press does nothing, and aria-busy and aria-disabled still follow that. It says "Checking…", which is true whether the press ends in the check or a publish, since a publish checks first. Both buttons, the header's and the phone bar's, read the one flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
… into the publish check Brings in, by way of PR 3: the close question's "Everyone already signed up keeps their spot." once someone has signed up; main's #569 cut of an over-long Magic Compose title or description; and PR 2's second owner round (the share message's date and time line, Copy message's widths, Email it's body without the title, one message label, the growing message box, the title under the QR code in its picture, the phone's large QR code with Save image, the new QR alt, and qr_code once per opening). docs/telemetry.md conflicted on the publish, copy and share rows: the publish_refused and link_copied rows keep this branch's words (the check's reasons, and that a draft has no link to copy), and link_shared takes PR 2's qr_code wording. CHANGELOG, the help article, its UI list and the walkthrough merged on their own; each fact is in them once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Since the last change, a value was left out of the warning's slot name only when, trimmed, it was the word, so "(TBD)", "TBD." or "TBD?" stayed in: "Fruit and water · Sat, Apr 6 · (TBD) still says “TBD”." The value is now compared with its punctuation turned to spaces, so those read "Fruit and water · Sat, Apr 6 still says “TBD”." again, and "Cashier TBD" still names the slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
With the answer at about 400 ms, the check's panel was open while the button kept its spinner until the 400 ms minimum ran out, and by then aria-busy was already false, so it looked busy while it worked. The busy look now ends in the same render as the press's answer shows: the check's panel, a refusal, or the line saying a change didn't save. PublishState keeps the result Publish had when pressed, and a different one (or that line) is this press's answer; it stays an answer after the panel is closed, so the spinner doesn't come back. The minimum is also let go then, so a press soon after waits its 300 ms again. It still holds 400 ms when the press changes nothing else on the page, so the button never flickers. The check's panel used to open from an effect, a render after its answer came, which with the spinner gone at once left a frame or two of the plain button first. It now opens in the same render: PublishState keeps the answer whose panel was closed, in place of an open flag set by an effect, and a check that isn't that one is open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
The button went from about 160 to 120 px wide while it said Checking…, moving Preview 39 px and back. Both labels, each with its icon, now sit in one grid cell, the one not shown invisible and aria-hidden, so the button is as wide as the wider label in any language and its name is the label shown. Each icon stays beside its own words, centred with them, so the spinner doesn't sit apart from a shorter "Checking…". Both buttons, the header's and the phone bar's, do it. The header's test reads each button's name as a screen reader would, leaving out the hidden label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
…o the publish check Brings in, by way of PR 3: PR 2's fixes from the independent check (the QR picture's right-to-left ellipsis and whole characters, Save image taking no second tap, the share message's days and time from the open slots, the message box sized as the browser measures it, and the help's "Most phones then open their share menu…"), and PR 1's KEEPS_THEIR_SPOT, shared by the closed banner and the close question. layout.tsx conflicted where this branch wraps the page in BuildBridge: it keeps that wrapping, with PR 1's import of KEEPS_THEIR_SPOT and the banner's body built from it. CHANGELOG and the help article merged on their own; each entry is there once, as its branch has it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
A value was left out of the warning's slot name only when, less its punctuation, it was one placeholder word, so "TBD/TBC" or "TBD TBD" stayed in: "Fruit and water · Sat, Apr 6 · TBD/TBC still says “TBD”." Now a value counts as naming nothing when every word in it is TBD, TBC or TBA, in any case, so those read "Fruit and water · Sat, Apr 6 still says “TBD”.", and "Cashier TBD" still names the slot. Words split where the check's own match sees them end, so "TBD_TBC", which it doesn't count as a placeholder, stays in the name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings PR 2's ff75578 and 7a019bb through 21eb67c: a QR picture's title read in one direction from its first letter, and a full slot left out of the share message's days and time. Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
|
Both are done. 1. The spinner flashIn When the spinner shows:
Keeping the header still: both labels sit in one grid cell, so the button keeps its width and Preview doesn't move. Its accessible name is always the visible label. Checked: an independent agent timed it frame by frame at 1280 and 390:
2. The TBD in the slot's nameIn The rule: a field is left out of the label only when its whole value is the placeholder word, give or take punctuation, or only such words.
Checks:
Generated by Claude Code |
Brings PR 2's d33a05a, 4e0e3fd and 1c178c4 through 64f5f93 (on a phone, QR code looks pressed and says Hide QR code while the code shows; the share sheet's button rows are two-column grids), and main's #614, #613 and #617 (next 15.5.28). Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Carries main's ee0f0d5 (vite 7.3.5 to 7.3.7) and 740e98d (nodemailer 10.0.9 to 10.1.4) up the stack. Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings PR 3's merge of main's 24f8689 and bcb82b3, the squashed #571 and #574. Their content was already on this branch, so the tree doesn't change; this moves the branch's merge base with main up to bcb82b3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Replaced by a newer auto-merge request
…he 5-prompt branch Through the server, telemetry and screen branches. rate-limit.ts keeps this branch's 12 drafts an hour and main's reworded link-beacon comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UW9ZpEu4MWkeqTv2Qe14ks
Part of #503, which the owner's design comment narrows to "the check at Publish". Its other checks (merging copies, empty fields, unnamed fields, ungrouped dates) aren't in this PR, so #503 stays open for the owner to close or rewrite. This is PR 4 of 5 for the "Publish and share" redesign in #498 (row I), stacked on #578. Review it against
claude/relaxed-dijkstra-cpx7zo-3-publish-button.The problem
Publish published whatever was stored, including:
Once published, a signup can't go back to being a draft. There was also a quieter race: edits on the Build tab save about 800 ms after the last keystroke, so a Publish straight after typing published the version from before the edit.
The fix
Publish and share sends what's waiting to save, then checks.
aria-busyandaria-disabledare set at once, so a second press is refused and focus stays on the button.checkBeforePublish. It's a pure function,src/lib/publish-check.ts, and returns the first problem:publish_signuptool,POST /api/signups/[id]/publishand the service are unchanged, apart from the service taking an optionalacknowledgedthat only the header's Publish anyway passes (below).The panel
The fixes
BuildBridge) while it navigates to Build. The Build tab takes it once when it registers. It's never in the URL, so Back, Forward, a reload or a bookmark can't replay it.What's recorded. #525 records
signup.publish_refusedin the publish service. This check now stops "no slots" before the service, so the check writes the same event itself, throughrecordPublishCheckinsrc/services/signups.ts(policy guard, best effort, outside any transaction; a failed read is logged and never fails the publish):{ code: 'conflict', reason: 'no_slots' | 'no_fields' | 'all_blank' }for a blocking problem.no_slotsis the same shape Record link copies, failed AI drafts and refused publishes #525 writes.{ reason: 'placeholder', warning: true }for the warning. A warning doesn't stop anyone, sodocs/telemetry.mdsays to count real refusals as the rows whose payload hascode.acknowledged: 'placeholder'to thesignup.publishedrow. A publish after the slots were fixed has no flag. The REST and MCP publishes can't set it.Choices this makes
publishSignup's lock. The service's own "Add a slot before publishing." still covers the case where the last slot is deleted./and/s/[slug]over their budgets.mainhas the same steal when another slot is opened meanwhile. It wants a follow-up there.Tests
publish-check.test.ts: each kind, the order of precedence, near-misses, the slot label (whole-value and punctuation-only placeholders left out, "Cashier TBD" kept), every distinct word, and the copy for one slot and for several.useBuildState.test.ts: the flush sends pending saves and waits for those in flight and those that aren't debounced (a duplicated slot included), loops until nothing waits, reports a failure, and doesn't send twice on unmount.useDelayedFlag.test.ts: on fake timers, an instant answer never shows, a slow one shows at 300 ms, an early end keeps it to 400 ms, and a press after it hid waits again.PublishButton.test.tsx: the flush resolves before the action; the 10 s cap; a refused save stops it; the panel's focus and dismissal; the fixes; a phone sheet. On fake timers: "Checking…" on both buttons only after 300 ms, gone the moment the panel or a refusal shows and not back after Escape; and the stacked labels' names.BuildBridge.test.tsx, with the realTabsNav:auxclick);actions.db.test.ts: each kind blocks or warns and leaves a draft; an acknowledged placeholder publishes; an acknowledged blocking problem still blocks; a viewer is refused; a non-draft gets the reload message.acknowledged: 'placeholder'onsignup.published, and a fix-then-publish, or a Publish anyway after the slot was named elsewhere, writes{ from, to }only. A viewer writes nothing. A read that throws records nothing and still returns the check. The service's own row still appears when the check is bypassed.organizer.spec.ts, 1280 and 390):mainand the earlier PRs: Record link copies, failed AI drafts and refused publishes #525 and Tell Magic Compose to use one slot with a capacity, not a row per person #526 in2fd9d88and0c9f1c5; Put a duplicated slot right after the original and open it #568 in5d166f4; and the owner's tophat rounds on Ask before closing a signup, and move Close signup into the ⋯ menu #571 and After Publish, a share panel to send the link #574, with Cut an over-long Magic Compose title or description instead of throwing the draft away #579, inaf65c82,daad50c,84f146eandf95e870.f95e870:pnpm lint && pnpm typecheck && pnpm knip && pnpm test(1675);84f146e:pnpm test:dbfor the touched files,pnpm build && pnpm budgets, and the chromium e2e fororganizer.spec.tsand the publish walkthrough (20);e8150c0: the fullpnpm test:db(562) and the full chromium e2e withCI=1(72).3952591, and code reviews of every round, including the owner's tophat round, are below. Several of them include a running-app check at 1280 and 390. The review summarises the first five rounds. The latest fix (b3316f9, "TBD/TBC" in the label) closes an edge case the latest re-check found; I read it, and it hasn't had a separate check of its own.Budgets
Every signup tab's first load grows by about 1.7 kB gzip, and Build's by 2.7 kB. The tabs carry:
Build also carries the flush that makes Publish wait for its saves, and the code that opens a slot from the check. Each budget goes to the next whole kB above its new size. The owner's tophat round added about 220 B per tab, which still fits.
21eb67c)f95e870)/app/signups/[id]/build/app/signups/[id]/responses/app/signups/[id]/settings/app/signups/[id]/edit,/app/signups/[id]/,/s/[slug], previewDocs
create-and-publish-a-signuphas a note under the Publish step covering Nothing to sign up for yet, Fill it in, Before people see it, Name it first and Publish anyway. Those names are in itsUIlist, and the walkthrough clicks through them. No screenshots change.docs/telemetry.md: thesignup.publish_refusedrow lists:acknowledged: 'placeholder'. A publish with no flag after a warning means the slots were fixed first, or the signup was published through the API or an AI assistant, which don't check.The stack
🤖 Generated with Claude Code
https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H