services/tls_identity.py creates an EC P-256 key + self-signed cert (10y,
SAN localhost/127.0.0.1/HOST_IP) under <data dir>/tls at startup, and
re-issues the cert with the SAME key when < 2 years remain. A cert that
doesn't belong to the key is replaced. Nothing to renew by hand; a backup of
the data directory keeps the identity. Runs in lifespan before the app is
healthy, so the proxy (which waits for healthy) always finds the files.
/api/system/identity and /api/system/status now include
tls: {port: TLS_PORT (default 8443), spki_sha256} - the base64 SHA-256 of the
public key that phones pin. Adds cryptography==46.0.4.
Tests: create / restart (no change) / renewal 8 years later keeps the key
and pin / foreign cert replaced; pin equals openssl's SPKI sha256.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend's bridge network can't see the host's NICs, so HOST_IP from
install.sh went stale silently after a DHCP change. Now:
- services/netinfo_helper.py runs as a new `netinfo` service (same backend
image, network_mode: host): every 60s it picks the PHYSICAL LAN address
(main-table default-route NIC if real hardware, else first real NIC with
IPv4; never WireGuard/Tailscale/ZeroTier/bridges/veths, ignores 169.254)
and writes it to the shared `netinfo` volume. Stdlib only. On Docker
Desktop (linuxkit/WSL2 kernel) it reports "unsupported" instead of the
VM's meaningless address.
- services/lan_ip.py: one resolver used by /api/system/status (lan_ip +
lan_ip_info), the pairing QR and the cloud heartbeat's local_ip:
override (pos_settings network.lan_ip_override) → live detection (ignored
when older than 5 min) → HOST_IP. Flags `mismatch` when a pinned address
is no longer on any of the machine's NICs.
- PUT /api/system/lan-ip-override (manager): set a private IPv4 or null to
return to automatic; public/loopback/link-local/IPv6 rejected (422).
- cloud_sync._get_local_ip uses the resolver (no more socket trick that
returned the container IP).
Tested: helper selection on a fake sysfs/route table (8 cases incl. VPN
default routes) + real ioctl/route parsing on a Linux kernel; resolver
priority/staleness/mismatch/validation (18 cases); isolated full stack:
HOST_IP fallback on Docker Desktop, override save/validate/auth, simulated
Linux detection incl. DHCP change and dead helper, heartbeat IP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/system/identity (unauthenticated, license-exempt) returns
{app: 'xenia-pos', site_id, venue_name, version, api_version: 1} so a phone
can confirm which venue a server is when pairing and when rediscovering the
server after an IP change. No secrets: SITE_KEY never leaves the server.
/api/system/status now also returns site_id and lan_ip (HOST_IP only - in
Docker any auto-detected address is the unreachable bridge IP).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
local_backend now uploads each product's image file to cloud_backend
during the existing ~5 min menu sync, so the QR menu can show real
photos without needing a manually-set digital_image_url. Only
re-uploads images whose content hash changed since the last push
(Product.cloud_image_hash), to avoid re-sending unchanged binaries
every cycle.
Also adds a manual "sync now" trigger (POST /api/system/sync-menu) and
a matching button in Settings → Operation, for pushing menu/price/image
changes immediately instead of waiting for the next automatic cycle.
Co-Authored-By: Claude Sonnet 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>
- New GET /api/system/printers/scan SSE endpoint: parallel async TCP
scan of a /24 subnet, streams found/progress/done events
- New GET /api/system/printers/scan-hints: returns auto-detected local
subnets from socket.getaddrinfo, excludes loopback/docker ranges
- FindPrintersModal in Settings > Print: shows subnet+port inputs,
live progress bar, found printers list; selecting a result pre-fills
the new printer form (user still names it before saving)
- "Εύρεση εκτυπωτών" button added next to "+ Νέος εκτυπωτής"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New reservations module: model, schema, router (CRUD + status updates + upcoming alerts)
and background task for auto-expiring stale reservations
- Reports: print_products, print_categories, print_tables analytics endpoints
plus meta_products and business_day_summary for workday close/view flow
- printer_service: configurable font sizes/weights, donut/bar chart print layout helpers,
analytics print blocks per printer
- tables/schemas: surfaced color, zone, and other new fields on Table, Product, User, Printer
- demo_seed.py for quick dev DB population; wipe_database.py utility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds configurable buzzer control so the printer alerts kitchen staff
on each new ticket print.
Backend:
- printer_service.py: ESC 0x07 beep command fired before paper cut,
reads print.beep_on_ticket / print.beep_pattern settings; supports
single/double/triple/long presets and a custom:n1:n2:n3 format
- settings.py: registers the two new print.beep_* settings with
defaults (beep_on_ticket=true, beep_pattern=double)
- system.py: POST /api/system/printers/test-beep endpoint for
live testing from the dashboard
Frontend (PrintFontsTab):
- BeepSection component with preset grid, custom n1/n2/n3 inputs,
and a live test-beep button targeting any configured printer
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>