The sysadmin panel URL uses site.site_id (UUID string) not site.id (int PK).
All manager account endpoints were querying/expecting the integer PK — none matched.
- GET /by-site/{site_id}: param type str, filter by Site.site_id
- POST /register: site_ids type list[str], query Site.site_id.in_()
- DELETE /site-access: site_id type str, query Site.site_id
- schemas/manager.py: ManagerRegisterRequest.site_ids list[int] → list[str]
- SiteDetailPage.jsx: remove Number() casts on siteId in add/remove calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (manager_auth.py):
- register: upsert instead of 409 — if email exists, just add site access
- GET /api/manager/by-site/{site_id}: list managers linked to a site (admin auth)
- DELETE /api/manager/site-access: remove a manager's access to a site (admin auth)
Sysadmin API client:
- getManagersBySite, addManagerToSite, removeManagerSiteAccess
SiteDetailPage.jsx:
- Remote Managers section: lists all linked managers with email, name, active badge
- Add Manager modal: email + full name + password form (password ignored if account exists)
- Remove access button with optimistic removal from list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLAlchemy couldn't resolve the string 'Site' in ManagerAccount's
relationship() at query time because Site wasn't guaranteed to be
imported first. Adding the explicit import ensures the mapper is
configured correctly regardless of import order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same fix as manager_auth — OAuth2PasswordBearer was showing an OAuth2
form in Swagger instead of a plain Bearer token input, causing
'Not authenticated' when testing via the docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OAuth2PasswordBearer caused Swagger UI to show a username/password/
client_id form instead of a simple token input, making the docs
unusable for testing. HTTPBearer shows a plain 'Bearer token' field,
consistent with how the local_backend handles auth.
No runtime behaviour change — token extraction is identical.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>