block / block/proto-fleet

Rack reparent: over-capacity false rejection when a Manage-racks confirm both removes and reparents into a full building

Open
#778 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.