Skip to content

Give every slot the same number of people from the draft review page - #537

Closed
richshaw wants to merge 4 commits into
claude/issue-504-remove-dayfrom
claude/issue-504-people-each
Closed

richshaw wants to merge 4 commits into
claude/issue-504-remove-dayfrom
claude/issue-504-people-each

Conversation

@richshaw

Copy link
Copy Markdown
Owner

Fixes #504 (part 3 of 3; parts 1 and 2 are #535 and #536). Part of #498. Stacked on #536; the diff here is only "People for each slot".

What changes

On the review page, next to the slots' headline ("4 slots · 2 days · 1 to 3 people each"), there's now People for each slot: a number box and Set. It appears when the draft has at least two slots.

  • Set gives every slot that number of spots, then says "Set every slot to 4 people." with Undo. Undo puts back what each slot had before.
  • If every slot already has that number, it says "Every slot is already for 3 people." with no Undo, and nothing is written.
  • The box starts with the shared number when every slot has the same one.
  • On a phone, the control wraps under the headline.

How it works

  • A new service, updateSlotCapacities in src/services/slots.ts. The issue suggested the slots/bulk route, but that route only adds slots. The service:

    • runs in one transaction under the signup and slot locks, in the documented order;
    • refuses unknown ids;
    • refuses a capacity below the spots already taken, mirroring updateSlot, so it's safe on a published signup too;
    • skips unchanged slots, and writes nothing when nothing changes;
    • otherwise writes one slot.updated row { bulk: true, slotIds, changed: ['capacity'] }.

    It returns each changed slot's previous capacity.

  • The review page calls it through setCapacityAction → setDraftCapacity in src/services/draft-fixes.ts. That shares loadDraft, so it works on drafts only and refuses viewers. A fingerprint of all the draft's slots, as the page showed them, must match the slots as they are now. Otherwise: "That changed since you opened this page, so nothing was done."

  • Undo with a staleness guard. Undo goes through the shared undoDraftFixAction. It's refused ("That changed again since…") unless every slot still exists and still has the number Set gave it. So a number changed since, in the slot editor, the builder or by an assistant, is never overwritten, and a second Undo is refused.

  • One line, one Undo. It shares the card's status line and last-action Undo with the one-tap fixes and Remove day (Remove a whole day from the draft review page, with Undo #536). An open slot editor saves first. A refusal shows by the control and takes focus.

  • Telemetry: review.slots_edited { kind: 'set_capacity' } after Set, and review.fix_undone { kind: 'set_capacity' } after Undo. docs/telemetry.md and the privacy page say so.

Budget

/app/signups/[id]: 114.9 / 115 kB, so no change to the budget.

Help

"Create and publish your first signup": "To give every slot the same number of spots, type the number in People for each slot, above your slots. Then choose Set. Right after, Undo puts back what each slot had." A new walkthrough test sets three slots at 1, 2 and 3 to 4, checks the rows and the headline, then undoes and reloads. The review page's picture (check-your-signup.png) is re-taken to show the control, with the pointer moved off the rows first.

Checks

  • pnpm lint && pnpm typecheck && pnpm knip && pnpm test: pass (1571)
  • pnpm test:db: pass (634), including:
    • the service's tests: bulk set, no limit, unchanged skipped, unknown id, below spots taken on a published signup, viewer, deleted signup, round trip, concurrent calls;
    • 9 action tests.
  • pnpm build && pnpm budgets: pass
  • Full e2e (chromium): 65 passed

Tophat

In the running app with made-up data (4 slots at 5, 1, 2 and 3 spots):

  • Set 4: "Set every slot to 4 people.", with focus on Undo. One bulk slot.updated row covers all 4, and review.slots_edited is written.
  • Undo: each slot gets its own number back, and review.fix_undone is written.
  • Set 3, then 3 again: the second time says "Every slot is already for 3 people.", with no Undo and nothing written.
  • Phone (390×844): the control sits under the headline and fits.
  • No server errors.

Generated by Claude Code

updateSlotCapacities takes a list of slots of one signup, each with a
new capacity (a positive whole number, or null for no limit), and sets
them in one transaction. It is the service behind "People for each
slot" on the draft review page (issue #504); the page and its Undo come
in a later change.

It locks the signup, then its slots, as reorderSlots does. A slot that
is not one of the signup's is refused as invalid_input, naming it, with
a suggestion to reload. It is not limited to drafts, but it is safe on a
published signup: the places each slot has taken (confirmed and
tentative, counted by quantity, as updateSlot counts them) are read in
one grouped query under the slot locks, and a capacity below them is
refused with updateSlot's conflict, naming the slot. Either refusal
changes nothing.

Slots already at the capacity sent are skipped. When none changes,
nothing is written and no activity is recorded. Otherwise one UPDATE
sets every changed slot, with the new capacities passed as one JSON
parameter, and one slot.updated row records { bulk: true, slotIds,
changed: ['capacity'] }, stamped with the connected app when there is
one. It returns the slots sent as they now stand, and each changed
slot's previous capacity, which sent back puts them back.

The DB tests cover several slots at once, no limit, skipped and no-op
calls, a foreign slot, a capacity below a booking, a sign-up in flight,
a viewer, a deleted signup, the round trip through previous, and two
calls at once. docs/telemetry.md describes the bulk payload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
"People for each slot" sits beside the slots card's headline on the
draft review page (issue #504), on a draft with two to 500 slots: a
number box, filled in with the number every slot takes when they all
take the same one, and Set. It gives every slot that many people through
updateSlotCapacities, and the card's one line for its changes says "Set
every slot to 4 people." ("... to 1 person.") with Undo, shared with the
notices' fixes and Remove day. When every slot already takes that number
it says "Every slot is already for 4 people." with no Undo. An open slot
with unsaved changes is saved first, as for the other changes.

The page sends the number and a fingerprint of every slot, not their
ids, to a new server action, setCapacityAction. setDraftCapacity in
src/services/draft-fixes.ts works the fingerprint out again from the
draft as it is now and sets nothing unless it matches, so a slot added
or deleted since is refused as stale. Undo goes through the shared undo
path and refuses unless every slot it changed is still there and still
takes the number set, so a number changed since is never overwritten
and a second Undo does nothing. A refusal shows under the box and takes
focus, with "Type a whole number of people, 1 or more." for a number
the server won't take; the refusal block Remove day used is now shared
as OwnRefusal. The provider takes a header, so the headline and the
control sit inside it above the line.

It writes review.slots_edited with kind set_capacity, and its Undo
review.fix_undone with the same kind. MAX_CAPACITY, the top of the
capacity column, moves to src/schemas/slots.ts, shared with the merge
service. The telemetry doc, privacy page, changelog and the help page
"Create and publish your first signup" say so, and its walkthrough sets
three slots to 4 people and undoes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
Re-taken with the pointer moved off the slot rows, which highlight under
it; the article's size and alt text follow the new picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
…en it does nothing

Code-review fixes for People for each slot on the draft review page.

updateSlotCapacities takes three optional guards, checked under the
signup's and its slots' locks: onlyDraft refuses a signup that is no
longer a draft (conflict on status), wholeSignup refuses a list that
isn't every slot of the signup (conflict on slots), and a slot's
expected capacity refuses one that has changed since (conflict on
slots). Set uses onlyDraft and wholeSignup, so a publish or a slot added
between the page's read and the write is refused, rather than a live
signup changed or a slot left out while the page says every slot was
set. Undo uses onlyDraft and expects the number Set gave each slot, so a
capacity changed after Undo read the draft is never overwritten. The
page words them as before: not a draft any more, changed since, or
changed again.

The shared CapacitySchema now stops at MAX_CAPACITY, the top of the
column, so a bigger capacity is a structured invalid_input rather than a
database error; the undo's previous capacities use it too. A number too
big for a slot now reads "That’s more people than one slot can take.",
told apart from the zod issue, rather than asking for a whole number.

When every slot already takes the number, the card's line and the last
change's Undo stay as they were, and "Every slot is already for 3
people." shows under the box as a polite note that takes focus, not as
an error. The box no longer starts again when the page comes back, so a
number typed before a refusal stays. Its words for when no answer comes
back are passed in from the server, from the same list fixRefusal uses,
rather than typed into the page's script.

DB tests cover each guard, a publish and a slot added while the call
waits behind the signup lock, a capacity changed while an Undo waits,
and a too-big capacity; the component test checks the note keeps Remove
day's Undo.

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

Copy link
Copy Markdown
Owner Author

Tophat: pass in the running app with made-up data (pat@example.com).

  • Set 4 on 4 slots (5, 1, 2 and 3 spots). The page says "Set every slot to 4 people." and focus moves to Undo. One bulk slot.updated row and review.slots_edited {kind: set_capacity} are written. (93c73e1)
  • Undo gives each slot its own number back and writes review.fix_undone. (93c73e1)
  • No change: Set 3 when every slot already has 3 writes nothing. (93c73e1)
  • No change keeps the last Undo (960aa8b). Remove day on Thu, Jun 13, then Set 3 with every slot already at 3. "Every slot is already for 3 people." shows as a note by the box and takes focus, and the Remove day line and its Undo stay. That Undo still brings Jun 13 back. Before the review fixes, the note replaced the line and the day could no longer be brought back.
  • Phone (390×844): the control wraps under the headline and fits. No console errors.

Code review on this PR, fixed in 960aa8b:

  • Checks run under the lock. updateSlotCapacities takes three optional guards: onlyDraft, wholeSignup (the ids sent must be every slot as locked) and a per-slot expected capacity. Set uses the first two, so a publish or a new slot landing between the page's read and the lock is refused, and no live signup is changed. Undo uses onlyDraft and expected, so a number changed meanwhile is never overwritten. DB tests cover each guard landing while the call waits behind the lock.
  • No-change answers. They keep the card's last Undo and show as a note by the control.
  • Capacity is capped at MAX_CAPACITY on the shared CapacitySchema. A too-big number now gets a structured invalid_input and its own words ("That's more people than one slot can take."), not a 500.
  • The typed number survives a refused Set.
  • One copy of the fallback words, passed from the server.

Left as is: the extra signup read and policy check that updateSlotCapacities repeats after loadDraft. It costs two round trips on a click, and the new guards need the locked read anyway.

Checks on 960aa8b: lint, typecheck and knip pass; unit (1573), test:db (645), build + budgets (/app/signups/[id] 114.9 / 115 kB) and e2e (65/65) all pass.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Closing without merging, as part of the redesign in #498. The branch stays: updateSlotCapacities and the set-every-slot service and their tests are meant to be reused.


Generated by Claude Code

@richshaw richshaw closed this Oct 22, 2026
@richshaw
richshaw removed this pull request from stack #549 October 23, 2026 22:38
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.

Edit slots from the review page

2 participants