502bf5bbeb
The legacy RosterUnit.deployed flag drives heartbeat polling and
benched-vs-deployed roster filters. Three workflows ended an
assignment without flipping it, so the outgoing unit kept being
polled and showed up as "deployed" forever:
- swap endpoint (POST /locations/{loc}/swap)
- unassign endpoint (POST /assignments/{aid}/unassign)
- promote-pending endpoint (POST /deployments/pending/{id}/promote)
All three now: close the previous active assignment, break the
outgoing unit's modem pairing (both directions), and set
`deployed = False` on the outgoing unit. Unassign and swap also
clear the modem's back-reference.
The promote-pending path additionally handles the case where the
target location already has an active assignment — that previously
silently created two active assignments at the same location. Now
the old one is closed (assigned_until = pending capture time, status
= completed), the old unit is benched and unpaired, and an
"assignment_swapped" history row is written. Incoming unit gets
`deployed = True` if it was on the bench.
Verified live: triggered a swap via the existing endpoint and saw
the outgoing unit flip True → False while the incoming flipped
False → True. Test mutations rolled back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>