Mobile Scanner PWA

The handheld and sled reference. Five scan modes, an offline queue that is a first-class screen rather than an error state, and two device profiles with materially different capabilities. Every event captured here is a unit hop written by the same transactional writer as everything else.

Hardware is not deployed. 1 MC3390xR and 2 RFD40 sleds are commissioned; 1 handheld and 3 sleds are still ordered. Every screen below works in simulator mode against the same endpoints, and a non-commissioned device's reads are stored with source_mode = 'sim' and can never post a ledger row.
Device profile
MC3390xR handheld
RFD40 sled
read write battery geiger 200 reads/s heartbeat 900s offline severity P3 Session lock: Adnan S. · H1 · cycle_count · HQ_319 zone A3 · open 14 min

The scan loop, eight screens

Reviewable in one glance with no navigation. Every touch target is at or above 44px and every motion degrades under prefers-reduced-motion.

MC3390xR chrome shown
Mode picker 01 / 08
08:14H1 · HQ_319 · 88%
i3 ScanAdnan S. · warehouse
online
Cycle countneeds a zone
Receiveneeds a GRN
Dispatch verifyneeds a manifest
Put-awayneeds a bin
Find a unitneeds an EPC or a serial. Uses the RSSI proximity loop.
CC-1108resume412/415
GRN-2319awaiting0/48
A mode cannot open without its context. That is what stops a scan landing on nothing.
Scan 02 / 08
08:16H1 · reading
3034F00001D21A0000
MSR31-07 · rssi -47 · reads 3
Manual entryConfirm
RFID trigger first, camera barcode as fallback. Haptic on a resolved EPC, a different haptic on an unknown one.
Cycle count 03 / 08
08:31H1 · 88%
Cycle countHQ_319 · zone A3 · CC-1108
3 missing
409
found
3
missing
0
unexp
2
zone
412 of 415 expected · 99.3%
3034F...D21AMSR31-07found
3034F...C112MSR31-07missing
3034F...C118CDW01missing
3034F...9F02PP02zone A1
Rescan zoneClose count
Missing units are named, not counted. Closing the count opens shrinkage findings; it never adjusts stock.
Receive 04 / 08
08:44H1 · 87%
ReceiveGRN-2319 · CIC · batch 2607
short 2
PP02exp 4848
MSR31-07exp 2422
JAE17exp 1214
Condition
RNIQUARANTINESAMPLE_QC
2 short on MSR31-07, 2 excess on JAE17. Resolve both before submit.
PhotoSubmit
Submit is disabled while any line is unresolved. Excess goes to QUARANTINE with a claim; it never posts straight to STOCK.
Dispatch verify 05 / 08
14:20H1 · 81%
Dispatch verifyDB-0712 · 41 AWBs · HQ_319 exit
67/67
67
matched
0
short
1
extra
99.4
gate %
3034F...D21AMSR31-07on manifest
3034F...D107MSR31-07on manifest
3034F...FFA2unknown EPCon dock
One tag on the dock is not on any manifest. It will be recorded and routed to drift, never posted.
HoldRelease truck
Gate compliance is matched over manifest. A location with no commissioned gate reads n/a, never 0%.
Put-away 06 / 08
09:02H1 · 86%
Put-away48 units · PP02 · GRN-2319
suggested
Suggested bin
319-B2-04
62% full · same SKU already binned here · 14 slots free
Alternative319-B2-0588% full
Alternative319-C1-1131% full
Scan the bin label to confirm. An override needs a typed reason.
OverrideScan bin
Confirming by scanning the bin label, not by tapping, is what stops a mis-binned pallet from looking correct in the ledger.
Offline queue 07 / 08
10:04S4 · AJM · no signal
i3 ScanFatima Z. · store · AJM
offline 41m
116 events queued in IndexedDB. Auto-retry with exponential backoff.
Count · 142 on-hand
MST-CRM-42-BLK · AJM display · 10:02:14
try 3
Receive · 40u RNI
LBA01 · AJM stockroom · 10:00:41
try 3
Damage · 1u clasp
MIN-VAS-022 · AJM · 09:58:06
try 2
Zone move · rejected
stale_state · unit already sold at 09:41
decide
Manual entryRetry sync
The idempotency key is minted on the device before the first attempt. The server never generates one, so a replay is exactly-once by construction.
Committed 08 / 08
10:06S4 · synced
DoneHop and posting committed together
posted
Receipt posted
RECEIVED_NOT_INVOICED +40 at AJM
LBA01 · GRN-2319 line 3
unit_ledgerhop 9e02bb
ledger_entriesLE-482114
chain prevc7f4a1
Scan next in 2s · Undo posts a compensating hop, it never deletes
UndoScan next
The hash is shown because the operator's proof that a scan landed should be the same artefact the auditor reads.

Offline queue depth and sync latency · 12h

S4 at AJM offline since 09:23
04:00 · 4 queued · all synced within 2s 05:00 · 3 queued 06:00 · 6 queued 07:00 · 12 queued · shift start burst 08:00 · 21 queued · GRN-2319 receive 09:00 · 17 queued 10:00 · 48 queued · AJM sled offline, nothing draining 11:00 · 62 queued · AJM still offline 12:00 · 78 queued · escalated to ops 13:00 · 96 queued 14:00 · 116 queued · current · oldest item 41 min 15:00 · projected 116, no drain path until signal returns median sync latency, seconds, rising as the queue ages
040506070809101112131415
Draining normallyGrowingEscalatedMedian sync latency

Device profile matrix

4 profiles
ProfileFormReadWritePrintBattGeigerReads/sHB sOffline
fxr90fixed gateyesnononono70060P1
mc3390xrhandheldyesyesnoyesyes200900P3
rfd40sledyesyesnoyesyes1501800P3
zt411rprinternoyesyesnono0300P2
A device is late at its own heartbeat interval and offline at three times that. A commissioned gate on a mandatory dispatch flow is late at 60 seconds. A sled in a closed store is not late at 12 hours. A single 30-minute threshold would either page all night or miss a dead gate for half an hour.

Mode contract

What each mode needs before it opens, what it emits, and whether it can run without a signal.

ModeContext requiredEvents emittedPosting roleOffline safeApprovalBlocks on
Cycle countlocation + zone + an open count sessionCYCLE_COUNTobservingyesyes, to post any variancenothing, a count never blocks
Receivea GRN with an accepted QC dispositionRECEIVE, ENCODEoriginatingyesnounresolved short or excess lines
Dispatch verifya dispatch manifest in packing or dispatchedDISPATCHconfirmingno, needs the live manifestnoan unmatched tag on the dock
Put-awayunits in state new or in_stock plus a binZONE_CHANGEobservingyesnobin label not scanned
Find a unitan EPC or a serial codenoneobservingyes, from the cached indexnonothing, read only

Offline replay rules

12 rules
#RuleEnforced by
1The idempotency key is minted on the device before the first send attempt and never changes across retries.uq_mqi_idem
2The server never generates a key. A server-side key would make two attempts two events.API contract, 400 on a missing key
3Items replay in client_seq order within a session. Out-of-order arrival is buffered, not applied.idx_mqi_session
4A replayed scan against a unit whose state has since moved is rejected as stale_state and surfaces for a decision. It is never force-applied.R-12.41
5A rejected item is never silently dropped. It stays in the queue with a reason until an operator resolves it.status='rejected'
6Device clock skew is captured, not corrected. occurred_at uses the device clock; recorded_at uses the server clock.device_clock_skew_ms
7One open session per device and one per operator. A second device cannot capture into the same session.two partial unique indexes
8Backoff is 2s, 4s, 8s, 16s, then 30s steady. It never gives up while the session is open.service worker
9The queue is unbounded. A 41-minute outage at AJM must not start dropping the oldest scan.IndexedDB, no cap
10Undo posts a compensating hop within 120 seconds. After that the correction goes through the drift workbench.R-12.42
11Closing a session with queued items is allowed, and the queue keeps draining. Closing does not discard.mobile_sessions.ended_at
12A scan from a device that is not commissioned lands with source_mode='sim' and posts nothing, however it arrived.R-12.20

Why the queue is a screen

Warehouse gates and mall stockrooms have poor connectivity. If the offline queue is an error banner, the operator's mental model becomes "the app is broken" and they stop scanning. If it is a screen with a count, a per-item state and a retry button, the model becomes "the app is holding my work", and they keep going.

That is not a UX preference. A scan not taken is a unit i3 never sees, and the reconciliation reads it as shrinkage three hours later at 06:00 the next morning. Every abandoned scan becomes a false variance that a human then has to investigate.

The client-minted key is the whole mechanism. The device generates it once, at capture. The PWA can be force-quit, the iPad can reboot, the sync can be interrupted mid-batch and retried, and every one of those paths resolves to the same row. There is no window in which two attempts become two events.

The incumbent gets this wrong. The rfid-app passes an idempotency key on 5 of its 14 ledger write paths. A repeated receive POST on any of the other 9 credits the same stock twice. That is exactly the class of bug this design removes at source rather than patching per endpoint.

Connected to

Gates and feed Unit events 3-way reconcile Stock count Cartons and bins GRN and QC Transfers Locations HQ_319 detail Damages Incidents Device settings Drift