Sites now enforce their license offline (client-services KI-006): the
cloud is needed to renew a license, not to run it. Each heartbeat carries
license_token = base64url(payload).base64url(signature), payload
{v:1, site_id, active, locked, lock_reason, expires_at, issued_at}, signed
with LICENSE_SIGNING_KEY (base64 raw Ed25519 private key, cloud .env). The
matching public key is built into the site code, so a stored token can't be
edited and a clock can't be set before issued_at unnoticed.
Additive field only (old sites ignore it). Without the key the field is
null and a warning is logged. Pins cryptography==46.0.4 (was transitive).
Co-Authored-By: Claude Opus 5.5 <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>
- 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>