From 0e13bd4158ea527c776bebd87149f0978d0b209b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 15:23:51 +0000 Subject: [PATCH 1/2] Look for a new release while the update is on screen The Pi looked a minute after it started and then every six hours, so a release published in between stayed hidden for hours: the app only shows the update button once the Pi has found one. GET /api/state, which the app asks for while More is open and the settings page asks for too, now starts a look in the background when the last one is older than ten minutes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gby235Tvdg4WjSB3MXB5YG --- README.md | 5 +++-- pi/src/update.ts | 16 ++++++++++++++++ pi/src/web.ts | 4 ++++ pi/test/update.test.ts | 20 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36228a7..ac2f8cb 100644 --- a/README.md +++ b/README.md @@ -264,8 +264,9 @@ sudo systemctl restart led-matrix # after changing config.ini **Update:** when a newer release is out, the settings page says so and has an *Update now* button. The Pi looks for one a minute after it starts and -then every six hours; it only asks GitHub, nothing is opened towards the -internet. The button takes about a minute, the panel goes dark briefly, and +then every six hours, and again while the settings page or the app's *More* +is open (at most every ten minutes); it only asks GitHub, nothing is opened +towards the internet. The button takes about a minute, the panel goes dark briefly, and the page reloads in the new version. The app connected to the display offers the same under *More*, with a dot on the tab while an update is waiting. The button looks for the newest release once more before it starts, so it never diff --git a/pi/src/update.ts b/pi/src/update.ts index 3dd2ac3..e0f1ac4 100644 --- a/pi/src/update.ts +++ b/pi/src/update.ts @@ -68,6 +68,9 @@ export class Updates { latest: string | null = null; private running = false; private failed = false; + /** When the last look started (ms), and the look that is under way. */ + private looked = 0; + private looking: Promise | null = null; /** * `directory`: where VERSION and REPO are (the app's folder). `beta`: whether betas are wanted @@ -97,6 +100,7 @@ export class Updates { if (!this.enabled) { return; } + this.looked = Date.now(); await this.checkUnit(); try { const tag = this.beta() ? await this.newestBeta() : await this.newestRelease(); @@ -109,6 +113,18 @@ export class Updates { } } + /** + * Looks again in the background when the last look is older than `maxAge` (ms), so somebody + * who opens the app or the settings page sees a release from a minute ago on the next answer, + * not only after the next regular look hours later. Never more than one look at a time. + */ + lookIfOlder(maxAge: number): void { + if (!this.enabled || this.looking || Date.now() - this.looked < maxAge) { + return; + } + this.looking = this.check().finally(() => { this.looking = null; }); + } + /** The newest release: .../releases/latest redirects to .../releases/tag/ (it skips pre-releases). */ private async newestRelease(): Promise { const response = await this.request(`https://github.com/${this.repo}/releases/latest`, diff --git a/pi/src/web.ts b/pi/src/web.ts index 543e678..ac2c065 100644 --- a/pi/src/web.ts +++ b/pi/src/web.ts @@ -23,6 +23,7 @@ const log = getLog("web"); const MAX_BODY = 64 * 1024; // small JSON requests only const MAX_SAVE = 2 * 1024 * 1024; // a save file loaded on the settings page const PREVIEW_WAIT = 1; // seconds the preview waits for the next frame at most +const LOOK_AGAIN = 10 * 60_000; // ms after a look for a new release before GET /api/state looks again // background of the pet app, as "#111318" // ---------- Pages ---------- @@ -176,6 +177,9 @@ export function createApp(options: WebOptions): (request: IncomingMessage, respo return html(page(request, "index.html", { languages: settings.languages() })); case "GET /api/state": + // the app and the settings page ask for this while they show the update: a release + // published since the last look shows up there within minutes (GitHub allows 60 looks an hour) + updates.lookIfOlder(LOOK_AGAIN); return json(stateJson()); case "POST /api/mode": diff --git a/pi/test/update.test.ts b/pi/test/update.test.ts index a003caf..0f79532 100644 --- a/pi/test/update.test.ts +++ b/pi/test/update.test.ts @@ -177,3 +177,23 @@ test("an app asks for an update up to its own version: the newest release up to assert.equal(await plain.start("not a version!"), true); assert.equal(readFileSync(env, "utf-8"), "LED_MATRIX_VERSION=v1.1.0\n"); }); + +test("a look that is old is done again in the background, one at a time", async () => { + let asked = 0; + const request = (async () => { + asked++; + return { ok: true, status: 200, url: "https://github.com/someone/blobling/releases/tag/v1.1.0" }; + }) as unknown as typeof fetch; + const updates = new Updates(release("v1.0.0"), request, systemctl("inactive")); + updates.lookIfOlder(60_000); + updates.lookIfOlder(60_000); // the first one is still under way + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.equal(asked, 1); + assert.equal(updates.state().latest, "v1.1.0"); + updates.lookIfOlder(60_000); // just looked + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.equal(asked, 1); + updates.lookIfOlder(0); + await new Promise((resolve) => setTimeout(resolve, 10)); + assert.equal(asked, 2); +}); From 2fae395dc0fd36ccb0ad192614229ac4d9144f63 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 15:26:58 +0000 Subject: [PATCH 2/2] Offer the display update whenever the display is older than the app The app knows its own version, so a display below it has a fitting release waiting even before the Pi's next look has found it. The button then shows right away and makes the Pi look. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gby235Tvdg4WjSB3MXB5YG --- app/pet.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/pet.html b/app/pet.html index 5e82033..b0fe6ad 100644 --- a/app/pet.html +++ b/app/pet.html @@ -3217,16 +3217,20 @@

// (the Pi then installs the newest release up to the app's version, see startUpdate) const ahead = u.available && app.aheadOfApp(u.latest); const upToApp = ahead && app.belowApp(u.current); + // a display older than this app has a release waiting (the app's own), even before the Pi's + // next look has found it: the button makes it look right away + const behind = !u.available && !u.running && app.belowApp(u.current); const lines = [t("page.app.update_version", { version: u.current })]; if (u.running) lines.push(t("page.app.update_running")); - else if (upToApp) lines.push(t("page.app.update_available", { latest: app.version })); + else if (upToApp || behind) lines.push(t("page.app.update_available", { latest: app.version })); else if (ahead) lines.push(t("page.app.update_app_first", { latest: u.latest })); else if (u.available) lines.push(t("page.app.update_available", { latest: u.latest })); else if (u.failed) lines.push(t("page.app.update_failed")); $("update-text").textContent = lines.join(" "); $("update-text").hidden = false; - $("update-start").hidden = (ahead && !upToApp) || (!u.available && !(app.fit() === "display_old" && !u.running)); - document.querySelector('[data-view="more"]').classList.toggle("news", u.available && (!ahead || upToApp)); + $("update-start").hidden = (ahead && !upToApp) + || (!u.available && !behind && !(app.fit() === "display_old" && !u.running)); + document.querySelector('[data-view="more"]').classList.toggle("news", (u.available && (!ahead || upToApp)) || behind); } // The Pi looks for the newest release once more before it starts, so this never waits for its next