fix(cassettes): order ATM state events, and reconcile the bay set #44

Merged
padreug merged 2 commits from fix/cassette-state-reconcile into main 2026-09-22 20:30:45 +00:00
Showing only changes of commit 106da5b46b - Show all commits

fix(cassettes): drop bays the machine no longer reports
Some checks failed
ci.yml / fix(cassettes): drop bays the machine no longer reports (pull_request) Failing after 0s

apply_reported_state upserted the positions in the payload and left every
other row untouched. When a machine's bay count shrank, the stale row
stayed: the dashboard showed a mix of old and new bays, and publish
validation then rejected every operator payload for a position-set
mismatch. The error text told the operator to fix it with atm-tui, which
could not propagate either, so the only way out was DELETE FROM by hand.

The payload is the machine's full bay set and the machine owns that layout,
so positions absent from it are now deleted. The delete runs before the
upserts: since this data layer commits per statement, a crash between the
two leaves rows missing rather than stale, and the next heartbeat
re-inserts them — the safe direction of the two.

Refs #43

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Padreug 2026-09-22 22:23:40 +02:00

20
crud.py
View file

@ -1563,6 +1563,13 @@ async def apply_reported_state(
Returns True if the state was applied, False if the event was not newer Returns True if the state was applied, False if the event was not newer
than what is already on file (see _should_apply_state_event). than what is already on file (see _should_apply_state_event).
The payload is the machine's full bay set, and the machine owns that
layout — the bay count is hardware-determined. So positions absent from
the payload are DELETED here. Leaving them behind was half the problem a
shrinking bay count caused: the stale row stayed, the dashboard showed a
mix of old and new, and every later publish was rejected for a position
mismatch with no way out but hand-written SQL.
Populates both the operator-believed columns (denomination, count, Populates both the operator-believed columns (denomination, count,
updated_at, updated_by) and the reported columns (state_denomination, updated_at, updated_by) and the reported columns (state_denomination,
state_count, state_at, state_event_id), so the UI can show reported state_count, state_at, state_event_id), so the UI can show reported
@ -1584,6 +1591,19 @@ async def apply_reported_state(
if not _should_apply_state_event(oldest_state_at, event_created_at): if not _should_apply_state_event(oldest_state_at, event_created_at):
return False return False
# Drop bays the machine no longer reports, before writing the rest. A crash
# between the two leaves rows missing rather than stale, and the next
# heartbeat re-inserts them — the safe direction.
keep = sorted(payload.positions.keys())
placeholders = ", ".join(f":p{i}" for i in range(len(keep)))
delete_values: dict = {"mid": machine_id}
for i, pos in enumerate(keep):
delete_values[f"p{i}"] = pos
await db.execute(
"DELETE FROM spirekeeper.cassette_configs "
f"WHERE machine_id = :mid AND position NOT IN ({placeholders})",
delete_values,
)
now = datetime.now() now = datetime.now()
for pos, row in payload.positions.items(): for pos, row in payload.positions.items():