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

# Hub server

> Node.js hub internals: storage, handlers, forwarder, configuration, and HTTP endpoints.

The hub server is a standalone Node.js service designed to run on a Raspberry Pi (or any ARM64/AMD64 machine) inside the restaurant's local network. It coordinates all POS terminals via WebSocket, persists state in SQLite, and forwards events to Odoo in the background.

**Package**: `nu_pos_hub/`

## Startup sequence

`nu_pos_hub/src/index.ts` initializes all components in order:

1. Load configuration from environment
2. Initialize SQLite database (WAL mode)
3. Create storage layers (orders, leases, events, tables, table locks)
4. Create lease manager with revoke callback
5. Create table lock manager with revoke callback
6. Create Odoo adapter and forwarder
7. Wire up message handlers
8. Start HTTP server (health, status, events, dashboard)
9. Start WebSocket server (attached to HTTP server)
10. Start periodic tasks (lease tick, forwarder, status broadcast)
11. Start mDNS advertisement (if enabled)
12. Register graceful shutdown handlers (SIGTERM, SIGINT)

## Configuration

`nu_pos_hub/src/config.ts` — All settings via environment variables:

| Variable                  | Default                 | Description                          |
| ------------------------- | ----------------------- | ------------------------------------ |
| `HUB_PORT`                | `8765`                  | WebSocket server port                |
| `HUB_HTTP_PORT`           | `8766`                  | HTTP server port (health, dashboard) |
| `HUB_DB_PATH`             | `./hub.sqlite`          | SQLite database file path            |
| `ODOO_URL`                | `http://localhost:8069` | Odoo instance URL                    |
| `ODOO_DB`                 | `pos`                   | Odoo database name                   |
| `ODOO_USER`               | `admin`                 | Odoo admin username for hub auth     |
| `ODOO_PASSWORD`           | `admin`                 | Odoo admin password                  |
| `HUB_LEASE_TTL_MS`        | `60000`                 | Lease validity (ms)                  |
| `HUB_HEARTBEAT_MS`        | `20000`                 | Heartbeat / tick interval (ms)       |
| `HUB_GRACE_MS`            | `10000`                 | Disconnect grace period (ms)         |
| `HUB_FORWARD_INTERVAL_MS` | `5000`                  | Odoo drain interval (ms)             |
| `HUB_FORWARD_BATCH_SIZE`  | `20`                    | Max events per drain batch           |
| `HUB_MDNS_ENABLED`        | `true`                  | Advertise via mDNS                   |
| `HUB_LOG_LEVEL`           | `info`                  | Pino log level                       |

## Storage layer

All stores use the same SQLite database with WAL mode for concurrent reads.

### SQLite schema

```sql theme={null}
CREATE TABLE orders (
  pos_reference TEXT PRIMARY KEY,
  version       INTEGER NOT NULL DEFAULT 1,
  snapshot      TEXT NOT NULL,       -- JSON OrderSnapshot
  table_id      INTEGER,
  terminal_id   TEXT,
  created_at    TEXT DEFAULT (datetime('now')),
  updated_at    TEXT DEFAULT (datetime('now'))
);

CREATE TABLE leases (
  pos_reference TEXT PRIMARY KEY,
  terminal_id   TEXT NOT NULL,
  uid           INTEGER,
  role          TEXT,
  version       INTEGER NOT NULL DEFAULT 0,
  granted_at    TEXT DEFAULT (datetime('now')),
  expires_at    TEXT NOT NULL
);

CREATE TABLE events (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  type          TEXT NOT NULL,
  pos_reference TEXT,
  terminal_id   TEXT,
  payload       TEXT,                -- JSON
  forwarded     INTEGER DEFAULT 0,
  created_at    TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_events_forwarded ON events(forwarded, id);

CREATE TABLE tables (
  table_id      INTEGER PRIMARY KEY,
  status        TEXT DEFAULT 'available',
  pos_reference TEXT,
  terminal_id   TEXT,
  updated_at    TEXT DEFAULT (datetime('now'))
);

CREATE TABLE table_locks (
  table_id      INTEGER PRIMARY KEY,
  terminal_id   TEXT NOT NULL,
  uid           INTEGER,
  user_name     TEXT,
  granted_at    TEXT DEFAULT (datetime('now')),
  expires_at    TEXT NOT NULL
);
```

### SQLite pragmas

```sql theme={null}
PRAGMA journal_mode = WAL;       -- Concurrent readers during writes
PRAGMA busy_timeout = 5000;      -- Wait 5s on lock contention
PRAGMA synchronous = NORMAL;     -- Balanced durability/performance
PRAGMA foreign_keys = ON;
```

### Store APIs

| Store            | Key operations                                                                                                               |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `orderStore`     | `upsert(ref, snapshot, terminalId)` auto-increments version; `get(ref)`, `getByTable(id)`, `count()`                         |
| `leaseStore`     | `grant()`, `renew()`, `release()`, `getExpired(now)`, `getByTerminal(id)`, `listAll()`                                       |
| `eventLog`       | `append(type, ref, terminalId, payload)`, `getPending(limit)`, `markForwarded(ids)`, `getRecent(limit)`, `count(forwarded?)` |
| `tableStore`     | `upsert(tableId, status, ref?, terminalId?)`, `get(tableId)`, `getAll()`                                                     |
| `tableLockStore` | `grant()`, `renew()`, `release()`, `getExpired(now)`, `getByTerminal(id)`, `listAll()`                                       |

## Transport-level PING/PONG

The WebSocket server handles `{"type":"PING"}` messages at the transport level before routing to protocol handlers. It responds immediately with `{"type":"PONG"}`. This keeps connections alive through proxies and NAT without requiring protocol-level message definitions.

## Message handlers

### Auth handler

`handlers/authHandler.ts`

1. Receives raw WebSocket + `AUTH` message
2. Validates session token against Odoo: `POST /pos-react/api/session` with the token as cookie
3. On success: creates `TerminalInfo`, registers in terminal registry, sends `AUTH_OK`
4. On failure: sends `AUTH_FAIL`, connection is not registered

### Lease handler

`handlers/leaseHandler.ts` — Delegates to `LeaseManager` (see [leasing](/sync/leasing)):

* `LEASE_ACQUIRE` → `leaseManager.acquire()` → sends `LEASE_GRANTED` or `LEASE_DENIED`
* `LEASE_RELEASE` → `leaseManager.release()` → appends `lease_release` event
* `LEASE_HEARTBEAT` → `leaseManager.heartbeat()` → renews lease expiry

### Order handler

`handlers/orderHandler.ts` — The core sync handler:

1. Verify terminal holds lease for `posReference`
2. Check `baseVersion >= storedVersion` (else send `CONFLICT`)
3. Upsert order in SQLite (version increments)
4. Append `order_snapshot` event to log (forwarded=0)
5. Broadcast `ORDER_UPDATED` to all terminals except sender
6. If `snapshot.tableId` is set: update table status (`occupied`, or `available` if paid)

### Table lock handler

`handlers/tableLockHandler.ts` — Delegates to `TableLockManager` (see [table locking](/sync/table-locking)):

* `TABLE_LOCK_ACQUIRE` → grants or denies, broadcasts `TABLE_LOCKS_STATE` on grant
* `TABLE_LOCK_RELEASE` → broadcasts `TABLE_LOCK_RELEASED`
* `TABLE_LOCK_HEARTBEAT` → sends `TABLE_LOCK_REVOKED` on failure
* `TABLE_LOCK_FORCE_ACQUIRE` → Server-side role check → evicts current holder, grants to requester

### Floor plan handler

`handlers/floorPlanHandler.ts` — Broadcasts floor plan changes to other terminals:

* `FLOOR_PLAN_UPDATE` → Broadcasts `FLOOR_PLAN_CHANGED` to all terminals except sender (includes `floorId`, `tables`, `layoutMode?`)

## Terminal registry

`server/terminalRegistry.ts` — In-memory map of connected terminals:

```typescript theme={null}
interface TerminalInfo {
  terminalId: string;
  deviceId: string;
  uid: number;
  role: string;
  name: string;
  ws: WebSocket;
  lastSeen: number;
}
```

Key methods: `register()`, `remove()`, `send()`, `broadcast()`, `count()`, `getAll()`.

## Odoo adapter

`upstream/odooAdapter.ts` — Converts snapshots to Odoo format and forwards.

### Snapshot transformation

| Snapshot field      | Odoo payload field                   |
| ------------------- | ------------------------------------ |
| `posReference`      | `pos_reference`                      |
| `lines[].productId` | `lines[].product_id` (parsed to int) |
| `lines[].quantity`  | `lines[].qty`                        |
| `lines[].unitPrice` | `lines[].price_unit`                 |
| `lines[].taxIds`    | `lines[].tax_ids` (parsed to int\[]) |
| `lines[].id`        | `lines[].client_line_id`             |
| `totals.total`      | `amount_total`                       |
| `totals.tax`        | `amount_tax`                         |
| `totals.subtotal`   | `amount_untaxed`                     |
| `tableId`           | `table_id`                           |
| `partnerId`         | `partner.partner_id` (parsed to int) |
| `paymentLines`      | `statement_ids`                      |

Voided lines (`isVoided: true`) are **filtered out** before forwarding.

### Endpoint selection

* New orders (no `odooId`): `POST /pos-react/api/orders/create`
* Existing orders (has `odooId`): `POST /pos-react/api/orders/update`

Both endpoints are idempotent via `pos_reference`.

## HTTP endpoints

| Endpoint            | Method  | Purpose                                                                      |
| ------------------- | ------- | ---------------------------------------------------------------------------- |
| `/health`           | GET     | Hub health: status, uptime, terminal count, orders, pending/forwarded events |
| `/status`           | GET     | Connected terminals list, active leases list, pending count                  |
| `/events`           | GET     | Last 50 events with type, reference, forwarded status, timestamp             |
| `/orders`           | GET     | Active orders with reference, table, status, item count, version             |
| `/tables`           | GET     | Table statuses with current order reference and terminal                     |
| `/table-locks`      | GET     | Active table locks with holder name, terminal, expiry                        |
| `/ticket-sequences` | GET     | Today's ticket sequences by order type (takeout, delivery)                   |
| `/reset`            | POST    | Reset all SQLite data                                                        |
| `/` or `/dashboard` | GET     | Self-contained monitoring dashboard                                          |
| `*`                 | OPTIONS | CORS preflight                                                               |

## Periodic tasks

| Task             | Interval | Action                                              |
| ---------------- | -------- | --------------------------------------------------- |
| Lease tick       | 20s      | Expire stale leases, send `LEASE_REVOKED`           |
| Table lock tick  | 15s      | Expire stale table locks, send `TABLE_LOCK_REVOKED` |
| Event forwarder  | 5s       | Drain pending events to Odoo                        |
| Status broadcast | 30s      | Send `SYNC_STATUS` to all terminals                 |

## Graceful shutdown

On `SIGTERM` or `SIGINT`:

1. Stop all periodic timers
2. Stop forwarder
3. Stop mDNS
4. Close HTTP server
5. Close SQLite database
6. Exit process

## File map

```
nu_pos_hub/src/
├── index.ts                  # Entry point, wires everything together
├── config.ts                 # Environment-based configuration
├── storage/
│   ├── db.ts                 # SQLite init, schema, WAL mode
│   ├── orderStore.ts         # Order CRUD with versioning
│   ├── leaseStore.ts         # Lease CRUD with expiry
│   ├── eventLog.ts           # Append-only event log
│   ├── tableStore.ts         # Table status CRUD
│   └── tableLockStore.ts     # Table lock CRUD with expiry
├── leasing/
│   ├── leaseManager.ts       # Lease business logic
│   └── tableLockManager.ts   # Table lock business logic
├── server/
│   ├── httpServer.ts         # Health, status, events, dashboard endpoints
│   ├── wsServer.ts           # WebSocket connection handler
│   ├── messageRouter.ts      # JSON parse, validate, route
│   └── terminalRegistry.ts   # In-memory terminal map
├── handlers/
│   ├── authHandler.ts        # AUTH message handler
│   ├── leaseHandler.ts       # LEASE_* message handler
│   ├── orderHandler.ts       # ORDER_SNAPSHOT handler
│   ├── tableHandler.ts       # TABLE_UPDATE handler
│   ├── tableLockHandler.ts   # TABLE_LOCK_* message handler
│   └── floorPlanHandler.ts   # FLOOR_PLAN_UPDATE handler
├── upstream/
│   ├── types.ts              # UpstreamAdapter interface
│   ├── odooAdapter.ts        # Odoo HTTP client
│   └── forwarder.ts          # Background event drain
├── discovery/
│   └── mdns.ts               # Bonjour/mDNS advertisement
├── dashboard/
│   └── html.ts               # Self-contained HTML dashboard
└── cli/
    └── index.ts              # CLI: stats, drain, events, reset-leases, reset-data
```
