- Android: MainActivity installs a WebViewClient whose onReceivedSslError
proceeds only when the server's SPKI SHA-256 is in TrustStore (pins of all
paired venues, SharedPreferences); XeniaTlsPlugin lets JS set that list.
Spike showed this one callback covers fetch/XHR, images AND WebSockets.
Pins are per key, not per address: IP changes, renewals and expiry never
need action; a different key is always refused.
- Pairing: QR key (k=) must equal the server's advertised pin, else refused;
typed addresses trust the advertised key on first use. Then the venue moves
to https://<ip>:<tls.port> (stays on HTTP if that port isn't reachable yet).
- upgradeToTls(): on every start, a plain-HTTP venue moves onto TLS once the
server offers it (never accepting a key different from the stored one).
- main.jsx pushes the venues' pins to native before the first request.
- Rediscovery made proactive: checks the saved address at start and every
minute while the live connection is down, instead of waiting for requests
to an unanswered IP to time out (minutes). HTTPS probes get 5s: the first
TLS connection in a fresh process takes ~2s (measured).
E2E on the emulator vs an isolated stack: wrong-key QR refused; pairing on
TLS; tables + wss live; impostor server with another key refused natively;
HTTP venue upgraded on start; server cert renewed (same key) - app keeps
working without re-pairing; rediscovery over TLS (11s). Step 5 HTTP
rediscovery (now 2.7s/4.8s) and cold-start login tests, and web modes, pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the saved server address stops answering, the native app re-checks it
once and then probes every address in the same /24 (same scheme and port,
nearest first, 48 in parallel, 1.5s timeout) for /api/system/identity,
switching only to the server that reports THIS venue's site_id - never to
"any Xenia server". A found address is saved on the venue and the app
reloads onto the same screen (/offline → start page) after a short
"Ο server άλλαξε διεύθυνση" notice; the offline queue syncs after reload.
- src/native/rediscovery.js: subnet candidates + scan (pure, injectable)
- src/native/autoRediscover.js: re-check first, one scan at a time,
automatic attempts at most once a minute; skipped for dev / site-less venues
- ServerRediscovery: triggers when the connection is confirmed offline
(retry every 2 min) or any request fails with a network error
- Offline page: manual "Αναζήτηση server στο δίκτυο" with progress and
"not found → scan the QR" guidance
Tests: 11 unit checks (ordering, port kept, non-IP hosts, other venue never
chosen, early stop, progress). Emulator E2E with a real address change
(.99 → .2): logged in → found in ~3.5s, back on /tables with live WS;
logged out → found in ~6.5s, waiter list loads; server really down →
address untouched, manual search reports not found. Web modes unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AuthRehydrator called logout() on ANY failure of /api/auth/me, including a
network error. So opening the app while WiFi was flaky, while the server was
restarting, or right after its IP changed silently threw the waiter back to
the login screen and offline mode could not survive an app restart. Now a
network error keeps the token and retries every 5s; a real server answer
(401 and other errors) still ends the session as before.
Verified on the Android emulator: cold start with the backend stopped keeps
the token; starting the backend brings the app to /tables on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- First run in the native app shows only the pairing screen (VenuesPage):
scan the manager's pairing QR or type the server address; the server must
answer /api/system/identity as xenia-pos with a supported api_version, and
a QR's ?pair=<site_id> must match - a code from another venue is refused
- /venues (native only): list of paired venues, switch (reloads into that
venue's own token/IndexedDB), remove (data kept - may hold unsynced
orders). Reachable from the login screen, the user menu and the offline page
- QrScannerModal: WebView camera via qr-scanner (no Google Play Services
dependency, carries over to iOS); requests the camera once up front so the
Android prompt appears a single time and "denied" gets its own message
- Hardware back: @capacitor/app listener - no-op on root screens (/, /tables,
/login), otherwise history back. Path-based because Chrome's history
intervention makes the WebView report canGoBack=false for the sentinel
entry AndroidBackGuard pushes
- Before pairing no IndexedDB is opened, so nothing is created under the
un-namespaced name
- InstallAppBanner now shows in plain-HTTP browser mode only when the venue
server actually serves the APK (HEAD /downloads/xenia-waiter.apk)
Verified on an Android 15 emulator (API 35) with real taps via adb and state
via WebView DevTools: wrong address -> error; other venue's QR -> refused;
pairing -> venue-namespaced storage, live WebSocket; back on /tables keeps the
app open; restart keeps venue + session; second venue pairs logged-out and
switching back keeps venue 1's session; camera deny -> one prompt + message;
allow -> live preview. Signed release APK installed and paired. Web modes
(same-origin, plain-HTTP LAN IP, VITE_SERVER_URL) re-verified in Edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capacitor.config.json: appId gr.bonamin.xenia, webDir dist-native,
androidScheme http (origin http://localhost - no mixed-content block when
calling venue servers over plain HTTP on the LAN)
- vite: `--mode native` builds to dist-native with the PWA plugin disabled
(no service worker inside the app)
- Android: minSdk 24 / target 36; CAMERA permission for QR pairing;
cleartext allowed via network_security_config (LAN IPs can't be listed
per-domain); allowBackup=false so backups never carry login tokens
- Release signing reads ~/.xenia/keystore.properties (override with
XENIA_KEYSTORE_PROPS) - the key never enters the repo; versionName 1.0.0 /
versionCode 1 with a bump-both rule for sideloaded updates
- npm scripts build:native, apk:debug, apk:release (scripts/build-apk.mjs);
APKs land in releases/ (gitignored); release APK is also copied to
public/downloads/ so venue servers serve it at /downloads/xenia-waiter.apk
- waiter nginx: /downloads/ served as application/vnd.android.package-archive,
real 404 when missing (never falls through to index.html)
- launcher icons + splash generated from the app icon (assets/ is the source)
- .gitattributes: gradlew LF, *.bat CRLF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend:
- Add can_cancel_orders to User model and schema
- Add global orders.waiter_cancellations_allowed setting (migration)
- Cancel endpoints: mark items with cancelled_by/cancelled_at, fire cancellation print
- print_cancellation_ticket: routes to same printer zones, prints ΑΚΥΡΩΣΗ banner
- Fix cancellations_log: date filter, waiter filter, join syntax
- shift/orders: add cancellations count and hours_worked per waiter
- _enrich_shift: add cancellations count to shift data
- Add cancel-permissions endpoint for PWA
Manager dashboard:
- Global cancel setting toggle in Settings > Operation > Shift Settings
- Per-waiter can_cancel_orders checkbox in staff modal
- Manager cancel flow: print confirmation prompt (Ναι/Όχι) in DashboardPage
- ShiftsOverview: Ακυρώσεις column per shift
- Activity: multi-bar chart with ORDERS/ITEMS/CANCELLATIONS/ΕΣΟΔΑ/ΩΡΕΣ checkboxes,
grouped/stacked switch, right X-axis for hours, full waiter name on hover
- OrderHistory: cancelled items count column per order
- WorkDaySummary drill-down: cancelled items column in orders tab
Waiter PWA:
- Replace 3 pills with CLEAR | ALL | ACTIONS
- ACTIONS opens ItemActionModal for selected items
- ItemActionModal: ORDER AGAIN, MOVE TO OTHER TABLE, SPLIT, CANCEL ORDER
- ActionsSheet: Cancel Παραγγελίας option (greyed if no permission)
- CancelConfirmModal: requires confirmation before cancelling
- TableListPage: cancel order from long-press quick modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Includes all work to date:
- local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync
- manager_dashboard: React manager UI with product/category management, reports, settings
- waiter_pwa: React PWA for waiter devices
- Category reparent endpoint and UI
- Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response
- QR code modal in AppInfoTab for waiter domain
- Product form: number input spinners removed, category pre-selected on new product
- Category row: count badge moved to far right
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>