> ## 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.

# Offline and outbox

> Offline-first design, localStorage outbox, retry with backoff, and hub-side event log.

The POS system is designed to **never block** staff from taking orders, regardless of network conditions. Every local action is captured as an idempotent event and safely queued for later delivery.

## How offline works

| Scenario                 | Behavior                                                      |
| ------------------------ | ------------------------------------------------------------- |
| Odoo down (direct mode)  | Outbox queues HTTP requests, retries with exponential backoff |
| Hub down (hub mode)      | Transport falls back to outbox; orders still editable locally |
| Internet down (hub mode) | Hub still works on LAN; terminals sync via hub normally       |
| Hub + Internet down      | Terminal works fully offline, outbox accumulates              |
| Recovery                 | Outbox drains automatically; hub reconciles on reconnect      |

## localStorage outbox

`nu_pos_react/src/realtime/sync/outbox.ts`

The outbox is a queue of pending HTTP actions stored in `localStorage` under the key `pos_sync_outbox`. It survives browser refreshes and crashes.

### Entry structure

```typescript theme={null}
interface OutboxEntry {
  id: string;                    // UUID
  action: "orders/create" | "orders/update";
  payload: Record<string, unknown>;
  createdAt: string;             // ISO timestamp
  attempts: number;              // Number of send attempts
  nextAttemptAt: string | null;  // Earliest retry time
  lastError: string | null;      // Last failure message
}
```

### Queuing

```typescript theme={null}
queueOutbound("orders/create", {
  pos_reference: "POS-001-0001-00001",
  lines: [...],
  amount_total: 150.00,
});
```

Entries are **deduplicated by `pos_reference`**: if an entry with the same reference already exists, it's replaced with the newer payload. This prevents duplicate orders.

In hub mode, when `connection.send()` fails, the snapshot is queued as an outbox entry with an `_hubSnapshot` field.

### Flushing

`flushOutbound()` processes the queue:

1. Reads all entries from localStorage
2. Filters entries where `nextAttemptAt <= now` (respects backoff)
3. For each entry, sends `POST /pos-react/api/orders/{action}` to Odoo
4. On success: removes entry from queue
5. On failure: increments `attempts`, computes next retry time, stores error

### Retry backoff

| Attempt | Delay            |
| ------- | ---------------- |
| 1       | 1s               |
| 2       | 2s               |
| 3       | 4s               |
| 4       | 8s               |
| 5       | 16s              |
| 6+      | 32s-60s (capped) |

After `maxRetry` attempts (default 8), the entry stays in the queue but won't be retried until app restart.

### Outbox depth monitoring

The transport exposes `getOutboxDepth()` which returns the current number of pending entries. This is reflected in the sync state machine via `OUTBOX_CHANGED` events and displayed by the `SyncStatusIndicator` badge.

## Hub-side event log

The hub maintains its own event log for upstream forwarding to Odoo. This is a separate mechanism from the terminal outbox.

```sql theme={null}
CREATE TABLE events (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  type          TEXT NOT NULL,     -- 'order_snapshot', 'table_update', etc.
  pos_reference TEXT,
  terminal_id   TEXT,
  payload       TEXT,              -- JSON
  forwarded     INTEGER DEFAULT 0, -- 0=pending, 1=forwarded to Odoo
  created_at    TEXT DEFAULT (datetime('now'))
);
```

### Forwarder

`nu_pos_hub/src/upstream/forwarder.ts` drains the event log to Odoo:

1. Runs every **5 seconds** (`FORWARD_INTERVAL_MS`)
2. Queries up to **20** pending events (`FORWARD_BATCH_SIZE`), oldest first
3. Non-order events are marked as forwarded immediately (no Odoo action needed)
4. Order snapshot events are forwarded via the Odoo adapter
5. On success: marks events as `forwarded=1`
6. On first failure: logs error, stops processing batch (preserves ordering)

If Odoo is unreachable, the hub authenticates first, then retries. Events accumulate safely in SQLite.

## Idempotency

All order operations are idempotent via `pos_reference`:

* Odoo's create endpoint returns the existing order if the reference already exists
* The hub's `orderStore.upsert()` uses `INSERT OR REPLACE` keyed by `pos_reference`
* The outbox deduplicates by `pos_reference` before queuing

Re-forwarding the same event (after a crash, restart, or retry) is always safe.

## Visibility

| Tool                               | What it shows                                    |
| ---------------------------------- | ------------------------------------------------ |
| `SyncStatusIndicator` (frontend)   | Transport state, outbox depth, error indicator   |
| Hub dashboard (`http://hub:8766/`) | Connected terminals, pending forwards, event log |
| CLI `pos-hub stats`                | Uptime, terminal count, pending/forwarded events |
| CLI `pos-hub events`               | Recent event log with forwarded status           |
