Skip to content

Show slots inside the sign-up lockout as closed on the public page - #543

Merged
richshaw merged 4 commits into
mainfrom
claude/issue-513-lockout-public-page
Oct 22, 2026
Merged

richshaw merged 4 commits into
mainfrom
claude/issue-513-lockout-public-page

Conversation

@richshaw

Copy link
Copy Markdown
Owner

Fixes #513.

The problem

When a signup sets lockoutHoursBeforeSlot, the service refuses any sign-up for a slot that starts within that many hours. The public page /s/[slug] didn't apply the same rule, so it still showed the slot with a working Sign up button and an "N left" count. Every Confirm was refused with "Sign-ups for this slot have closed. Reload the page to see what's still open.", and reloading showed the same page again. No screen sets the lockout, but PATCH /api/signups/[id] and the MCP update_signup tool do.

The fix

  • slotInLockout(settings, slotAt, now) is new in src/lib/signup-closing.ts, next to signupShutReason. It holds the lockout rule, unchanged: counted back from slot_at, so a date-only slot still uses its noon-UTC anchor, and it's off when the lockout is unset or 0 or the slot has no date.
  • whyNotTakingPlaces in src/services/commitments.ts now calls it instead of computing the lockout inline. The refusal, its detail: 'slot_lockout' and its message are unchanged.
  • toSignupViewSlots (src/app/s/[slug]/public-signup.ts) gives a slot inside the lockout the status closed. It takes an optional now for tests. The row then renders like any other closed slot: "Closed", with no count and no button. I picked this over adding a lockedOut flag because the row already has a closed path and the page needs nothing new.
  • Because the organizer's preview and GET /api/public/signups/[slug] use the same mapper, they also show such a slot as closed. That's what a participant would actually find.
  • The timezone question (Reminders, the sign-up lockout and the CSV export treat a slot's local time as UTC #319) is not touched.

Tests

  • signup-view.test.tsx builds a signup with lockoutHoursBeforeSlot: 24, with one slot 2 hours away and one 72 hours away, through toSignupViewSlots. The near slot shows Closed, with no Sign up button and no count, and the far slot still offers an enabled Sign up. Without the fix, this test fails because the near slot's button is still there. A second test checks that no slot closes when the lockout isn't set.
  • signup-closing.test.ts covers slotInLockout: inside the window, past the slot, outside the window, and no lockout, a 0 or null lockout, or no date.
  • These passed in this session: pnpm lint, pnpm typecheck, pnpm knip, pnpm test (1292 tests) and pnpm test:db (492 tests, including the existing lockout refusal in commitments.db.test.ts). affected-articles.mjs reports that no help article is touched.

Generated by Claude Code

The service refused a sign-up for a slot starting within the signup's
lockoutHoursBeforeSlot, but /s/[slug] still offered it, so every Confirm
was refused and reloading showed the same page. The lockout test moves to
slotInLockout in src/lib/signup-closing.ts; whyNotTakingPlaces and
toSignupViewSlots both call it, so the page shows such a slot as Closed.

Fixes #513.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZTvpEXM4vaLSndhJ7emU7
@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 21, 2026 — with Claude
@richshaw
richshaw requested a balanced review from Copilot October 21, 2026 13:00

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 21, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

Tophat: passed

I ran aa21e9e locally with pnpm dev, Postgres 16, and Chromium at 1280×900 and 390×844. I didn't run the worker, because nothing here touches jobs or email. I made a published signup with two date-only slots: "Fruit and water" on the day after the run, whose noon-UTC anchor was about 22.5 hours away, and "Juice boxes" nine days later. Through PATCH /api/signups/[id] I set lockoutHoursBeforeSlot: 24.

Check Result What I saw
The bug on the base (8600bed) ✅ reproduced Both slots showed Sign up and "2 left". I signed up for Fruit and water and got a 409, with "No one new can sign up for this slot now. Reload the page to see what's still open." in the dialog (543-before-public-desktop, 543-before-refused-desktop). The public API listed both slots as open.
Public page, desktop ✅ Fruit and water shows "Closed", with no count and no button. Juice boxes still shows "2 left" and Sign up (543-after-public-desktop).
The far slot still signs up ✅ I signed up for Juice boxes and saw "saved your spot" (543-after-far-slot-confirmed-desktop).
Phone ✅ The same rows at 390 px: Closed, then "1 left" with Sign up. scrollWidth is 390, so the page has no sideways scroll (543-after-public-phone).
Organizer preview and public API ✅ /app/signups/[id]/preview shows Fruit and water as Closed (543-after-organizer-preview-desktop). GET /api/public/signups/[slug] now gives that slot "status":"closed" and the other "open".
Lockout turned off ✅ With lockoutHoursBeforeSlot: 0, both slots show their count and Sign up again, and the API lists both as open (543-after-no-lockout-desktop).
Console and server logs ✅ The PR head shows no browser console errors. The only console error was the expected 409 on the base. The only server errors come from the health check's pgboss query, because the worker wasn't running.

One thing I noticed that this PR didn't cause: the organizer preview showed Juice boxes as "2 left" after the sign-up, while the public page showed "1 left". The preview doesn't seem to count commitments on main either. The PR description gives the refusal text as "Sign-ups for this slot have closed…", but the dialog on main actually says "No one new can sign up for this slot now." That doesn't change the fix.

For the owner (UX): when a signup has a sign-up lockout (set through the API or an AI assistant, since no screen sets it), a slot that starts within the lockout now shows Closed on the public page and in the preview. Before, it offered Sign up and then refused every attempt. Slots outside the window are unchanged. Compare 543-before-public-desktop / 543-before-refused-desktop (main) with 543-after-public-desktop and 543-after-public-phone.

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


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 21, 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 1)

The PR moves the lockout rule out of whyNotTakingPlaces and into slotInLockout in src/lib/signup-closing.ts. It keeps the same slot_at anchor and the same "off when unset, 0 or no date" behaviour. The service calls the helper, and so does toSignupViewSlots, so a slot inside the lockout comes out closed from the one mapper that feeds the live page, the organizer preview and GET /api/public/signups/[slug]. Every item in the issue's outline and "Done when" is done. The new signup-view.test.tsx case fails without the fix. The refusal's detail: 'slot_lockout' and message haven't changed. /s/[slug] is dynamic (it reads cookies()), so the Date.now() default runs per request, not once at build. A returning participant whose slot is now closed still gets the "No one new can sign up, but your spot is still yours" line and their edit link.

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

  • CHANGELOG.md:57: the entry gives the old refusal as "Sign-ups for this slot have closed. Reload the page to see what's still open." That string isn't in the code. The dialog says "No one new can sign up for this slot now." (src/app/s/[slug]/closed-message.ts:18), as the tophat saw. Worth correcting if the PR gets another push.

Earlier threads: none. Copilot couldn't review because its quota was used up.
Checks: CI is green on aa21e9e (lint-type-test, e2e, CodeQL). The tophat passed on this same SHA, so no runtime code has changed since. I read the diff, every caller of toSignupViewSlots and the remaining uses of lockoutHoursBeforeSlot. I didn't need to run anything locally to prove a finding.

Why owner: this is UX (pipeline:ux). When a signup has a sign-up lockout, a slot inside it now shows Closed on the public page and in the preview, and the public API gives it "status":"closed", which self-hosters reading that API will notice. Compare the tophat's 543-before-public-desktop / 543-before-refused-desktop with 543-after-public-desktop and 543-after-public-phone.

https://claude.ai/code/session_01FwwHciGGPzwcq6fj32dXNe


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 21, 2026 — with Claude
@richshaw

Copy link
Copy Markdown
Owner Author

This need fixing "CHANGELOG.md:57: the entry gives the old refusal as "Sign-ups for this slot have closed. Reload the page to see what's still open." That string isn't in the code. The dialog says "No one new can sign up for this slot now." (src/app/s/[slug]/closed-message.ts:18), as the tophat saw. Worth correcting if the PR gets another push."

@richshaw richshaw added pipeline:fix and removed pipeline:owner Passed; waits for the owner (UX or high-risk) labels Oct 21, 2026
@richshaw richshaw added the pipeline:working A session is working on it right now label Oct 21, 2026 — with Claude
…GELOG entry

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

Copy link
Copy Markdown
Owner Author

Confirmed and fixed in 60aa9bb.

  • The CHANGELOG entry now quotes the refusal the dialog really shows: "No one new can sign up for this slot now. Reload the page to see what’s still open." (CLOSED.join in src/app/s/[slug]/closed-message.ts).
  • 8289810 merges main in. The only conflict was CHANGELOG.md, and all the entries are kept, with this PR's first. src/services/commitments.ts merged without a conflict.

Checked on 60aa9bb: pnpm lint, pnpm typecheck, pnpm knip and pnpm test (1309) passed, and so did pnpm test:db (494). No runtime code of the PR's own changed, so once CI is green this goes back to review rather than tophat.

https://claude.ai/code/session_0115MCPHsyBah5fBHmYV2aVR


Generated by Claude Code

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

Copy link
Copy Markdown
Owner Author

Merged main into the branch in 1ef58e3. It had started conflicting after #547 and the PRs before it landed. The only conflict was in CHANGELOG.md, where #542's entry and this PR's entry landed on the same line. I kept both, this PR's first. No runtime code changed.

  • Ran in this session on 1ef58e3: pnpm lint && pnpm typecheck && pnpm knip && pnpm test (1316 passed) and pnpm test:db (all 34 files, 501 tests).
  • Once CI is green this goes back to review, not tophat. The PR has a commit of its own since the review (60aa9bb, the CHANGELOG wording fix), but nothing in runtime code has changed since the tophat passed.

https://claude.ai/code/session_014u7aKHNDyvxAhDHCQui3eJ


Generated by Claude Code

@richshaw richshaw added pipeline:review 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 22, 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 the PR has one commit of its own, 60aa9bb. It changes the CHANGELOG entry to quote the refusal the dialog really shows ("No one new can sign up for this slot now. Reload the page to see what’s still open.", CLOSED.join in src/app/s/[slug]/closed-message.ts), which is what the owner asked for. The rest is two merges of main whose only conflicts were in CHANGELOG.md. The runtime code is the same as in round 1. slotInLockout in src/lib/signup-closing.ts holds the lockout rule, with the same slot_at anchor and off when the lockout is unset, 0 or the slot has no date. whyNotTakingPlaces and toSignupViewSlots both call it, and toSignupViewSlots is the one mapper behind the live page, the organizer preview and GET /api/public/signups/[slug]. I read the full diff again against current main. toSignupViewSlots still has only those two callers, and neither caches across requests, so the Date.now() default is evaluated on each request.

Blocking (0): none.
Optional (0): none.
Earlier threads: none open. The owner's CHANGELOG comment is addressed in 60aa9bb.
Checks: CI is green on 1ef58e3 (lint-type-test, e2e, CodeQL). The tophat passed on aa21e9e. The shared check for runtime changes since then prints 0. I didn't need to run anything locally to prove a finding.

Why owner: pipeline:ux. When a signup has a sign-up lockout, a slot inside it now shows Closed on the public page and in the preview, and the public API gives it "status":"closed". Compare the tophat's 543-before-public-desktop / 543-before-refused-desktop with 543-after-public-desktop and 543-after-public-phone.

https://claude.ai/code/session_01JzPKsKUTWrw9xQT1XzxEK9


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 22, 2026 — with Claude
@richshaw richshaw added pipeline:merge and removed pipeline:owner Passed; waits for the owner (UX or high-risk) labels Oct 22, 2026
@richshaw richshaw added the pipeline:working A session is working on it right now label Oct 22, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

Approved by the owner at 1ef58e3.

https://claude.ai/code/session_01UzuZgVYSekfFbYc7jjF3i7


Generated by Claude Code

@richshaw
richshaw merged commit a0b3f49 into main Oct 22, 2026
5 checks passed
@richshaw
richshaw deleted the claude/issue-513-lockout-public-page branch October 22, 2026 17:26
@richshaw richshaw removed pipeline:merge pipeline:ux Changes what a person using OpenSignup sees or does pipeline:working A session is working on it right now labels Oct 22, 2026 — with Claude
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.

A slot inside the sign-up lockout still shows "Sign up" on the public page, and every try is refused

3 participants