Skip to content

Email participants when the organizer moves their slot to a new date or time - #567

Merged
richshaw merged 5 commits into
mainfrom
claude/issue-524-slot-change-email
Oct 24, 2026
Merged

richshaw merged 5 commits into
mainfrom
claude/issue-524-slot-change-email

Conversation

@richshaw

@richshaw richshaw commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Fixes #524.

The problem

When an organizer changed a slot's date or time, nobody holding it heard about it until the next reminder, and their confirmation still showed the old time. With reminders off, they never heard at all. The slot editor in the Build tab didn't show how many people held the slot either, so an organizer could move one without knowing anyone was affected.

The fix

  • The email. src/email/templates/slot-changed.tsx, subject Time changed: <signup> · <slot> (slotChangedSubject). It prints every field through SlotDetailsText, the participant's manage link, and an unsubscribe link with one-click List-Unsubscribe headers, as a reminder does.

  • Recording moves. When updateSlot changes a slot's slot_at, it now writes a slot.rescheduled row ({ slotId, from, to }) in the same transaction. That covers the Build tab, group heading renames (one updateSlot per slot) and the update_slot tool. Capacity-only edits and edits that keep the same date write nothing.

  • Sending. src/jobs/slot-changes.ts adds two queues: slot-changes.dispatch (cron every 2 minutes) and slot-changes.send (one job per commitment, retried like reminders). A commitment is due once three things hold:

    • its slot has gone SLOT_CHANGE_SETTLE_MINUTES (10) without another move
    • the time the participant was last told is not where the slot is now
    • it passes the same rules a reminder does

    The send job asks all of that again before it sends and records slot.change_sent with the slotAt it told them. So quick edits send one email for where the slot ended up, a move and a move back send nothing, and a second job finds nothing to do.

  • "Last told". This is toldDifferentTime in src/jobs/notify-rules.ts. It's the newest of the participant's confirmation, reminders and change emails, read from the activity log. A confirmation (or a reminder from before reminders named their time) has no slotAt, so its time is the from of the first slot.rescheduled after it. A reminder.sent with no slotAt therefore counts as "told the time the slot had then", as the issue asks.

  • Shared rules. The reminder scan's rules for who may be emailed at all are now notifiableRules in the same file, and both scans spread them in:

    • active commitment
    • open or closed signup, not deleted
    • dated slot that's still ahead
    • reminders on, participant not opted out, participant has an email

    Turning reminders off or unsubscribing during the 10-minute wait is caught when the job runs (tested). The TypeScript willSendReminder stays as it is: it is the confirmation's promise, not a scan.

  • Reminders stay quiet. changed on a reminder now means "the last email they got was for another time" (wasToldDifferentTime), instead of "an earlier reminder was for another time". After a change email, the reminder for that time reads as a normal "Reminder". The changed reminder's intro now says "since we last emailed you" instead of "after your last reminder", because the last email may have been the confirmation.

  • Organizer warning. SlotEditor shows a note above its fields whenever the slot has sign-ups:

    • Reminders on: "2 people are signed up for this slot. If you change its date or time, we'll email them to let them know."
    • Reminders off: "… Reminders are off, so we won't email them if you change it."

    The issue's wording ("they'll get an email", "they won't be told") was made friendlier at the owner's request in bf22b52. "Reminders on" is state.reminderFieldRef !== null, so it switches as soon as the organizer flips the setting. The note's look came from the reminder-field note in InlineFieldForm. I extracted it as src/components/ui/inline-note.tsx, and both use it.

  • Copy updates. The unsubscribe page says it stops reminders and date or time change emails. The privacy page lists the fourth kind of email ("Last updated" moved to 23 October 2026). The reminders section of "Create and publish a signup" gets a paragraph that says the reminders setting covers these emails, using the imported constant. Also updated: docs/telemetry.md (both new events), the Jobs paragraph in CLAUDE.md, and CHANGELOG.

Decisions the issue didn't make:

  • A scan, not a job queued from the request. The issue outlined enqueuing a delayed job when slot_at changes. The web process never talks to pg-boss today ("the dispatch scan is the scheduler", CLAUDE.md), and the request path would have needed a pg-boss client in both updateSlot callers. Recording the move in the transaction and letting the worker's scan do the debounce gives the same emails, and nothing is lost if the web process dies after the commit. The cost is that emails go 10 to 12 minutes after the last change, not exactly 10.
  • Only updateSlot moves send. rebuildSlotAt also re-times slots, when a time field is deleted or the reminder date field changes. Those aren't the organizer moving a slot, and they write no slot.rescheduled, so they send nothing. If one is followed by a real move, the email compares against the time the participant was last told, so it is still right.
  • 7-day lookback. The scan only looks at moves from the last 7 days. If the worker is down longer than that, those change emails are dropped, and the reminder for the new time still says "Time changed".
  • The editor's count is spots, not people. It uses taken from committedBySlot, as the issue says. Someone who took 2 spots counts as 2 "people".

Tests

  • src/jobs/slot-changes.db.test.ts (new, 17 tests):
    • a move records from and to, and capacity or same-date edits record nothing
    • one email per commitment, only after 10 minutes; a second job sends nothing
    • quick moves send one email, for the final time
    • moved back: nothing
    • a second move after the first email is sent again
    • someone who signed up after the move gets nothing
    • cancelled, moved into the past, reminders off and unsubscribed send nothing
    • turning reminders off, unsubscribing, deleting the signup or moving again while the job waits skips it, with the rule logged
    • a reminder after a change email is a plain "Reminder"
    • a reminder before any change email says "Time changed", and no change email follows it
  • SlotEditor.test.tsx: no note without sign-ups, plus the singular/plural wording with reminders on and off.
  • subjects.test.ts, the worker render script and its test cover the new template.
  • Ran and passed:
    • pnpm lint && pnpm typecheck && pnpm knip && pnpm test (1350 tests, also on bf22b52)
    • pnpm test:db (525 tests)
    • pnpm build && pnpm budgets (builder 151.4 kB of 152)
    • the full Playwright suite with the container config, chromium (60 passed)

Generated by Claude Code

When an organizer changes the date or time of a slot people hold, each
of them now gets one "Time changed" email with the new details, about
10 minutes after the last change. updateSlot records a slot.rescheduled
row with where the slot moved from and to; a worker scan every 2
minutes picks commitments whose slot has sat still for 10 minutes and
whose last email (confirmation, reminder or change email) was for
another time. It follows the reminders setting and the participant's
unsubscribe, sharing those rules with the reminder scan.

A reminder for a time the participant was already told about now reads
as a normal reminder. The slot editor in the Build tab says how many
people are signed up and whether they'll be told about a move.

Fixes #524.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBWyCHhNPaKX2GtNcNGzCF
@richshaw richshaw added pipeline:ux Changes what a person using OpenSignup sees or does pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude
@richshaw
richshaw requested a balanced review from Copilot October 23, 2026 07:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@richshaw richshaw added pipeline:tophat pipeline:working A session is working on it right now and removed pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

Tophat: passed

I ran 882ec37 locally with pnpm dev and pnpm worker, Postgres 16, and Chromium at 1280×900 and 390×844. All the data is made up: one open signup, "Snack duty — Spring season", with a "Fruit and water" slot (Sat, Apr 6 2030, 3 spots) held by sam@example.com, and a "Paper plates" slot (Sun, Apr 7 2030, 2 spots) held by riley@example.com. To skip the 10-minute wait, I moved each slot.rescheduled row's occurred_at back 11 minutes, then let the worker's own 2-minute scan pick it up. (Once I also had to shift the sign-up's own activity and commitments.created_at by the same amount, so the moves stayed after the sign-up. My first backdate put them before it, and nothing was sent, which is correct.)

Check Result What I saw
Editor note, no sign-ups ✅ A fresh slot's editor shows no note (567-head-editor-empty).
Editor note, reminders on ✅ "1 person is signed up for this slot. If you change its date or time, they'll get an email." It's a blue info box above the fields (567-head-editor-reminders-on).
Editor note, reminders off ✅ With sendReminders: false the note reads "1 person is signed up for this slot. Reminders are off, so they won't be told if you change it." (567-head-editor-reminders-off). On a phone at 390 px the note wraps inside the editor and the page is 390 wide, so nothing scrolls sideways (567-head-phone-editor).
Moves are recorded ✅ Each date change through the builder or PATCH /api/slots/:id wrote one slot.rescheduled row with the right from and to.
Quick moves send one email, for the final time ✅ I moved Fruit and water Apr 6 → Apr 13 in the builder, then → Apr 20 a second later. The worker sent one email to Sam, subject "Time changed: Snack duty — Spring season · Fruit and water · Sat, Apr 20". It has What: Fruit and water / Date: Sat, Apr 20, the manage link and the unsubscribe link (567-head-email-phone, rendered from the template with the same data). It recorded slot.change_sent with slotAt 2030-04-20.
No second email ✅ The next three scans (07:40, 07:42, 07:46) sent nothing more for that move.
Moved and moved back ✅ Paper plates Apr 7 → Apr 8 → Apr 7: Riley got nothing.
Reminders off ✅ I turned reminders off and moved Fruit and water to Apr 27. Two scans sent nothing.
Unsubscribed ✅ The unsubscribe page reads "We'll stop sending sam@example.com reminders … and emails when the organizer changes your slot's date or time" (567-head-phone-unsubscribe). After Stop reminders, the done page says the same. A further move to May 4 sent nothing.
Manage link ✅ The email's link opens Sam's slot with the moved date (567-head-phone-manage).
Help and privacy ✅ The Reminders section of /help/create-and-publish-a-signup has the new paragraph ("… about 10 minutes after your last change …"). /privacy lists four kinds of email and reads "Last updated: 23 October 2026".
Console and server logs ✅ No console errors and no worker errors. The dev server printed Node's MaxListenersExceededWarning for the dev logger's stream, which this PR doesn't touch.

Not a failure, for the owner: a move made while reminders are off is not forgotten. I moved the slot to Apr 27 with reminders off, and nothing went out. When I turned reminders back on, the next scan emailed Sam "Time changed … Sat, Apr 27", because what Sam was last told (Apr 20) still differed from the slot. That follows from the "last told vs now" design, and it gives the participant the right time. But the editor note ("they won't be told if you change it") and the help text ("if you turn reminders off, nobody is told when you move a slot") read as if the move will never be told. It only stays untold while reminders are off. Within the 7-day lookback, turning reminders back on sends the email.

For the owner (UX): participants now get a "Your slot has changed" email about 10–12 minutes after the organizer last changes a slot's date or time. Its subject starts "Time changed:". It lists every field, with the manage link and an unsubscribe link that stops these emails along with reminders. In the builder, a slot with sign-ups shows a blue note saying how many people are signed up and whether they'll be emailed. The unsubscribe page, the privacy page and the help article say the change emails exist. See 567-head-editor-reminders-on, 567-head-editor-reminders-off, 567-head-email-phone and 567-head-phone-unsubscribe.

Screenshots are in https://claude.ai/code/session_01GZdAJ7AgwrXMcDs8nmZM1r.


Generated by Claude Code

@richshaw richshaw added pipeline:review pipeline:working A session is working on it right now and removed pipeline:tophat pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude

@richshaw richshaw left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: changes needed (round 1)

The PR does what #524 asks, and the design holds up. updateSlot records a slot.rescheduled row inside the slot's own transaction. A worker scan sends the email once the slot has settled, and the send job asks the same rules again. "Last told" comes from the activity log. The reminder and change-email scans share notifiableRules, so the reminders switch and an unsubscribe silence both. Each "Done when" item is done and has a DB test: one email per commitment, the debounce, a move and move back sending nothing, cancelled, deleted, reminders off, unsubscribed, the switches flipped during the wait, and a reminder after a change email reading as a plain "Reminder". The privacy page, the unsubscribe page, the help article, docs/telemetry.md, CLAUDE.md and CHANGELOG are all updated. One claim isn't fully delivered: a field change that re-times slots is supposed to send nothing, but it does send when the slot moved in the last week.

Blocking (1):

  • src/jobs/slot-changes.ts:45-55: after a move in the last 7 days, a rebuildSlotAt re-time (switching the reminder date field, deleting the time field) passes every rule. Everyone holding the slot gets "moved your slot to a new date or time" although no value changed. Send only when the slot's current slot_at is the to of its newest slot.rescheduled, and add a DB test for it.

Optional (6):

  • src/email/subjects.ts:20-25: slotChangedSubject was inserted between reminderSubject's JSDoc and reminderSubject. The changed comment now sits on the wrong function, and it still gives the old meaning ("follows an earlier one"). Move it back and reword it for "last email was for another time".
  • src/email/templates/reminder.tsx:21 and :72: the prop doc still says reminders are "the only email it silences", and the link reads "Stop reminders for …". The privacy page now says every reminder's link stops both. The change email's own link already reads "Stop reminders and updates for …". The reminder's link could match it.
  • src/jobs/notify-rules.ts:77: TOLD_EVENTS adds slot.change_sent, so the lookup no longer matches the partial index activity_commitment_lookup (src/db/concurrent-indexes.ts), which covers only reminder.sent and commitment.confirmation_sent. It falls back to activity_by_signup_occurred, which is bounded per signup, so this is fine at today's sizes. Worth adding slot.change_sent to that index if the scan ever shows up as slow.
  • src/jobs/worker.ts:36: reminders.send and slot-changes.send have separate pg-boss pollers, so one worker can run a reminder job and a change job for the same commitment at the same moment. Both read toldDifferentTime before either records, and both send. That needs a slot moved to just past the 24h lead, with both scans coming due on the same tick, so it's rare. CLAUDE.md's "one worker, sends one at a time" guarantee no longer quite holds within a single process, though.
  • src/jobs/reminders.ts:36: the reminder scan has no settle period. A slot inside the 24h lead that is edited over several saves can get a "Time changed" reminder for an in-between time, then a change email for the final one. Before this PR, the reminder re-arming for each time already did much the same, so this isn't a regression.
  • From the tophat: a move made while reminders are off is emailed if reminders are turned back on within 7 days. The editor note ("they won't be told if you change it") and the help line ("nobody is told when you move a slot") say less than that. It's the owner's call whether that's the behaviour they want, and whether the wording should say "while reminders are off".

Earlier threads: none. Copilot couldn't review because its quota is used up.
Checks: CI is green on 882ec37 (lint-type-test, e2e, CodeQL). The tophat passed on 882ec37, the current head. I read the full diff against main, rebuildSlotAt / syncAnchorAndSlotAt and the partial index, and ran a high-effort code review whose findings I checked by hand. I dropped one: the editor's count is spots, not people, which the PR description already calls out as the issue's choice. Checklist: the activity writes for moves are inside the slot transaction, the new events are in ACTIVITY_EVENTS and docs/telemetry.md, there's no new column, no migration and no new client route, and the legal page is updated with a new date.

Why owner, once fixed: pipeline:ux. It adds a new participant email ("Time changed: …") and a note in the slot editor, and it changes the unsubscribe page, the privacy page and the help article. See the tophat's 567-head-editor-reminders-on, 567-head-editor-reminders-off, 567-head-email-phone and 567-head-phone-unsubscribe.

https://claude.ai/code/session_01YURJFhAiKJ5eyih7mdpNF4


Generated by Claude Code

Comment thread src/jobs/slot-changes.ts
@richshaw richshaw added pipeline:fix pipeline:working A session is working on it right now and removed pipeline:review pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude
…ed slot

A slot moved in the last week and then re-timed by a field change
(switching the reminder date field, deleting the time field) passed every
due rule, so everyone holding it heard the slot had moved. The scan now
also requires the slot to still be where its newest move put it.

Also puts the reminderSubject comment back on reminderSubject, reworded for
"last emailed a different time", and corrects the reminder template's
unsubscribe prop doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ps7eQtsKz9CUWmHc2qs835

Copy link
Copy Markdown
Owner Author

Here's how I handled the six optional points from the round 1 review, in 3f70bef:

  • subjects.ts comment: taken. The changed JSDoc is back on reminderSubject, reworded for "last emailed a different slot time".
  • reminder.tsx prop doc: taken. It now says the link stops reminders and slot-change emails. I left the link text ("Stop reminders for …") alone, because that's wording participants see and the owner decides it.
  • activity_commitment_lookup index: left as is. The fallback index is bounded per signup, as the review says. Worth revisiting only if the scan shows up as slow.
  • Reminder and change jobs racing in one worker: left as is. It needs both scans due on the same tick, for a slot moved to just past the 24h lead. Making it impossible would need a lock across both queues, which is more than this PR should add.
  • No settle period on the reminder scan: left as is. The review notes it isn't a regression.
  • Reminders turned back on still send the change email: this is the owner's call (behaviour and wording), so I didn't change it.

Generated by Claude Code


Generated by Claude Code

@richshaw richshaw added pipeline:tophat pipeline:working A session is working on it right now and removed pipeline:fix pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

Tophat: passed

I ran 3f70bef locally with pnpm dev and pnpm worker, using Postgres 16 and Chromium at 1280×900 and 390×844. I let the worker's own crons send every change email, with no shortcuts on the 10-minute wait. All the data is made up:

  • "Snack duty — Spring season": a dated slot held by sam@ and riley@example.com.
  • "Bake sale — Spring": two dated slots. Cupcakes is held by alex@. Cookies is held by jordan@, who unsubscribed through the unsubscribe page, and by casey@.
  • "Car wash — Saturday": a Start time field, and slots for tomorrow held by morgan@ and taylor@.
Check Result What I saw
Builder note, reminders on ✅ A slot with 2 sign-ups shows "2 people are signed up for this slot. If you change its date or time, they'll get an email." above its fields (567-head-editor-reminders-on). A 1-sign-up slot shows "1 person is signed up…".
Builder note, no sign-ups ✅ Paper plates has no sign-ups, and it has no note (567-head-editor-no-signups).
Builder note, reminders off ✅ After I turned off sendReminders, the note reads "…Reminders are off, so they won't be told if you change it." (567-head-editor-reminders-off).
Phone ✅ At 390 px the note wraps inside the editor and the page is 390 wide (567-head-phone-editor). No console errors.
What gets recorded ✅ A capacity-only edit wrote slot.updated and no slot.rescheduled. Each date or time move wrote slot.rescheduled with the right from and to.
Quick moves send one email ✅ I moved Fruit and water from Apr 6 to Apr 7 in the builder, then to Apr 8 about 13 s later. Nothing was sent on the 09:00 scan, 2.5 minutes after the last move. On the 09:08 scan, 10.5 minutes after it, Sam and Riley each got one email: "Time changed: Snack duty — Spring season · Fruit and water · Mon, Apr 8". The email has every field, the manage link and the unsubscribe link. Nothing was sent after that.
Move and move back ✅ Cupcakes went to May 3 and back to May 1. Alex got nothing.
Unsubscribed participant ✅ When Cookies moved to May 9, Casey got the email and Jordan, who was unsubscribed, didn't.
Reminders off ✅ With reminders off, I moved Cookies again, to May 10. Casey got nothing on any scan after it settled.
Change email, then nothing more ✅ Taylor's slot moved from 9:00 to 10:00. One "Time changed: … · Dry · Sat, Oct 24 · 10:00 AM" email went out about 11.5 minutes later, and then nothing more.
Reminder, then a move ✅ (see note) Morgan got the 8:00 reminder (I backdated the test sign-up past the 1-hour rule to trigger it). I then moved the slot to 7:00 in the builder. The next reminder scan (09:10, 8 minutes later) sent the reminder's "Time changed" version, which says "…changed your slot for Car wash — Saturday since we last emailed you". The slot-change scan then sent nothing more. That's one email, as the PR says.
Unsubscribe page wording ✅ Before: "We'll stop sending jordan@example.com reminders for Bake sale — Spring, and emails when the organizer changes your slot's date or time…" After: "You won't get any more reminders for Bake sale — Spring, or emails when the organizer changes your slot's date or time…" (567-head-unsubscribe-before, 567-head-unsubscribe-done).
Help and privacy pages ✅ The help article has the new paragraph with "about 10 minutes" (567-head-help). The privacy page says "four kinds of email" and is dated 23 October 2026.
DB tests and logs ✅ pnpm test:db src/jobs/slot-changes.db.test.ts src/jobs/reminders.db.test.ts: 44 passed. That covers "a reminder after a change email is a plain Reminder", which I couldn't reach live without waiting about 40 minutes for Taylor's slot to come within 24 h. The worker log shows no errors or warnings, and the browser console showed no errors.

Notes, not failures:

  • Slots less than 24 h away. When a slot already less than 24 hours away is moved, the 10-minute reminder scan can get there before the change email's 10-minute settle. Morgan's moved slot was told by a "Time changed" reminder 8 minutes after the move, not by the change email. Only one email went out, so it works. But for slots that close, quick successive moves aren't held back the way change emails are. That's how reminders behaved before this PR too.
  • Who the note counts. The note counts spots taken, as the PR says. It counts people who unsubscribed or gave no email too: Cookies said "2 people… they'll get an email" while Jordan was unsubscribed.

For the owner (UX): participants now get a "Time changed: · " email about 10 minutes after the organizer last moves their slot's date or time. It says "Your slot has changed", lists every field, and has the manage and unsubscribe links. In the builder, a slot with sign-ups shows a blue note saying how many people are signed up and whether they'll be emailed if it moves. A changed-time reminder now says "since we last emailed you". The unsubscribe page, the help article and the privacy page say the reminders switch and the unsubscribe link cover these emails too. See 567-head-editor-reminders-on, 567-head-editor-reminders-off, 567-head-phone-editor and the two unsubscribe screenshots.

Screenshots are in https://claude.ai/code/session_011Tt4FkAMnhMJwsj4djzvdH.


Generated by Claude Code

@richshaw richshaw added pipeline:review pipeline:working A session is working on it right now and removed pipeline:tophat pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude

@richshaw richshaw left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: passed (round 2)

Since round 1 (at 882ec37), 3f70bef fixes the one blocking finding. It adds a due rule, 'slot re-timed by a field change', to slotChangeDueRules in src/jobs/slot-changes.ts: the newest slot.rescheduled for the slot must have a to equal to slots.slot_at, cut to milliseconds. Because the scan and the send job both use the same rule set, a rebuildSlotAt re-time after a recent move now sends nothing, and skipReasons logs it by name. to is written with toISOString() in updateSlot, so the comparison is exact at millisecond precision, and a move to an undated slot (to null) fails the rule, which is right. The new DB test moves the slot, lets the change email go out, then switches reminderFromFieldRef to a second date field through updateSignup. It checks that slot_at moved and that the scan selects nothing. The reminder-lead helper now shifts the moves' to along with slot_at, so the "no change email follows" tests still pass for the right reason. The round 1 optionals on subjects.ts and reminder.tsx were taken. The rest were answered on the PR. The rest of the diff is what round 1 reviewed.

Blocking (0): none.
Optional (2):

  • src/jobs/slot-changes.ts:62: the newest move is picked by occurred_at, which defaults to now(), so it's the transaction's start time. If two saves of the same slot overlap, the later commit can carry the earlier occurred_at. Then the "newest" move's to isn't where the slot ended, and that slot's change email is held until its next move. Slot updates lock the row, so this needs two saves of one slot within milliseconds of each other. Leave it as is.
  • src/jobs/slot-changes.ts:36: one line of the reworded JSDoc runs to about 110 characters. Prettier doesn't reflow comments. Rewrap it if the file is touched again.

Earlier threads: all resolved (the re-time thread, answered and fixed in 3f70bef). Copilot couldn't review because its quota is used up.
Checks: CI is green on 3f70bef (lint-type-test, e2e, CodeQL). The tophat passed on 3f70bef, the current head, and it let the worker's own crons send every email. I read the diff since round 1, moveOfThisSlot, the slot.rescheduled write in updateSlot, and the activity schema's occurred_at default.

Why owner: pipeline:ux. Participants get a new "Time changed: · " email about 10 minutes after the organizer last moves their slot's date or time. The slot editor shows a note when a slot has sign-ups. The unsubscribe page, the privacy page (dated 23 October 2026) and the help article mention the change emails. A changed-time reminder now says "since we last emailed you". One open question is yours, from both tophats: a move made while reminders are off is emailed if reminders are turned back on within 7 days. Do you want that, and should the editor note ("they won't be told if you change it") and the help line say "while reminders are off"? See the latest tophat's 567-head-editor-reminders-on, 567-head-editor-reminders-off, 567-head-phone-editor and the two unsubscribe screenshots, plus the first tophat's 567-head-email-phone.

https://claude.ai/code/session_01Bk2pWM6VExQLT1HFZyC4d4


Generated by Claude Code

@richshaw richshaw added pipeline:owner Passed; waits for the owner (UX or high-risk) and removed pipeline:review pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude
"If you change its date or time, they'll get an email" now reads "we'll
email them to let them know", and with reminders off "we won't email
them if you change it", rather than "they won't be told".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DP1nWWH41t7Xjtf862UmoN

Copy link
Copy Markdown
Owner Author

Tophat: passed

Ran bf22b52 locally with pnpm dev and the worker, Postgres 16, Chromium at 1280×900 and 390×844. The only change since the last tophat (3f70bef) is the wording of the slot editor's note, so that's what I checked. All the data is made up: one open signup, "Snack duty — Spring season", with a dated "Fruit and water" slot (3 spots) held by sam@example.com and riley@example.com.

Check Result What I saw
Note, reminders on (desktop) ✅ "2 people are signed up for this slot. If you change its date or time, we'll email them to let them know." above the slot's fields.
Note, reminders off (desktop) ✅ After clearing Send a reminder email before this date and saving (sendReminders false in the database): "2 people are signed up for this slot. Reminders are off, so we won't email them if you change it."
Phone ✅ At 390 px both versions wrap inside the editor, and the page is 390 wide, so nothing scrolls sideways.
Switching the setting ✅ Turning reminders off and back on saved each time, and the note followed without a reload.
Console ✅ No console errors.

Not re-checked: the change email itself and everything else in the PR, which this commit doesn't touch. The 3f70bef tophat covers those. The one-person wording is covered by SlotEditor.test.tsx.

For the owner (UX): the note now says what we'll do ("we'll email them to let them know", "we won't email them") rather than what happens to them ("they'll get an email", "they won't be told"). You asked for this wording and approved it in the session below.

Screenshots are in https://claude.ai/code/session_01DP1nWWH41t7Xjtf862UmoN.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Approved by the owner at bf22b52.

The owner approved this code in a Claude Code session after asking for the friendlier note wording in bf22b52, and asked for the PR to go to pipeline:merge. The review marker is still on 3f70bef, so the pipeline's review of this commit comes first.


Generated by Claude Code

@richshaw richshaw added pipeline:merge and removed pipeline:owner Passed; waits for the owner (UX or high-risk) labels Oct 23, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

Not merged yet. Sent back to pipeline:review.

Gates that failed at bf22b52:

  • Review is stale. The latest review marker passed 3f70bef, and bf22b52 (the note wording) is a commit of the PR's own after it. It needs a review on the current code.
  • Branch was behind main. I've merged main into it, so CI will run again on the new head.

Every other gate passed. CI was green on bf22b52, the tophat passed bf22b52, all threads are resolved, and the owner's approval of bf22b52 is recorded above. That approval carries over the merge from main. So if the review passes, the PR can go straight to pipeline:merge.

https://claude.ai/code/session_01PtqmJDYFWZ87GsyZe2Ay2u


Generated by Claude Code

@richshaw richshaw added the pipeline:working A session is working on it right now label Oct 23, 2026 — with Claude

@richshaw richshaw left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: passed (round 3)

Round 2 passed 3f70bef. Since then the PR has one commit of its own, bf22b52, plus a merge from main (ead50d6) that resolved no conflicts (git show --cc lists no files). bf22b52 only rewords the slot editor's sign-up note, as the owner asked: "If you change its date or time, we'll email them to let them know." with reminders on, and "Reminders are off, so we won't email them if you change it." with reminders off. Both strings in SlotEditor.tsx and their two SlotEditor.test.tsx assertions changed together. I searched src, tests, docs, the skills and CHANGELOG for the old wording ("they'll get an email", "they won't be told"), and nothing is left. The CHANGELOG line ("whether they'll be told if it moves") still describes the note correctly. The rest of the diff is what rounds 1 and 2 reviewed.

Blocking (0): none.
Optional (0): none new. Round 2's two optional notes still stand. So does the owner question from both tophats: a move made while reminders are off is emailed if reminders come back on within 7 days, and the help line create-and-publish-a-signup.tsx:205 ("nobody is told when you move a slot") says less than that. The owner approved bf22b52 with that behaviour in place.

Earlier threads: all resolved.
Checks: CI is green on ead50d6 (lint-type-test, e2e, CodeQL). The tophat passed on bf22b52, and no runtime code has changed since (the shared check prints 0). I read the diff since 3f70bef and checked the merge commit's resolution.

Why owner, and why it goes to merge: pipeline:ux (a new participant email, the editor note, and changes to the unsubscribe, privacy and help pages). The owner's approval at bf22b52 (recorded above) covers this code, since the PR has no commits of its own after it, only a merge from main. It carries over, so this goes to pipeline:merge. Note for the merge stage: #565 has just landed and also touches SlotEditor.tsx. A test merge comes out clean, but the branch is behind main, so it will need an update and a fresh CI run.

https://claude.ai/code/session_01QT55BacbKh9sMx6AUmN8bU


Generated by Claude Code

@richshaw richshaw added pipeline:merge and removed pipeline:review pipeline:working A session is working on it right now labels Oct 23, 2026 — with Claude
@richshaw
richshaw merged commit 4c86b7f into main Oct 24, 2026
5 checks passed
@richshaw
richshaw deleted the claude/issue-524-slot-change-email branch October 24, 2026 00:37
@richshaw richshaw removed pipeline:merge pipeline:ux Changes what a person using OpenSignup sees or does labels Oct 24, 2026 — with Claude
richshaw pushed a commit that referenced this pull request Oct 24, 2026
Resolves conflicts with #565 and #567 in SlotEditor, WysiwygSlot, the
SlotEditor tests and CHANGELOG by keeping both sides: the editor takes
focusOnOpen/onFocused alongside the new taken/remindersOn props, and the
focus ref sits on the field grid below the new signed-up note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pfDqwVMpLjQtmFKGF5jBa
richshaw pushed a commit that referenced this pull request Oct 24, 2026
Resolves the CHANGELOG conflict with #567 by keeping both Added entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTuDgfKhQSncqThSL226TC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Participants aren't told when the organizer moves their slot to a new date or time

3 participants