Files
xenia-pos-local/docs/README.md
T
bonaminandClaude Opus 5.5 251cac9807 chore: retire the per-client domain setup (plan step 9); addresses → bonamin.net
- remove setup-ssl.sh (mkcert IP certs) - superseded by install.sh's
  self-signed cert and the backend-managed TLS identity
- manager: "Waiter Domain (παλιό σύστημα)" - shown only for sites that
  still have one
- .env.example: REGISTRY=registry.bonamin.net and
  CLOUD_URL=https://xenia-api.bonamin.net (it pointed at the admin panel,
  xenia-admin, even before the domain move); install.sh points to the
  sysadmin panel at xenia-admin.bonamin.net
- pack README registry example updated
Proxy config unchanged (HTTPS domain blocks stay so not-yet-migrated sites
keep working until their visit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:06:45 +03:00

10 KiB

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. 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://<LAN IP> (80, LAN only) · https://waiter.<domain> / https://<IP> (443) pos-waiter
manager_dashboard React + Vite 5174 http://<LAN IP>:8081 (LAN only; http://<IP>/manager redirects there) · https://manager.<domain> (443) · https://<IP>:4443 pos-manager
proxy nginx:alpine — 80, 443, 4443, 8081, 8443. See the header of nginx-proxy/nginx.conf for the full routing map. Waits for the backend to be healthy stock

Request path in production

Phones normally use plain HTTP by LAN IP (http://<HOST_IP> → proxy:80 default_server). Only private source IPs are allowed; everything else gets 403. Legacy domain sites use https://waiter.<domain> (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/tls_identity.py, waiter_pwa/android/.../TrustStore.java, src/native/tls.js Encrypted LAN link: the server's self-managed key and cert, and the app's key pinning
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 <t> 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://<LAN IP>) 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

# 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.net/pos-backend:<ver>   # + 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.

cd waiter_pwa
npm run apk:debug     # releases/xenia-waiter-<ver>-debug.apk  (WebView debuggable via chrome://inspect)
npm run apk:release   # releases/xenia-waiter-<ver>.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://<IP>/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/<you>/AppData/Local/Android/Sdk.

  • UI bundles (plan step 8): every waiter image also publishes the native UI bundle (scripts/pack-bundle.mjs → /downloads/waiter-bundle.json + zip). Phones run their venue's bundle via @capgo/capacitor-updater (manual mode, no Capgo cloud), driven by src/native/bundles.js.

CS-10. MIN_SHELL_BUILD guards native compatibility. When web code starts relying on something new in android/ (a plugin, a Java class, a manifest change), bump MIN_SHELL_BUILD in src/native/shell.js and versionCode, in the same commit. Phones with an older APK then keep their current UI and show "update the app", instead of loading a UI that calls missing native code. Every bundle must call markBundleReady() at start (BundleSync does this); otherwise it is rolled back after 15 s.

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.