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>
Phones can now open the waiter app at http://<LAN IP> with no domain, DNS
record or certificate (works around DNS-rebinding failures, KI-001). The
manager gets http://<LAN IP>:8081, with http://<LAN IP>/manager redirecting
there. waiter.*/manager.* hostnames on :80 still redirect to https, so
legacy domain sites behave as before.
- Both plain-HTTP servers are LAN-only (allow RFC1918/loopback/ULA/link-local,
deny all -> 403), so a router port-forward can't expose an unencrypted POS
- nginx-proxy/nginx.conf and the install.sh heredoc are now byte-identical
(one canonical config, routing map in its header)
- install.sh generates a 10-year self-signed cert when certs/ is empty (nginx
won't start its TLS listeners without one), detects HOST_IP via
'ip route get', prompts for it on fresh installs and backfills it into an
existing .env, always starts the stack, prints the LAN URLs
- docker-compose publishes 8081; .env.example documents HOST_IP
- pack README: ports/request path updated, CS-5 byte-identical check
Verified: nginx -t; install.sh in Debian (fresh / upgrade without HOST_IP /
re-run - no duplicate HOST_IP, cert SAN includes HOST_IP, key 600); full
stack from freshly built images: every entry point returns the expected
200/301/302, and removing the gateway's range from the allow list yields 403
on :80 and :8081.
Co-Authored-By: Claude Opus 5.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>
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>