Documentation Index
Fetch the complete documentation index at: https://docs.laportenard.com/llms.txt
Use this file to discover all available pages before exploring further.
The frontend sync integration consists of three layers:
- State machine (
syncMachine) — Tracks transport state, lease state, and outbox depth
- Provider (
RealtimeProvider) — Creates the transport, wires event handlers, bridges to UI state
- UI components — Visual indicators for sync status, conflict dialogs, lease notifications
Sync state machine
nu_pos_react/src/state/machines/syncMachine.ts
An XState machine managing the sync lifecycle:
States
ONLINE
+------+
+------->| |<--------+
| +--+---+ |
| | |
ONLINE TRANSPORT_ TRANSPORT_
RECONNECTING CONNECTED
| | |
| v |
| RECONNECTING |
| +------+ |
| | |--------+
| +--+---+
| |
OFFLINE TRANSPORT_
DISCONNECTED
| |
v v
OFFLINE OFFLINE
+------+ (from reconnecting)
| |
+------+
|
ONLINE
|
v
HUB_OFFLINE
+------+
| |---> ONLINE (on TRANSPORT_CONNECTED)
| |---> RECONNECTING (on TRANSPORT_RECONNECTING)
+------+
Context
interface SyncContext {
isOnline: boolean;
lastSyncAt: number | null;
lastSeenSyncId: number;
appliedCount: number;
lastError: string | null;
transportMode: "direct" | "hub";
hubUrl: string | null;
leaseHeld: string | null;
outboxDepth: number;
connectedTerminals: number;
transportState: TransportState;
}
Events
| Event | Source | Effect |
|---|
ONLINE | Browser online event | Transition to online state |
OFFLINE | Browser offline event | Transition to offline state |
SYNC_APPLIED | Remote events applied | Update lastSeenSyncId, appliedCount, clear error |
SYNC_ERROR | Sync failure | Set lastError |
TRANSPORT_CONNECTING | Transport state change | Update transportState |
TRANSPORT_CONNECTED | Transport state change | Update transportState, return to online |
TRANSPORT_DISCONNECTED | Transport state change | Update transportState |
TRANSPORT_RECONNECTING | Transport state change | Transition to reconnecting |
TRANSPORT_HUB_OFFLINE | Transport state change | Transition to hubOffline |
LEASE_ACQUIRED | Lease granted | Set leaseHeld |
LEASE_RELEASED | Lease released | Clear leaseHeld |
LEASE_LOST | Lease revoked | Clear leaseHeld, set error |
OUTBOX_CHANGED | Outbox depth change | Update outboxDepth |
SET_TRANSPORT_MODE | Config change | Set transportMode, hubUrl |
RealtimeProvider
nu_pos_react/src/app/providers/RealtimeProvider.tsx
The main sync orchestrator. Creates the transport on mount and wires all event handlers:
Event wiring
transport.onStateChange() → Maps transport state to sync machine events
transport.onConflict() → Shows conflict dialog via SyncUIProvider
transport.onLeaseRevoked() → Sends LEASE_LOST to sync machine
transport.onOrderUpdated() → Applies remote order update to local state
transport.onReconnecting() → Sets isReconnecting in SyncUIProvider (shows overlay)
transport.onReconnected() → Clears isReconnecting (hides overlay)
transport.onStateChange() to disconnected while isReconnecting → Sets reconnectionFailed (session expired)
- Browser
online/offline events → ONLINE/OFFLINE to sync machine
Provider tree
<SyncUIProvider> // Sync UI state (conflicts, lease notifications)
<RealtimeProvider> // Transport lifecycle, event handling
<PosProvider> // Order state, reducer
<App />
</PosProvider>
</RealtimeProvider>
</SyncUIProvider>
SyncUIProvider wraps RealtimeProvider so that the realtime layer can push events into the UI context.
UI components
SyncStatusIndicator
A compact badge reading from the sync machine context:
| State | Icon | Color | Label |
|---|
connected | Cloud check | Green | ”Synced” |
connecting | Cloud | Blue (spinning) | “Connecting” |
reconnecting | Cloud off | Yellow (spinning) | “Reconnecting” |
hubOffline | Cloud off | Orange | ”Hub Offline” |
disconnected | Cloud off | Gray | ”Offline” |
| Error | Alert triangle | Red | ”Sync Error” |
When outboxDepth > 0, the badge shows a count of pending items.
ConflictDialog
Modal dialog shown on version conflicts:
- “Load Latest” → Calls
reconcile() with accept_remote strategy
- “Keep Mine” → Dismisses dialog, allows user to manually resolve
SyncOverlay
Component connecting SyncUIProvider state to visible UI:
- Renders
ConflictDialog when conflict is non-null
- Renders
ReconnectionOverlay for sleep/wake recovery (see below)
- Shows a destructive toast when
leaseRevoked is non-null (“Manager took over this order”)
ReconnectionOverlay
nu_pos_react/src/features/sales/components/reconnection-overlay.tsx
A full-screen blocking overlay shown during sleep/wake WebSocket recovery. Reads isReconnecting and reconnectionFailed from SyncUIProvider:
| State | Display |
|---|
isReconnecting && !reconnectionFailed | Spinner + “Restoring connection…” + “Please wait while we reconnect to the server” |
isReconnecting && reconnectionFailed | ”Session expired. Redirecting to login…” (shown briefly before auth redirect) |
!isReconnecting | Hidden (null) |
The overlay uses z-50 with a semi-transparent backdrop to block interaction during reconnection.
TableLockedDialog
Floor plan modal shown when a table lock is denied:
- “Dismiss” — Close dialog, release any pending lock
- “Take Over” (managers only) — Force-acquire the lock
TableLockGuard
Renderless component on the order page managing table lock lifecycle:
- Releases the lock on unmount
- Re-acquires the lock after WebSocket reconnect
- Redirects to the floor plan if the lock is revoked
Bridge components
Renderless components that wire hub transport events into the POS reducer. Must be rendered inside both <POSProvider> and <SyncTransportContext.Provider>.
HubOrderBridge
nu_pos_react/src/realtime/HubOrderBridge.tsx — Used on the order page:
- Outbound: Builds
OrderSnapshotData from active order on every state change → sends ORDER_SNAPSHOT via transport (with hash-based change detection to avoid redundant sends)
- Inbound
SYNC_INIT: Applies full order reconciliation on connect (gated on hydrated to wait for IndexedDB restore)
- Inbound
ORDER_UPDATED: Applies remote snapshots via applyRemoteSnapshot({ posReference, snapshot }) — auto-creates orders for the active table, closes orders with paid/closed status or empty lines
- Lease lifecycle: Acquires lease when active order has items + posReference, releases on switch/unmount
- Deletion detection: Sends
closed snapshot to hub when local orders are removed
FloorPlanSyncBridge
nu_pos_react/src/realtime/FloorPlanSyncBridge.tsx — Used on the floor plan page:
- Inbound
SYNC_INIT: Full reconciliation (same as HubOrderBridge)
- Inbound
ORDER_UPDATED: Applies remote snapshots so table occupation status stays current across terminals. Closes orders with paid/closed status or empty lines
FloorPlanStructureSyncBridge
nu_pos_react/src/realtime/FloorPlanStructureSyncBridge.tsx — Used on the floor plan page:
- Inbound
FLOOR_PLAN_CHANGED: Updates floor plan table positions and structure when another terminal edits the layout
- Outbound: Sends
FLOOR_PLAN_UPDATE when the current terminal saves layout changes
APPLY_SYNC_INIT is a destructive full reconciliation that prunes all hubSynced orders not in the payload. Never use it for single-order updates — use APPLY_REMOTE_SNAPSHOT instead (safe single-order upsert).
How everything connects
Bootstrap config (Odoo)
|
v
createTransport() --> directTransport or hubTransport
|
v
RealtimeProvider
|
+-- transport.onStateChange() --> syncMachine (TRANSPORT_* events)
+-- transport.onConflict() --> SyncUIProvider.showConflict()
+-- transport.onLeaseRevoked()--> SyncUIProvider.notifyLeaseRevoked()
+-- transport.onOrderUpdated()--> apply to local order state
+-- transport.onTableLocksState()--> update floor plan lock indicators
+-- transport.onTableLockRevoked()--> TableLockGuard redirects to floor plan
|
v
syncMachine context
|
v
SyncStatusIndicator (reads transportState, outboxDepth, lastError)
ConflictDialog (reads SyncUIProvider.conflict)
SyncOverlay (reads SyncUIProvider.leaseRevoked, shows toast)
i18n
Translations are in the sync namespace (src/i18n/locales/{en,es}.json):
{
"sync": {
"connected": "Synced",
"connecting": "Connecting",
"reconnecting": "Reconnecting",
"hubOffline": "Hub Offline",
"offline": "Offline",
"error": "Sync Error",
"pendingItems": "{count} pending",
"conflictTitle": "Version Conflict",
"conflictAcceptRemote": "Load Latest",
"conflictKeepLocal": "Keep Mine",
"leaseRevokedTitle": "Order Taken Over",
"leaseRevokedGeneric": "Another user took over this order",
"reconnectionTitle": "Restoring connection...",
"reconnectionSubtitle": "Please wait while we reconnect to the server",
"reconnectionFailed": "Session expired. Redirecting to login..."
}
}