Etap 4.5 — Offline mode guarantees¶
The kiosk is designed to keep authenticating users and releasing parcels even when its uplink to api.smartbox.ergoflow.app is down. Backend remains the source of truth — anything the kiosk does offline is a local approximation that gets reconciled on the next sync tick.
What the kiosk caches¶
SyncService pulls a delta from GET /machines/me/sync?since=<isoCheckpoint> every 2 minutes (and once at boot) and writes into sqflite:
| Table | Source | Purpose |
|---|---|---|
cached_users |
active User rows updated since checkpoint |
recipient identity for pickup screen |
cached_credentials |
active Credential rows (id / userId / type / bcrypt valueHash only) |
offline PIN auth (OfflineRepo.authenticatePin) |
cached_lockers |
this machine's Locker rows |
future deposit-offline support; menu currently uses availableFunctions from /machines/me |
cached_shipments |
this machine's DEPOSITED shipments |
offline pickup list (OfflineRepo.shipmentsForUser) |
sync_cursor |
one row, holds latest checkpoint ISO |
next pull is a delta, not a full snapshot |
Plaintext PINs / RFID values never leave the backend. We ship the bcrypt hash and BCrypt.checkpw locally — exactly the same primitive CredentialsService.authenticate uses on the server side.
ErgoFlow employees (etap 6) flow through this same pipeline. The backend's ErgoflowService JIT-creates a SmartBox User (email = <userNumber>@ergoflow.local) plus an RFID Credential (valueHash = bcrypt(userNumber)) for every active upstream employee. The next /machines/me/sync tick fans those rows out to cached_users + cached_credentials, so a kiosk card swipe from an ErgoFlow employee authenticates offline with no extra setup. When ErgoFlow flips allowLogin=0 for an employee or drops them from the active list, the backend marks the SmartBox User inactive — the next sync tick removes the row from cached_users and offline auth stops accepting that card.
What the kiosk queues for replay¶
outbox table — one row per recorded mutation that the kiosk wants the backend to apply once the link is back. Drained in insert order by SyncService after every successful pull.
kind |
payload |
Server handler | Notes |
|---|---|---|---|
pickup.completed |
{ shipmentId } |
MachineSyncService.replayPickupCompleted |
flips DEPOSITED → PICKED_UP, locker → FREE; honours kiosk's occurredAt for pickedUpAt |
deposit.completed |
{ shipmentId } |
replayDepositCompleted |
flips ASSIGNED → DEPOSITED, locker → OCCUPIED. Sender flow is online-only today; format reserved for etap 7 |
Each event in the replay POST gets one of three outcomes:
OK— server applied the mutation. Kiosk deletes the outbox row.CONFLICT— server already moved on (panel cancelled the shipment, another kiosk pickup, expiry worker fired etc.). Kiosk deletes the outbox row anyway — retrying never resolves a conflict — and the conflict is captured inAuditLog(actorType=SYSTEM,action=system.notification_*).FAILED— unexpected server error or unknown event kind. Kiosk incrementsretryCountand retries on the next tick. Manual ops intervention is needed if these stack up.
Operations matrix¶
| Operation | Online behaviour | Offline behaviour | Reconciliation |
|---|---|---|---|
| PIN at login | POST /deposit/verify-sender |
bcrypt-check against cached_credentials |
n/a — read-only |
| List my parcels (menu → Pickup) | GET /pickup/by-user?userId= (creates OPEN_LOCKER tasks) |
OfflineRepo.shipmentsForUser (no task creation — kiosk drives LockerHardware.open() locally) |
next sync rebuilds cached_shipments |
| Open a locker | LockerHardware.open() (Mock today, Modbus etap 5) |
identical | hardware is local |
| Confirm pickup | POST /pickup/:id/complete |
queue pickup.completed in outbox + local cached_shipments.status='PICKED_UP' |
next replay flips server-side; CONFLICT possible if panel cancelled meanwhile |
| Send a parcel (deposit) | full online flow via POST /deposit/begin |
rejected — kiosk shows network error. Couldn't allocate a tracking ID without server | n/a |
| OTA install | runs through PS agent + state file | n/a (OTA needs internet) | n/a |
Header indicator¶
The SyncStatusBanner chip in the kiosk header has three visual states:
- Hidden — online + outbox empty. Healthy steady state.
- Cyan
SYNC (n)— online + outbox hasnrows pending replay. Transient (next tick clears it on success). - Amber
OFFLINE (n)— last sync attempt failed;nrows queued. Stays up until the link returns.
Manual recovery¶
If the operator suspects the kiosk has a stuck queue:
```powershell
Windows¶
Get-Content C:\smartbox-kiosk\agent.log -Tail 50 # shows last sync attempt + reason
Optional: open the cache directly with a SQLite browser¶
C:\smartbox-kiosk\kiosk-cache.sqlite — examine outbox, sync_cursor¶
```
```bash
Linux¶
tail -50 ~/.smartbox-kiosk/agent.log sqlite3 ~/.smartbox-kiosk/kiosk-cache.sqlite "SELECT * FROM outbox; SELECT * FROM sync_cursor;" ```
A drastic reset (clears outbox + cache; kiosk re-pulls from scratch on next tick): delete kiosk-cache.sqlite and restart the UI. Any in-flight pickup completions in the outbox are lost — only do this if you've already manually verified the panel state matches.