Skip to main content

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 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:
VariableDefaultDescription
HUB_PORT8765WebSocket server port
HUB_HTTP_PORT8766HTTP server port (health, dashboard)
HUB_DB_PATH./hub.sqliteSQLite database file path
ODOO_URLhttp://localhost:8069Odoo instance URL
ODOO_DBposOdoo database name
ODOO_USERadminOdoo admin username for hub auth
ODOO_PASSWORDadminOdoo admin password
HUB_LEASE_TTL_MS60000Lease validity (ms)
HUB_HEARTBEAT_MS20000Heartbeat / tick interval (ms)
HUB_GRACE_MS10000Disconnect grace period (ms)
HUB_FORWARD_INTERVAL_MS5000Odoo drain interval (ms)
HUB_FORWARD_BATCH_SIZE20Max events per drain batch
HUB_MDNS_ENABLEDtrueAdvertise via mDNS
HUB_LOG_LEVELinfoPino log level

Storage layer

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

SQLite schema

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

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

StoreKey operations
orderStoreupsert(ref, snapshot, terminalId) auto-increments version; get(ref), getByTable(id), count()
leaseStoregrant(), renew(), release(), getExpired(now), getByTerminal(id), listAll()
eventLogappend(type, ref, terminalId, payload), getPending(limit), markForwarded(ids), getRecent(limit), count(forwarded?)
tableStoreupsert(tableId, status, ref?, terminalId?), get(tableId), getAll()
tableLockStoregrant(), 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):
  • LEASE_ACQUIREleaseManager.acquire() → sends LEASE_GRANTED or LEASE_DENIED
  • LEASE_RELEASEleaseManager.release() → appends lease_release event
  • LEASE_HEARTBEATleaseManager.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):
  • 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:
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 fieldOdoo payload field
posReferencepos_reference
lines[].productIdlines[].product_id (parsed to int)
lines[].quantitylines[].qty
lines[].unitPricelines[].price_unit
lines[].taxIdslines[].tax_ids (parsed to int[])
lines[].idlines[].client_line_id
totals.totalamount_total
totals.taxamount_tax
totals.subtotalamount_untaxed
tableIdtable_id
partnerIdpartner.partner_id (parsed to int)
paymentLinesstatement_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

EndpointMethodPurpose
/healthGETHub health: status, uptime, terminal count, orders, pending/forwarded events
/statusGETConnected terminals list, active leases list, pending count
/eventsGETLast 50 events with type, reference, forwarded status, timestamp
/ordersGETActive orders with reference, table, status, item count, version
/tablesGETTable statuses with current order reference and terminal
/table-locksGETActive table locks with holder name, terminal, expiry
/ticket-sequencesGETToday’s ticket sequences by order type (takeout, delivery)
/resetPOSTReset all SQLite data
/ or /dashboardGETSelf-contained monitoring dashboard
*OPTIONSCORS preflight

Periodic tasks

TaskIntervalAction
Lease tick20sExpire stale leases, send LEASE_REVOKED
Table lock tick15sExpire stale table locks, send TABLE_LOCK_REVOKED
Event forwarder5sDrain pending events to Odoo
Status broadcast30sSend 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