# client-services — On-site Pack Everything that runs **inside the restaurant**, on the local server box. This folder is its **own git repo**, nested inside the `xenia-pos` parent repo. > **Read the global rules first:** [../../docs/README.md](../../docs/README.md). This file adds only the rules specific to this pack. > The feature catalog, the cloud contract, known issues and site records all live in the parent's `docs/`. --- ## Services | Service | Stack | Dev port | Prod exposure | Image | |---|---|---|---|---| | `local_backend` | FastAPI + SQLAlchemy + SQLite (`/app/data/pos.db`) | 8000 | only through proxy / inner nginx | `pos-backend` | | `waiter_pwa` | React + Vite + vite-plugin-pwa, axios, zustand, react-query, Dexie (IndexedDB) | 5173 | **`http://`** (80, LAN only) · `https://waiter.` / `https://` (443) | `pos-waiter` | | `manager_dashboard` | React + Vite | 5174 | **`http://:8081`** (LAN only; `http:///manager` redirects there) · `https://manager.` (443) · `https://:4443` | `pos-manager` | | `proxy` | nginx:alpine | — | 80, 443, 4443, 8081. See the header of `nginx-proxy/nginx.conf` for the full routing map | stock | ### Request path in production Phones normally use **plain HTTP by LAN IP** (`http://` → proxy:80 default_server). Only private source IPs are allowed; everything else gets 403. Legacy domain sites use `https://waiter.` (proxy:443), and `waiter.*`/`manager.*` on port 80 still redirect to https. ``` phone ──http(s)──▶ proxy:80/443 ──http──▶ waiter_pwa nginx:80 ──┬─ / → static SPA ├─ /api/ → backend:8000 ├─ /api/ws/ → backend:8000 (WebSocket upgrade) └─ /static/ → backend:8000/static/ ``` Every hop must forward WebSocket upgrades: `proxy_http_version 1.1`, plus `Upgrade` and `Connection` headers, plus a long `proxy_read_timeout`. The manager calls the API **same-origin, with relative paths**. The waiter app resolves every backend URL through `waiter_pwa/src/config/server.js`: same-origin when served by the venue's server, absolute when running in the native app (see CS-8). Don't hardcode hosts. ## Key files | File | Why it matters | |---|---| | `local_backend/main.py` | App setup, router registration, CORS, **`_run_migrations()`** | | `local_backend/services/lan_ip.py`, `services/netinfo_helper.py` | Which LAN IP phones get (override → live detection → `HOST_IP`); the `netinfo` host-network helper | | `local_backend/services/cloud_sync.py` | Every call to the cloud (see the parent's `docs/reference/cloud-contract.md`) | | `local_backend/roles.py`, `routers/deps.py` | Roles, permission checks, auth dependencies | | `local_backend/routers/ws.py` | Real-time event stream (seq + cursor replay) | | `waiter_pwa/src/config/server.js` | **Where the backend is**: active venue, `apiBase()`, `wsUrl()`, `assetUrl()`, `storageKey()`, `dbName()`, `deliveryMode()` | | `waiter_pwa/android/`, `capacitor.config.json` | Native Android app (Capacitor 8). See "Native Android app" below | | `waiter_pwa/src/native/pairing.js`, `src/pages/VenuesPage.jsx` | Pairing with a venue (QR / typed address) and the venue switcher | | `waiter_pwa/src/api/client.js` | axios instance: auth header, 401 → logout, network error → offline | | `waiter_pwa/src/context/SSEContext.jsx` | Real-time lifecycle, event → store/cache updates, visibility refresh | | `waiter_pwa/src/db/posdb`, `src/services/offlineOrders` | Offline cache and queued orders | | `nginx-proxy/nginx.conf` | Proxy config used in dev / by hand | | `install.sh` | Site installer. **Writes its own copy of the proxy config.** | | `docker-compose.yml` / `docker-compose.dev.yml` | Prod (pull images) / dev override (build + expose ports) | --- ## Pack-specific rules **CS-1. Schema changes go through `_run_migrations()`.** A new table: `create_all` handles it. A new column on an existing table: append an `ALTER TABLE ADD COLUMN ...` to the `migrations` list in `local_backend/main.py`, with a SQLite-safe default for `NOT NULL`. Add the column to the SQLAlchemy model **and** the Pydantic schema in the same commit. Never drop or rename columns. **CS-2. New real-time events need both ends.** If the backend emits a new event type, handle it in `waiter_pwa/src/context/SSEContext.jsx` (and in the manager, if relevant), or state explicitly that it's ignored. Events carry IDs; clients re-fetch the full object (`/api/orders/{id}`) instead of trusting partial payloads. **CS-3. The waiter app must survive bad networks.** Waiters walk in and out of WiFi range and phones sleep. Any new waiter flow must: - work from the IndexedDB cache when offline, or clearly block with an offline state - never lose an order that was entered (queue it) - tolerate duplicate or replayed events **CS-4. Money and prices are snapshotted.** Prices, costs and discounts are copied onto order items and logs when the action happens (snapshot pattern, `PriceEventLog`). Reports read the snapshots, never the live catalogue. **CS-5. The proxy config lives in two places and must stay byte-identical.** `install.sh` writes the heredoc on client sites, and `nginx-proxy/nginx.conf` is the repo copy. Edit both together (global Rule 9) and check with: `sed -n "/<< 'EOF'/,/^EOF/p" install.sh | sed '1d;$d' | diff - nginx-proxy/nginx.conf` The plain-HTTP servers (80 default_server, 8081) must keep their LAN-only `allow`/`deny` block. **CS-6. Permissions are checked on the backend.** Hiding a button in the UI is not access control. Every new endpoint declares its role or `perm_*` requirement through `routers/deps.py`. **CS-8. The waiter app never assumes it's served by the backend.** The same waiter build runs same-origin (https domain, or plain `http://`) **and** inside the native app talking to a remote venue server. So in `waiter_pwa`: - REST goes through `api/client.js` (its `baseURL` is `apiBase()`). Any other fetch, `EventSource` or `WebSocket` must build its URL with `apiBase()` or `wsUrl()`. - Backend-served files (`/static/...` images, avatars) are rendered through `assetUrl()`. - Persistent **venue-specific** data (token, IDs of products/tables/zones) uses `storageKey()` for localStorage keys and zustand `persist` names. The IndexedDB name comes from `dbName()`. Device preferences (theme, payment safety…) stay global. - Features that need a secure context (service worker, camera, notifications, clipboard) must degrade gracefully when `deliveryMode()` is `'plain-web'`. **CS-7. Greek-market realities.** Staff UIs are used by Greek staff and must render Greek correctly, including on thermal printers (codepage). Money uses €. ## Common commands ```powershell # Dev: backend with reload cd local_backend; uvicorn main:app --reload --port 8000 # Dev: frontends (Vite proxies /api and /api/ws to :8000) cd waiter_pwa; npm run dev # :5173 cd manager_dashboard; npm run dev # :5174 cd waiter_pwa; npm run lint # Build release images (VERSION comes from .env). See the parent's DEPLOYMENT_GUIDE.md. docker compose -f docker-compose.yml -f docker-compose.dev.yml build docker push registry.bonamin.gr/pos-backend: # + pos-waiter, pos-manager ``` ## Native Android app (waiter_pwa) The same waiter code, bundled into an APK with Capacitor 8. It is sideloaded, not on Play Store yet. ```powershell cd waiter_pwa npm run apk:debug # releases/xenia-waiter--debug.apk (WebView debuggable via chrome://inspect) npm run apk:release # releases/xenia-waiter-.apk, signed, AND copied to public/downloads/ ``` - **Signing key:** `%USERPROFILE%\.xenia\xenia-release.jks` + `keystore.properties`. It lives **outside the repo; back it up.** A lost key means no phone can install updates (uninstall and reinstall on every phone). Cert SHA-256 `98:A2:60:25:…:61:7A`. - **Versioning:** bump **both** `versionCode` and `versionName` in `android/app/build.gradle` for every APK handed out. Android refuses an update whose versionCode isn't higher. - **Distribution:** `apk:release` copies the APK into `public/downloads/`, so the next web build (and Docker image) serves it at `http:///downloads/xenia-waiter.apk`. The browser-mode install banner links there. **Order: `apk:release` → `docker compose … build`.** - **Native specifics:** - `androidScheme: http` (origin `http://localhost`) - cleartext allowed via `network_security_config.xml` - no service worker (`vite build --mode native` → `dist-native/`) - back button handled in `src/native/backButton.js` - first run shows only the pairing screen - `android/local.properties` (SDK path) is gitignored. Recreate it on a new machine: `sdk.dir=C:/Users//AppData/Local/Android/Sdk`. **CS-9. Anything a native build needs must survive `npx cap sync`.** `android/app/src/main/assets/public` and the generated configs are rebuilt on every sync, so never edit them. Native changes go in `android/app/src/main/**` (manifest, res, java) or `capacitor.config.json`. ## Testing There is no automated test suite yet. Before committing: - the backend starts cleanly against an **existing** `pos.db` (this catches missing migrations) - `npm run build` passes for any frontend you touched - the flow was exercised by hand (the parent's `PLANS AND STRATEGIES/TESTING_CHECKLIST.md`) - printing was checked against real hardware or `esc-pos-emulator` Say in the commit or hand-off what was and wasn't verified.