Repository navigation
Let organizers make the participant email optional from the Settings tab - #389
Conversation
Moving a commitment to another slot cancelled it and signed up again through commitToSlot with the participant's email, which looked the participant up by that email. A following change makes the email optional, and without one the move would add a second participant row (and a participant.created event) each time. The move now commits the participant it already has, through an internal commitParticipantToSlot that commitToSlot also calls after parsing its input. The move's name, notes and quantity are still checked by the sign-up rules. The sign-up request sends the same statements as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Groundwork for #378, which lets organizers make the participant email optional. Nothing writes a participant without an email yet, so nothing users see changes. This makes the column nullable and every reader of it ready for the null, so the next PR only has to allow it. participants.email and email_lower may both be null, and a check keeps them null together. The unique index on (signup_id, email_lower) stays as it is: NULLs are distinct there, so many such participants can share a signup. Such a participant gets no confirmation email and no reminder (the scan skips them and the send job checks again), their edit page leaves the address out, and the Responses tab shows a dash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Review fixes for the slot move. The move took its quantity and notes from the read made before its transaction, so a change from another tab could be undone. It now takes them from the row its cancel locks, unless the request sends its own. A name the move sends is checked before anything is written, and the error is returned rather than thrown. A name it leaves out is not checked or written back, so a blank stored name can still move and a rename made meanwhile is kept. The participant update now runs once for an existing participant, by either path, and only matches a participant in the slot's signup and workspace. Anything else is refused as forbidden, as a move to another signup's slot is. The sign-up request sends the same statements as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Brings in the move fixes: quantity and notes from the locked row, the name checked before the transaction and written only when sent, and the participant update scoped to the slot's signup and workspace. A participant without an email still moves, through the same scoped update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
The participants table already takes a null email (#381). This makes it real: the signup's existing requireEmail setting decides whether the sign-up form asks for one. With it off, a blank email adds a new participant with no email and the confirmation screen says the link will not be emailed. With it on, a blank email is refused before anything is written. A move never checks it, so people who signed up without an email keep their spots if the organizer turns it back on. The AI connector can now read and set requireEmail, and the privacy and cookies pages say what happens without an email. The Settings tab switch comes next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Review fixes for the participant email groundwork. A participant without an email gets no confirmation of a move, and the move's response and cookie did not carry the new commitment either, so they lost their way back to it. A move now returns the new edit token, edit link and links as signing up does, and swaps the new commitment into the returning- participant cookie for the old one. No email now means exactly null: the check also refuses a blank email or email_lower, and the send guards test for null as the reminder scan does. Inserts take both columns from participantEmailColumns, whose type keeps the pair together. The migration adds the check NOT VALID and then validates it. Also: the reminder scan selects the email column without a cast, the dev reminder script and the telemetry doc say a participant without an email gets nothing, the Responses tab's dash reads "No email" to a screen reader, and the changelog says GET /api/commitments/[id] can return a null participantEmail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Brings in the stricter participant email check (no email means null, never blank), participantEmailColumns, the move's new edit link and cookie, and the null guards on sends. The sign-up insert now takes its email columns from participantEmailColumns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
The signup's requireEmail setting could only be changed through the AI connector or the API. The Settings tab now has a "Sign-up form" section with "Ask for an email address": Required (the default) or Optional, saved by a plain server form so the tab ships no new script. It sends only requireEmail, which updateSignup merges over the stored settings, and a viewer's refusal shows as the tab's error. The "link isn't working" page no longer tells everyone to try the link in their confirmation email, since someone who signed up without an email has none. The help article explains the setting, and its walkthrough clicks through it and takes the new picture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Review fixes for signing up without an email. - null in the email field now means no email, as blank and missing do. - The sign-up form no longer fills a remembered email into an optional box, and a blank sign-up keeps the remembered email instead of wiping it. - If the organizer makes the email required after the page loaded, the refused blank email turns the box required, label and help text included. - update_signup no longer advertises defaults for its settings, so a client that fills them in cannot make the email required again by accident. - The success screen said the link was the only way back, but the browser remembers it too. It now says so, in one paragraph for both cases. - The privacy page says the organizer decides whether the email is required, and the help pages no longer say everyone gives an email and gets one; the AI assistant page lists the setting it can now change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
richshaw
left a comment
There was a problem hiding this comment.
Review summary
I reviewed only this PR's own commit (7c86ffa), diffed against its merge-base with #385's branch (e9bf7b9). The code-review pass posted 14 inline comments. I checked each one against the code: 10 hold up and are ranked below, and I withdrew 4 in place (reasons at the end). I found no blocking problems. The policy guard still judges the save (updateSignup calls requireWorkspaceWrite), the action sends only requireEmail, and the screenshot is exactly twice its stated size. src/help/articles.test.tsx and gone-message.test.ts pass at this head (26 tests).
Findings, ranked
Medium
settings/page.tsx:83: The on-screen help says "If they lose it, you can remove them on the Responses tab". On this branch the Responses tab has no remove action. That comes from #383, which is still open and is stacked on a different branch (claude/issue-375-cancelled-edit-link). If this PR merges first, the UI promises a way out that doesn't exist. The PR body already keeps Remove out of the help page for this reason, but not out of the Settings text.
Low
2. create-and-publish-a-signup.tsx:191 (posted on line 178, because 191 isn't in the diff): The Reminders paragraph still says "If a slot has a date, people who take it get a reminder email". The new section says the opposite for anyone who left the email blank.
3. gone-message.ts:36: In "If you got a confirmation email, try its link again, or contact the organizer…", the "contact the organizer" part can be read as falling under the "if". Someone with no email then has no clear next step.
4. settings/page.tsx:93: "Saved" comes from ?saved=1 and is never cleared. It stays next to a radio that was changed but not saved, and comes back on reload. ?published=1 and ?disconnected=1 work the same way, so this is minor.
5. tests/e2e/organizer.spec.ts:101: The PR body and CHANGELOG say a viewer is refused and sees the tab's error, but no test runs the new ServiceException catch.
Nits
6. CHANGELOG.md:21: The CHANGELOG still says "Settings keeps only the danger zone".
7. actions.ts:45: The tab's error shows raw service messages, such as "your role cannot modify this workspace". deleteSignupAction and Connected apps already do the same, so this is polish, not a regression. The comment's "legacy groupByFieldRefs with two entries" case is speculative: the schema has always capped that list at one.
8. actions.ts:47: Only this action catches the guard's throw. publishAction and deleteSignupAction still send a viewer to the error page. A shared helper would cover all of them. This gap was there before the PR.
9. settings/page.tsx:94: This is another one-off "Saved" notice (CLAUDE.md asks to reuse banners and notices). SaveStatus is a client component, so reusing it as it is would add script to this page. Extracting a small server-safe notice is the option that keeps the page script-free.
10. tests/e2e/organizer.spec.ts:107: The post() helper and the create, add-slot and publish setup are duplicated in the help walkthrough. Inline setup is already the usual pattern across the e2e specs.
Withdrawn after verification (edited in place)
settings/page.tsx:53, refused save keeps the refused radio: This doesn't happen. In Next 15.5, aredirect()from an action rejects with a redirect error, and the segment'sRedirectBoundaryremounts the page subtree, so the radios come back with the stored value. Without JavaScript it's a full page load, with the same result.settings/page.tsx:53, key remount drops focus: Wrong cause. TheRedirectBoundaryremounts the page on every redirect, not the key, so the suggested fix wouldn't change anything.actions.ts:40, stale tab overwrites: Not a bug. The form sends only the one setting it owns, which is what #314 protects. An explicit Save of the value on screen should win.actions.ts:51,if (refused)truthiness: No reachable error has an empty message.fromZodErrorfalls back to'invalid input'.
Generated by Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Brings in #382, #386 and #388 (edit-token ops metered and read in one place). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
…tice
The Settings tab showed the service's own message when a save was
refused ("your role cannot modify this workspace"). It now maps the
error code to the same words the Build tab uses, which move with the
Build tab's "Saved" look into one server-safe module, so the Settings
"Saved" is no longer a third style and the page ships no new script. A
database test runs the action as a viewer: the refusal comes back in
those words and nothing is written.
The form spaces its parts with a gap rather than sibling margins: the
server's render starts it with React's hidden action inputs and a client
render doesn't, so the fieldset moved 4px after a save. The help step
names the signup's Settings tab, since a phone also shows an account
Settings link at the top.
The "link isn't working" page now sends everyone to the organizer, and
only those who got one to their confirmation email. The two e2e specs
that set up a published signup share one helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
…slot Fixes from the whole-stack review, for someone who signs up without an email. - The success screen offers Copy link in place of Share link. On a phone, Share link opened the share sheet with the private link that can cancel the spot. Copy link says "Link copied." in a live region, or says to copy the link by hand when the browser won't. With an email, nothing changes. - The optional email box's help no longer says "optional" a second time. It says what an email brings, the link and a reminder (only where that slot will get one, by the rule the confirmation email uses), and to save the link without one. - The os_commit cookie remembers each sign-up until a week after its slot, and never less than 60 days, up to the browser's 400. Someone who signed up early for a slot more than 60 days away lost their way back. Each entry carries its own expiry; old entries still read, and get 60 days on the next rewrite, which also drops expired ones. It holds 36 entries, down from 40, to stay under 4096 bytes. The slot's instant comes back with the commit and the move, from the row already locked, so no new statement. - The privacy page no longer says the link is shown only once; the cookies page gives the new lifetime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Brings in #385's review fixes (Copy link, the optional email's help, the cookie that lasts until after the slot) and main's #388 change to /api/commitments/[id]. The one conflict was the CHANGELOG's #378 entry: kept #389's Settings tab wording and took #385's new sentences about the help text and Copy link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Fixes from the whole-stack review.
- The Settings tab's help said people without an email see their link
"only once", and offered only removing them. It now says they see it on
screen and only that browser remembers it, and that if they lose it they
can sign up again while the organizer removes the old sign-up.
- The help page says the same, and says to remove the old sign-up first
when the slot is full. Its walkthrough opens the Responses tab, and the
Settings screenshot is re-taken for the longer help text.
- A viewer was told "You no longer have edit access", though they never had
it. Both tabs now say "You don't have edit access."
- Delete on the Settings tab showed the service's own message ("signup not
found") while Save on the same tab used plain words. It maps the refusal
by its code the same way. A viewer's Delete still throws; that is #390.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Once #383 lands, each Responses row also has a Remove cell named "Remove Sam Example from …", so a partial match on Sam Example finds two cells and the walkthrough fails in strict mode. Found by running the whole stack combined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
The line under "Ask for an email address" described leaving the email blank even while Required was selected. Each choice now has its own shorter line: Required says everyone is emailed their link, Optional says what leaving it blank means and what to do about a lost link. CSS shows the line for the radio picked, so it follows the choice before a save with no script, and each radio is described by its own line for screen readers. The help screenshot is re-taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
A participant without an email showed a dash, with "No email" for screen readers only. A bare dash means nothing without its column heading, and a row laid out for a phone has none, so the cell now says "No email" to everyone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
A fact-check and a style review of the help page found: - Losing a link said to remove the old "sign-up" after they sign up again, then that a full slot needs the removal first. It now says the full-slot case first, and that the old one is higher up the list, which is oldest first. "Sign-up" next to "signup" read as one word spelt two ways; the participant pages already say "spot". - "Email (optional)" changes what everyone sees, so it now comes before Save rather than after it. - On a phone, the header's Settings link comes before the tab, so the step warns about it first. - "Email" meant both what people type and the message. Passages this stack wrote now say "email address" for what people type, and say everyone sees their link on screen, not only people without an email. docs/writing-help.md adds "sign-up" to the words not to use for a spot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Main now has the rest of the stack. Merged against #385's last head, since main has #385 squashed, which leaves two conflicts, both with #383: - actions.ts keeps this branch's setRequireEmailAction and #383's removeCommitmentAction, with both sets of imports; - the Settings page imports both setRequireEmailAction and FormError. The tree is the combined one the whole stack was tested on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Fixes #378. Part of the work for #347.
Stacked on #385. Must merge after #383 too: the Settings help text and the help page point to removing someone on the Responses tab, which #383 adds. The full order is #380 → #383 → #379 → #381 → #385 → #389.
The problem
requireEmailsetting could only be changed through the AI connector or the API. Organizers had no way to make the email optional themselves.The fix
group-has-[…:checked]), so it changes as soon as the radio does, before a save, with no script. Each radio is described by its own line for screen readers.requireEmail, whichupdateSignupmerges over the stored settings.src/components/ui/save-notice.tsx). Settings reuses them instead of adding a third style.docs/writing-help.md: "sign-up" joins the words not to use for a spot. Beside "signup" it reads as the same word spelt two ways, and the participant pages now say "spot" (Show that a sign-up was cancelled when its edit link is opened again #380).Tests
actions.db.test.tsruns the actions against Postgres:SaveStatus.test.tsx: the Build tab tells a refused viewer "You don’t have edit access."organizer.spec.ts:createPublishedSignuphelper.gone-message.test.tschecks the new wording.pnpm lint && pnpm typecheck && pnpm knip && pnpm test(1088)pnpm test:db(401)pnpm build && pnpm budgets(the Settings page is 123.6 kB of 125;/s/[slug]is 130.2 kB of 131)Left out on purpose
?published=1and?disconnected=1elsewhere.main, and another help page and three tests use the name, so it's a separate change.🤖 Generated with Claude Code
https://claude.ai/code/session_0159b8Nfy3w7F8BVwvn37Fc3
Generated by Claude Code