The default sqlite:////app/data/cloud.db path only works inside Docker.
On Windows, Controlled Folder Access blocks writes to arbitrary paths.
Now mirrors the same pattern as local_backend/config.py:
- Windows: %LOCALAPPDATA%/xenia_cloud/cloud.db (always writable)
- Linux/Docker: next to main.py (overridden by DATABASE_URL in .env)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two Vite + React 18 + Tailwind apps under connect_frontend/.
Both use React Router v6, Axios, react-hot-toast, and Lucide icons.
Neither uses localStorage — tokens live in sessionStorage (manager-app)
or component state (menu-app cart).
── menu-app (public menu + ordering) ──────────────────────────────
Routing (basename /menu):
/:siteSlug → MenuPage
/:siteSlug/order → CartPage
/:siteSlug/confirm/:ref → OrderConfirm
Pages:
MenuPage — fetches menu snapshot, category nav tabs, product
cards; floating CartButton; opens ProductModal
ProductModal — quantity picker, quick-option toggles, price/discount
display, "Add to order" with line total
CartPage — order type selector (dine_in/delivery), customer
details form, cart summary, submits to cloud API
OrderConfirm — polls GET /api/orders/status/:ref every 10s;
renders status icon + label; stops polling on
terminal states (delivered/rejected)
Components:
CategoryNav — horizontal scrollable pill tabs
ProductCard — image, name, description, price with discount badge;
greyed + "Out of stock" badge when unavailable
CartButton — fixed bottom bar with item count + total
── manager-app (remote dashboard) ─────────────────────────────────
Routing (basename /manage):
/login → LoginPage
/ → SiteSelectorPage (auto-navigates if one site)
/:siteId → DashboardPage
/:siteId/orders → IncomingOrdersPage
/:siteId/orders/history → OrderHistoryPage
/:siteId/orders/:id → OrderDetailPage
RequireAuth wrapper redirects to /login if no sessionStorage token
Pages:
LoginPage — email/password → POST /api/manager/login;
stores JWT in sessionStorage
SiteSelectorPage — lists accessible venues; auto-selects if one
DashboardPage — polls snapshot + pending orders every 15s;
2×2 stat grid + pending order list
IncomingOrdersPage — polls pending orders every 15s; order cards
OrderDetailPage — full order detail; Accept/Reject with optional
rejection reason; lifecycle progression buttons
(Preparing → Ready → Delivered etc.)
OrderHistoryPage — all orders with status filter tabs
Components:
RequireAuth — route guard using sessionStorage token
StatCard — labelled metric tile
StatusBadge — colour-coded status pill
OrderCard — summary card linking to OrderDetailPage
(no OrderCard uses useParams to get siteId for nav — wired correctly)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Local backend needs the cloud DB integer PK to call Connect API
endpoints (menu sync, order pending poll). Heartbeat is the only
authenticated channel available at startup, so we piggyback the id
there rather than adding a new endpoint.
Changes:
- schemas/site.py: site_numeric_id: int | None added to HeartbeatResponse
- routers/heartbeat.py: site_numeric_id=site.id included in response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds four new routers, three schema files, and one new model.
All new endpoints use new URL prefixes — no existing routes touched.
routers/menu.py
GET /api/menu/{site_slug} — public menu snapshot (no auth)
POST /api/menu/sync — upsert snapshot (site API key)
routers/orders.py
POST /api/orders/{site_slug} — customer places order (public)
GET /api/orders/status/{public_ref} — customer tracks order (public)
GET /api/orders/pending/{site_id} — local backend polls (site key)
POST /api/orders/{id}/synced — mark synced (site key)
PATCH /api/orders/{id}/status — update status with transition
validation (site key)
routers/manager_auth.py
POST /api/manager/register — create manager account (admin JWT)
POST /api/manager/login — returns manager JWT
POST /api/manager/refresh — refreshes manager JWT
Shared _get_current_manager dependency used by remote_dashboard
routers/remote_dashboard.py
GET /api/remote/sites — manager JWT
GET /api/remote/sites/{id}/snapshot — manager JWT
GET /api/remote/sites/{id}/orders — manager JWT
GET /api/remote/sites/{id}/orders/pending — manager JWT
PATCH /api/remote/sites/{id}/orders/{oid}/status — manager JWT
POST /api/remote/snapshot — site API key
(stats push)
models/stats_snapshot.py (NEW)
stats_snapshots table — one row per site, holds serialized
operational stats; created by create_all on startup
config.py / .env.example
MANAGER_JWT_SECRET and MANAGER_JWT_EXPIRE_HOURS (default 72h)
added as separate settings from the admin SECRET_KEY
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds three new tables created automatically by create_all on next
startup, plus an additive migration for an existing table:
models/menu_snapshot.py (NEW)
- menu_snapshots: one row per site, holds the full serialized
category+product JSON; upserted on each sync from local_backend
models/online_order.py (NEW)
- online_orders: full order lifecycle from customer submission
through delivery; tracks sync state so local_backend can poll
for unprocessed orders (synced_to_local=0)
models/manager_account.py (NEW)
- manager_accounts + manager_site_access (M2M join table): remote
manager login accounts with per-site access control
models/site.py (MODIFIED)
- order_counter column added; incremented atomically when each
online order is created to generate "ORD-XXXX" public_ref values
main.py (MODIFIED)
- imports all three new models so create_all registers their tables
- _run_migrations() added (same pattern as local_backend) for the
additive order_counter column on the existing sites table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Site model: add waiter_domain and last_seen_local_ip columns
- HeartbeatRequest: accept optional local_ip field from local backend
- HeartbeatResponse: return waiter_domain to local backend
- heartbeat router: persist local_ip on each check-in
- SiteDetailPage: show Public IP / Local IP separately, add Waiter Domain
card with inline edit modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>