Repository navigation
Conversation
…ndoes it The review page's fix for identical rows (issue #503) needs one call that turns copies of a slot into one slot holding all their places, and one that puts them back for Undo. mergeSlots keeps one slot, gives it the copies' total capacity (worked out from the rows as locked, no limit when any had none) and, when sent, new values and the slot_at they resolve to, then deletes the rest. unmergeSlots gives the kept slot back its values and capacity and adds the others again at their old place in the order. Each runs in one transaction under the signup lock and then the slot locks, writes one activity row (slot.merged, slot.unmerged), and works on drafts only: on a published signup a merge would move people who have signed up. Both refuse any slot with a commitment rather than let a delete take it away, and an undo is refused once the kept slot has been deleted or its capacity changed. The slot insert addSlot and addSlotsBulk each wrote out is now one helper, insertSlot, which the undo uses too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
The review page's notice for slots that are copies of each other now has "Merge into one slot" in place of "Fix in the builder" (#503). It runs as the other one-tap fixes do: only on a draft, and only while the draft still has exactly those copies. The checks run again on fresh data and the request has to name the same set of slots. It then calls mergeSlots with the notice's kept slot and copies. Numbered rows ("Helper 1", "Helper 2") send the values without the number; identical rows keep the kept slot's own values. The page says "Merged 3 slots into one for 3 people." ("with no limit" when any copy had none). Undo sends back exactly what mergeSlots returned, checked by its own schema, to unmergeSlots. Before that, the page checks that the kept slot is still there with the merged total. The service refuses the same thing under its locks. When the kept slot had no limit, its capacity can't tell a second undo from the first, so the page also refuses while its copies are back, rather than putting them back twice. The merge services' refusals read as the other fixes' do: a changed draft is "That changed since you opened this page, so nothing was done.", and a kept slot gone or changed is "That changed again since, so it can't be undone." The one-tap merge is offered for up to MAX_MERGE_SLOTS copies, now exported. Unit tests cover matching, the merge request, the undo and its checks, and the wording. The actions' DB tests run the merge through fix, undo, a stale request, a viewer and a published signup. They also cover an undo after the kept slot's places changed, and a second undo of copies with no limit. The telemetry doc names slot.merged and slot.unmerged among what the fixes write. The help page "Create and publish your first signup" names the button, and its walkthrough merges two copies and undoes it. The changelog describes the service, the button and Undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
A kept slot with no limit reads the same before a merge and after its undo, so the capacity check couldn't tell a repeated undo from the first, and the copies came back twice. unmergeSlots now reads the latest slot.merged / slot.unmerged row for the kept slot under the signup lock, as reminder.sent stops a second reminder, and refuses once it's undone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
A merge now runs the review page's copy check again under its locks, on
the slots and fields as locked. The slots sent must be exactly one set of
copies, and for numbered rows the values sent must be the ones the copies
now share. A copy changed or added since, a set that is only part of the
copies, or values the page worked out from an older version are refused,
and nothing is deleted.
Undo is guarded by one version check instead of three. mergeSlots puts
the kept slot's updatedAt, as the merge wrote it, into the undo as
keptUpdatedAt. unmergeSlots refuses once the kept slot's updatedAt has
moved. Editing the slot, reordering it and the undo itself all move it.
So Undo no longer puts old values back over a rename or a new date made
since, and it can't put the copies back twice, whether the kept slot had
a limit or not. The undo writes an updatedAt past the merge's, so two
writes in one millisecond can't fool it. This replaces the capacity-sum
check, the activity-log lookup and the review page's copies heuristic,
which wrongly refused a first undo once a new copy of the kept slot had
been added. The review page now checks only that the kept slot exists.
A merge's refusals read as a change since ("stale" / "changed") only when
a change since causes them. Anything else reads as "Couldn't fix that.
Try again." or "Couldn't undo that. Change it in the builder.", rather
than offering the same button again. The service's field never passes
on: a slot field called "label" read as the rename box's refusal. The
rename refusal now applies to a fix only. The mapping is a pure
mergeRefusal in src/lib/draft-fixes.ts, unit-tested.
mergeRequestFor sends values only for numbered rows. The undo schema is
strict and has no per-row status (a merge only takes open slots), and
insertSlot no longer takes one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm
|
Tophat: pass, in the running app with made-up data (
Code review on this PR. Fixed in aeaa678 and 4baff66:
Left as is: restore order for copies that share a Checks on 4baff66: lint, typecheck, knip, unit (1486), Generated by Claude Code |
|
Closing without merging, as part of the redesign in #498. The branch stays: Generated by Claude Code |
Fixes #503 (part 3 of 3; parts 1 and 2 are #530 and #533). Part of #498. Stacked on #533; the diff here is only the merge.
What changes
When a draft has slots that are copies of each other, the review page's notice ("3 slots are the same: Setup · Thu, May 2. One slot for 3 people does the same job.") now has Merge into one slot:
This was the issue's check 4: "one slot whose capacity is the sum of theirs, with the others deleted … in one transaction with one activity row, through a small
mergeSlotsservice."How it works
src/services/slot-merge.ts:mergeSlots: one transaction and oneslot.mergedrow. It locks the signup, then its slots, in the documented order. Under the lock it refuses:findDuplicateSlotsruns again on the locked rows, so a copy renamed since the notice was shown isn't deleted.Capacity is worked out from the locked rows, never taken from the caller.
unmergeSlots: the exact inverse, with oneslot.unmergedrow. The undo carries the kept slot'supdatedAtfrom the merge. It is refused when that has changed, which covers:insertSlotinslots.tsis the insert path foraddSlot,addSlotsBulkand the undo, so restored rows get theirrefandslot_atthe same way.duplicate_slotskind. The request must match a fresh notice by its exact set of ids. The undo is exactly whatmergeSlotsreturned, checked against the strict unmerge schema on the way back.slot.merged({ slotId, mergedSlotIds, capacity }) andslot.unmerged({ slotId, restoredSlotIds, capacity }), plusreview.notice_accepted/review.fix_undonewith kindduplicate_slots.docs/telemetry.mdlists them.Help
"Create and publish your first signup": "For slots that are copies of each other, choose Merge into one slot: one slot takes all their spots." A new walkthrough test merges copies on the review page and undoes it.
Checks
pnpm lint && pnpm typecheck && pnpm knip && pnpm test: pass (1486)pnpm test:db: pass (592)pnpm build && pnpm budgets: pass (/app/signups/[id]112.7 / 113 kB, unchanged)Tophat and the code-review fixes are in the comments.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BTvMvHZ4i4FWQFAsxcr5Zm