Commit Graph
14 Commits
Author SHA1 Message Date
bonaminandClaude Opus 5.5 a349043abb feat(waiter): install-app dialog (first visit) + "Εγκατάσταση εφαρμογής" in the side drawer
Replaces the thin bottom banner with a proper dialog (taller than wide):
app icon, four reasons to install (starts instantly, survives WiFi drops,
encrypted link, finds the server + updates itself), the three install
steps, "Λήψη εφαρμογής" and "Όχι τώρα". It opens by itself once, on the
first browser visit; closing it or tapping download marks it seen for good
(previously a 7-day dismissal, and once gone there was no way back).
Afterwards it is reachable from the side drawer, right above Settings.

Offered only where it makes sense: a browser on an Android phone (never in
the native app, on iPhones or desktops) and only if this venue's server
actually serves the APK (HEAD /downloads/xenia-waiter.apk, checked once).

Verified in Edge emulating Android / iPhone / desktop against the prod
build: auto-opens once, "Όχι τώρα" persists across reloads, drawer item
above Settings reopens it, not offered on iPhone/desktop or when the
server has no APK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:01:05 +03:00
bonaminandClaude Opus 5.5 fe264f0a23 feat(waiter): native app runs each venue's own UI bundle (self-hosted updater)
@capgo/capacitor-updater (MPL-2.0) in manual mode: autoUpdate off,
stats/update/channel URLs empty (nothing contacts Capgo), appReadyTimeout
15s (auto rollback), resetWhenUpdate (APK update → built-in bundle, venue
bundle re-fetched), autoDeletePrevious off (we keep one per venue).

src/native/bundles.js syncVenueBundle():
- manifest fetched over the pinned TLS link; zip downloaded natively over
  the LAN HTTP port with the MANDATORY sha256 check - a tampered zip never runs
- applied at once only when nobody is mid-service (after pairing / venue
  switch, or no one logged in); otherwise staged with next() → applied at the
  next background/restart; never a mid-service reload
- APK too old for the bundle (min_shell_build) → skipped + update banner
  linking to the venue's /downloads/xenia-waiter.apk
- a rolled-back version is blocklisted (attempt tracking + grace window) -
  without it the app re-applied the still-advertised broken bundle in a loop
- server without bundles (404) → built-in UI; bundles no venue needs deleted
BundleSync: notifyAppReady on start; sync at start, on resume, every 30 min.
saveVenue merges fields (bundleVersion); switchVenue flags immediate apply.

E2E (emulator vs two isolated venue stacks): fresh pairing applies the venue
bundle and it stays healthy; logged-in update staged, applied after
background, session kept; tampered zip refused; shell-too-old banner;
broken bundle rolled back automatically and not retried (80s + resume);
two venues each run their own bundle, round trips served from cache with 0
downloads, stale bundles cleaned; no *.capgo.app traffic. Step 5/7-era
rediscovery + cold-start suites and web modes still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:24:48 +03:00
bonaminandClaude Opus 5.5 a1e415ac05 feat(waiter): venue servers publish the native UI bundle (manifest + zip)
- scripts/pack-bundle.mjs zips dist-native deterministically (sorted paths,
  fixed mtimes) into dist/downloads/waiter-bundle-<hash>.zip and writes
  waiter-bundle.json {format, version "<APP_VERSION>-<hash12>", file,
  sha256 (hex), size, min_shell_build, app_version, built_at}
- src/native/shell.js: MIN_SHELL_BUILD - lowest APK versionCode a bundle
  runs in (bump with native changes)
- Dockerfile: node 22; builds web + native bundle + pack (APP_VERSION build
  arg, from ${VERSION} in docker-compose.dev.yml)
- vite native mode strips public/downloads from dist-native - the 10.8 MB
  APK was being copied into the native build (and would have ended up inside
  the next APK); bundle 15 MB → 5 MB
- nginx /downloads/: json/zip types, CORS * (the app reads the manifest from
  origin http://localhost), no-cache
- .dockerignore: android/, releases/, dist-native/ out of the build context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:24:48 +03:00
bonaminandClaude Opus 5.5 18f13f12dd feat(waiter): encrypted, key-pinned connection to the venue server (native app)
- 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>
2026-09-28 17:41:25 +03:00
bonaminandClaude Opus 5.5 ab82598859 feat(waiter): native app finds the venue server again after its IP changes
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>
2026-09-28 17:00:44 +03:00
bonaminandClaude Opus 5.5 cae5d75005 fix(waiter): don't log the waiter out when the server is unreachable at startup
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>
2026-09-28 17:00:33 +03:00
bonaminandClaude Opus 5.5 a486e3ff63 feat(waiter): native pairing, venue switcher, QR scanner, Android back handling
- 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>
2026-09-28 13:31:23 +03:00
bonaminandClaude Opus 5.5 0b0f5c60c3 feat(waiter): Capacitor 8 Android project, native build mode, signed APK tooling
- 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>
2026-09-28 13:31:23 +03:00
bonaminandClaude Opus 5.5 18012c2c95 feat(waiter): configurable server/venue layer for native app and plain-HTTP browser mode
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>
2026-09-28 11:38:42 +03:00
bonaminandClaude Sonnet 5 34ae328b0d feat: bump client-services (accumulated feature work + deploy fixes)
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>
2026-07-19 10:00:14 +03:00
bonaminandClaude Sonnet 4.6 8a5a6f8be9 feat: Feature 2 — waiter order cancellation + report updates
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>
2026-06-09 12:47:02 +03:00
bonaminandClaude Sonnet 4.6 ac7ec45279 feat(frontend): workday summary modal, print analytics UI, staff zone-PIN management, report upgrades
manager_dashboard:
- DashboardPage: WorkdaySummaryModal integration (view / close-day flows); revenue chart upgrades
- WorkdaySummaryModal: new component — shows full shift/revenue summary before closing the day
- StaffTab: full rewrite — zone-assignment modal, reset-PIN modal, actions dropdown, richer staff card
- ProductsTab: digital-menu fields surfaced in product form
- PrintFontsTab: expanded font-size/weight controls per printer channel
- TablesConfigTab / TablesPage / tableColourStore: color picker and zone fields for tables
- FilterBar: unified filter component with date-range, printer, and category selectors
- CategoryPerformance / ProductPerformance / TableAnalytics: donut/bar charts,
  thermal+browser print modals, richer breakdown tables
- PrinterHistory: redesigned with per-printer drill-down and summary blocks
- TrafficAnalytics / WorkDaySummary / RevenueTrends / ShiftsOverview / StaffLeaderboard: polish pass
- tokens.js: design token updates

waiter_pwa:
- TableCard: show table color indicator from zone/colour config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:28:23 +03:00
bonaminandClaude Sonnet 4.6 5de89a722c feat: major dashboard & waiter PWA overhaul
- Manager dashboard: replaced monolithic DashboardTab/OperationsPage with new
  DashboardPage; added OrderDetailModal, ShiftDetailModal, DeleteConfirmModal,
  PaymentMethodModal; updated Sidebar routing and App navigation
- Reports: reworked WorkDaySummary, OrderHistory, ShiftsOverview with detail modals
- Backend routers: extended orders, reports, shifts, products, business_day endpoints;
  updated cloud_sync service
- Waiter PWA: refreshed app icons, improved ConnectionLostModal UX, updated
  TableCard, SSEContext, connectionStore; added useProductCache hook; vite config tweaks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:24:54 +03:00
bonaminandClaude Sonnet 4.6 8ba8c95ecd feat: initial commit — local services (backend + manager dashboard + waiter PWA)
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>
2026-05-20 14:04:38 +03:00