diff --git a/crud.py b/crud.py index f0f7f37..a622d96 100644 --- a/crud.py +++ b/crud.py @@ -1563,6 +1563,13 @@ async def apply_reported_state( 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). + 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, updated_at, updated_by) and the reported columns (state_denomination, 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): 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() for pos, row in payload.positions.items():