New waiter_pwa/src/config/server.js is the single place that knows where the backend is. Served by the venue's server (https domain or http://<LAN IP>) nothing changes: same-origin URLs and the original storage keys, so existing installs keep their token and unsynced offline queue. With an active venue (native app, or dev builds with VITE_SERVER_URL) URLs become absolute and all venue data is namespaced by siteId: token/savedUsername keys, the Dexie DB (pos_snapshot__<siteId>, which also covers the WS cursor), favorites and table-view prefs. Switching venue reloads the app. - api client baseURL, WebSocket and SSE URLs routed through the layer - product images / waiter avatars rendered via assetUrl() - service-worker update prompt skipped in native builds - InstallAppBanner: shown only in plain-HTTP browser mode and only when VITE_APP_DOWNLOAD_URL is set at build time (dismiss for 7 days) - VITE_SERVER_URL override is DEV-only (a URL-controlled server in prod would let a crafted link capture PINs) - pack README: rule CS-8 on never assuming same-origin Verified with Playwright/Edge against a local backend: prod build same-origin, prod build via LAN IP over plain HTTP (insecure context, no SW, banner shown), and dev build pointed at the backend by URL - all three log in, reach /tables and receive the WebSocket 'ready' frame; storage keys and IndexedDB names are as expected. Lint: no new problems (103 before/after). Build passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
6.5 KiB
Markdown
107 lines
6.5 KiB
Markdown
# 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 | `https://<host>` (443) | `pos-waiter` |
|
|
| `manager_dashboard` | React + Vite | 5174 | `https://<host>:4443` | `pos-manager` |
|
|
| `proxy` | nginx:alpine, TLS termination | — | 80 → 443 redirect, 443, 4443 | stock |
|
|
|
|
### Request path in production
|
|
```
|
|
phone ──https──▶ proxy: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/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/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.**
|
|
Edit `nginx-proxy/nginx.conf` **and** the heredoc in `install.sh` together (global Rule 9).
|
|
|
|
**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
|
|
|
|
```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:<ver> # + pos-waiter, pos-manager
|
|
```
|
|
|
|
## 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.
|