screentinker/server
Claude b44f9d4f03 Serve a beta APK alongside the stable one, and let a display move between them
1.9.26's opt-in was passive: it stopped a sideloaded build being reverted, but there was still one
APK slot and latest_version was the server's own VERSION, so a beta had to be installed by hand on
every display. This makes it a real channel.

- apk-cache tracks two slots. ScreenTinker-beta.apk is optional and reaches only displays with
  ota_beta = 1.
- A beta must DECLARE its version in a sidecar ScreenTinker-beta.apk.version. The server cannot
  infer it — stable's version is the server's own constant because the two ship together, and
  reading it from the APK means parsing binary AndroidManifest.xml on the request path. If the
  sidecar is missing or unparseable the channel does not activate at all and opted-in displays keep
  getting stable. Failing closed matters: advertising a version that does not match the bytes served
  is the OTA-loop condition this fleet has been bitten by before.
- The check and the download resolve the channel identically and fall back to stable identically, so
  apk_size always describes the bytes actually delivered. No APK change was needed — the client
  already fetches whatever download_url it is handed, so displays in the field can be moved between
  channels from the dashboard today.

Switching back needed care. Stable is semver-OLDER than the beta it replaces, so the ordinary
"never offer a downgrade" rule stranded the display and unticking the box would have been another
silent no-op. The first attempt returned any non-opted-in display running a pre-release — which
broke a #144 test, correctly: that would have dragged every existing pre-release tester back to
stable the moment their server upgraded, the exact harm the opt-in exists to prevent. So the return
now requires evidence we actually served that display the beta channel (devices.ota_channel_served,
written once on change, not per check). A tester ahead of the server on their own build is left
alone exactly as before.

Documented in the README, including the constraint that makes the switch-back physically possible:
beta builds must carry a versionCode no higher than the stable they branch from, because Android
refuses to install a lower one. Equal numbers install in both directions.

Verified end to end against a live server with two real signed APKs: stable serves 1.9.26, beta
serves 1.9.27-rc1, an unknown channel falls back to stable, removing the version file deactivates
the channel, and the full opt-in -> serve -> switch-back lifecycle produces offer / up-to-date /
channel-return in order. 859 server tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-07-30 19:12:46 -05:00
..
config fix(logging): gate CF-Connecting-IP on a Cloudflare peer, not any trusted proxy 2026-07-26 10:23:04 -05:00
db Serve a beta APK alongside the stable one, and let a display move between them 2026-07-30 19:12:46 -05:00
lib Serve a beta APK alongside the stable one, and let a display move between them 2026-07-30 19:12:46 -05:00
middleware Merge branch 'fix/recovery-grants' into release/auth-campaign 2026-07-26 16:22:59 -05:00
player Attribute a widget play to the widget that played 2026-07-28 19:06:56 -05:00
routes Let a display opt in to pre-release builds, so a test build is not reverted under the tester 2026-07-30 18:35:31 -05:00
scripts fix(content+android): rotation-aware media — portrait upright on dashboard AND player (#170) (#172) 2026-07-12 22:05:11 -05:00
services Report zero event-loop lag when a window recorded no samples 2026-07-28 14:11:32 -05:00
test Serve a beta APK alongside the stable one, and let a display move between them 2026-07-30 19:12:46 -05:00
ws Report the screen's own IP, and make the Wi-Fi name an honest optional 2026-07-29 21:57:59 -05:00
.gitignore feat(email): Microsoft Graph send + alert spam protection + preferences UI 2026-05-12 18:16:40 -05:00
config.js Add an operator override for self-update on MDM-managed panels 2026-07-28 23:07:30 -05:00
package-lock.json chore(release): v1.9.26 2026-07-30 18:43:35 -05:00
package.json chore(release): v1.9.26 2026-07-30 18:43:35 -05:00
server.js Serve a beta APK alongside the stable one, and let a display move between them 2026-07-30 19:12:46 -05:00
smoke-ui.js Keep the smoke test out of npm test, and update the lockfile 2026-07-28 20:34:34 -05:00
version.js chore(version): single-source VERSION, env-configurable data paths, bump tooling 2026-06-10 12:56:03 -05:00