Rack reparent: over-capacity false rejection when a Manage-racks confirm both removes and reparents into a full building
- Dominant language
- Go
- Stars
- 55
- Forks
- 16
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 87
Description
## Summary
In the building "Manage racks" flow, when a single confirmation both **removes** a current rack and **reparents in** a rack that's assigned elsewhere, and the building is **already at capacity**, the reparent is rejected as over-capacity even though the net result would fit.
## Root cause — ordering
Reparents commit immediately on Continue (option A), but removals stage to Save:
- `handleManageRacksConfirm` runs `promptReparentCommit(delta.reassigned, apply)` → `commitReparent` fires `AssignRacksToBuilding` **before** `apply()` updates the working set and before Save unassigns the removed rack server-side.
- `client/src/protoFleet/features/buildings/components/ManageBuildingModal/ManageBuildingModal.tsx` (`handleManageRacksConfirm`, `commitReparent`)
- The server capacity guard counts current membership + net-new members:
- `server/internal/domain/buildings/service.go:600-611` — `existing = CountRacksInBuilding(...)`, rejects when `existing + netNewMembers > capacity`.
At commit time the outgoing rack is still a member (removal not yet applied), so for a building at capacity N: `N + 1 > N` → `InvalidArgument`, even though the final selection (remove one, add one) = N and fits.
## Current behavior (safe, recoverable)
- The RPC is atomic → **nothing moves**.
- The operator sees an error toast: *"Couldn't move rack: cannot assign racks: building has N positions (A aisles × R racks per aisle) but N+1 racks would be assigned."*
- `apply()` is skipped → working set untouched, Manage-racks picker stays open to retry.
- **Workaround:** remove the outgoing rack → Save (frees the slot) → then reparent.
No data loss or partial state. The only issue is a **misleading message** (building reads "full" even though the swap nets to zero).
## Impact / priority
**Low (P2).** Requires a building sitting *exactly* at capacity **and** a remove+reparent in the same confirm. Plausible for a genuine "swap racks" op but uncommon; a clean workaround exists.
## Options (needs a design decision)
The clean fix means committing the **removals** server-side on Continue before the reparent — which changes the deliberate "reparents commit on Continue, removals/layout stage to Save" model (removals would no longer be undoable by simply not Saving). Layering a special case ("…but also commit removals when the building is full") onto the shared confirm path is the fragile special-case-on-shared-infra pattern we'd rather avoid.
Candidate directions:
1. Commit removals (unassign) server-side before the reparent when both are present — reconsider the removal-timing model holistically.
2. Client-side: block/deprioritize the reparent commit until capacity accounts for pending removals (can't fully prevent — the server still enforces), or surface a clearer "remove some racks or free a slot first" message tailored to the swap case.
3. Keep current behavior but improve the toast copy for the swap case (cheapest).
_Filed as a follow-up from PR #772 / #766 review (Codex P2). Deferred — the reparent RPC chunking (#777-adjacent) and partial-commit tracking from the same review are already fixed._
Contributor guide
Research direction
Start in client/src/protoFleet/features/buildings/components/ManageBuildingModal/ManageBuildingModal.tsx, reading handleManageRacksConfirm and commitReparent, then inspect server/internal/domain/buildings/service.go:600-611. Reproduce the full-building remove-and-reparent confirmation and trace the RPC ordering. Done means an agreed behavior for the swap case is implemented without the false rejection, with the resulting toast or capacity handling matching that decision.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, typescript
- Domain
- backend, frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100