feat(waiter): native app runs each venue's own UI bundle (self-hosted updater)

@capgo/capacitor-updater (MPL-2.0) in manual mode: autoUpdate off,
stats/update/channel URLs empty (nothing contacts Capgo), appReadyTimeout
15s (auto rollback), resetWhenUpdate (APK update → built-in bundle, venue
bundle re-fetched), autoDeletePrevious off (we keep one per venue).

src/native/bundles.js syncVenueBundle():
- manifest fetched over the pinned TLS link; zip downloaded natively over
  the LAN HTTP port with the MANDATORY sha256 check - a tampered zip never runs
- applied at once only when nobody is mid-service (after pairing / venue
  switch, or no one logged in); otherwise staged with next() → applied at the
  next background/restart; never a mid-service reload
- APK too old for the bundle (min_shell_build) → skipped + update banner
  linking to the venue's /downloads/xenia-waiter.apk
- a rolled-back version is blocklisted (attempt tracking + grace window) -
  without it the app re-applied the still-advertised broken bundle in a loop
- server without bundles (404) → built-in UI; bundles no venue needs deleted
BundleSync: notifyAppReady on start; sync at start, on resume, every 30 min.
saveVenue merges fields (bundleVersion); switchVenue flags immediate apply.

E2E (emulator vs two isolated venue stacks): fresh pairing applies the venue
bundle and it stays healthy; logged-in update staged, applied after
background, session kept; tampered zip refused; shell-too-old banner;
broken bundle rolled back automatically and not retried (80s + resume);
two venues each run their own bundle, round trips served from cache with 0
downloads, stale bundles cleaned; no *.capgo.app traffic. Step 5/7-era
rediscovery + cold-start suites and web modes still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 18:24:48 +03:00
co-authored by Claude Opus 5.5
parent a1e415ac05
commit fe264f0a23
9 changed files with 320 additions and 4 deletions
+18
View File
@@ -11,6 +11,7 @@
"@capacitor/android": "^8.5.2",
"@capacitor/app": "^8.1.1",
"@capacitor/core": "^8.5.2",
"@capgo/capacitor-updater": "^8.51.25",
"@tanstack/react-query": "^5.100.11",
"axios": "^1.15.1",
"dexie": "^4.4.2",
@@ -31,6 +32,7 @@
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"fflate": "^0.8.3",
"globals": "^17.5.0",
"vite": "^8.0.9"
}
@@ -1584,6 +1586,15 @@
"tslib": "^2.1.0"
}
},
"node_modules/@capgo/capacitor-updater": {
"version": "8.51.25",
"resolved": "https://registry.npmjs.org/@capgo/capacitor-updater/-/capacitor-updater-8.51.25.tgz",
"integrity": "sha512-lznyAv9Ktl1CD6/OKZY+lQZl9ke+dbadLEB2XQKP7VbQO8/5D8yOHS+pysQgVnNkiZYEC3v1AxgY4a5Z+DnxXw==",
"license": "MPL-2.0",
"peerDependencies": {
"@capacitor/core": "^8.0.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
@@ -3852,6 +3863,13 @@
}
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",