From 8d03741713b94a5343b1f3994eddf04b197e20cf Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Fri, 12 Jun 2026 20:35:07 -0500 Subject: [PATCH] feat(server): make OTA observable - log update-check + apk-download hits (#96) The OTA was invisible server-side: /api/update/check and /download/apk returned without logging, which is part of why the 1.9.0 auto-relaunch failure went unseen. Log every version check (client version vs latest, update_available, whether an APK is staged) and every APK download (a device actually applying an OTA), keyed on the CF-aware getClientIp so production logs show the real per-device IP behind Cloudflare, not the edge. Observability for the #96 auto-relaunch work (this is how we'll watch the OTA fire during the relaunch testing). Part of #96. Co-Authored-By: Claude Opus 4.8 (1M context) --- server/server.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/server.js b/server/server.js index fcbdbef..2c64966 100644 --- a/server/server.js +++ b/server/server.js @@ -521,6 +521,11 @@ app.get('/api/update/check', (req, res) => { const latestVersion = VERSION; const updateAvailable = currentVersion && currentVersion !== latestVersion; + // #96: log every version check so the OTA is observable - which devices check in, their + // version, and whether they'll update. This diagnosability gap is part of why the 1.9.0 + // relaunch failure went unseen. + console.log(`[ota] update check from ${getClientIp(req)}: client=${currentVersion || 'unknown'} latest=${latestVersion} update_available=${!!updateAvailable} apk=${apkExists ? 'present' : 'MISSING'}`); + res.json({ latest_version: latestVersion, current_version: currentVersion || 'unknown', @@ -638,11 +643,15 @@ function resolveApkPath() { app.get('/download/apk', (req, res) => { const apkPath = resolveApkPath(); if (apkPath) { + // #96: an APK download means a device is actually applying an OTA - log it so the + // update is observable end to end (check -> download -> [relaunch]). + console.log(`[ota] APK download by ${getClientIp(req)} (${fs.statSync(apkPath).size} bytes) - OTA update in progress`); res.setHeader('Content-Type', 'application/vnd.android.package-archive'); res.setHeader('Content-Disposition', 'attachment; filename="ScreenTinker.apk"'); res.setHeader('Cache-Control', 'no-cache'); res.sendFile(apkPath); } else { + console.warn(`[ota] APK download requested by ${getClientIp(req)} but no APK is available (404)`); res.status(404).send(`APK Not Found

APK Not Available

The Android APK has not been compiled yet. To build it from source:

cd android
./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk ../ScreenTinker.apk

See the README for full build instructions.

In Docker, mount a built APK at /data/ScreenTinker.apk (the data dir).

Alternatively, use the web player in any browser.

`); } });