feat(manager): pairing QR carries the server's TLS key pin (&k=)

The native app refuses a server whose key differs from the QR, then talks
to it encrypted on :8443.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 17:41:25 +03:00
co-authored by Claude Opus 5.5
parent 2b9f841bbd
commit 7811bf5dcb
@@ -435,7 +435,7 @@ function LanAccessSection({ status, onShowQr }) {
<span className="font-medium text-gray-800 text-xs font-mono break-all">http://{ip}</span>
<button
onClick={() => onShowQr({
url: waiterPairUrl(ip, status?.site_id),
url: waiterPairUrl(ip, status?.site_id, status?.tls?.spki_sha256),
caption: 'Σαρώστε με την κάμερα του κινητού — ανοίγει την εφαρμογή σερβιτόρου. Το κινητό πρέπει να είναι στο ίδιο WiFi.',
})}
className={SMALL_BTN}
@@ -454,8 +454,14 @@ function LanAccessSection({ status, onShowQr }) {
// Pairing URL: opens the waiter app when scanned with the phone camera, and the
// native app reads the same code (?pair=<site_id>) to pair with this venue.
function waiterPairUrl(lanIp, siteId) {
return `http://${lanIp}/` + (siteId ? `?pair=${encodeURIComponent(siteId)}` : '')
// k = the server's TLS public-key pin: the app refuses a server whose key
// differs, then talks to it encrypted on :8443 (plan step 7).
function waiterPairUrl(lanIp, siteId, spki) {
const params = new URLSearchParams()
if (siteId) params.set('pair', siteId)
if (siteId && spki) params.set('k', spki)
const query = params.toString()
return `http://${lanIp}/` + (query ? `?${query}` : '')
}
function formatUptime(seconds) {