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/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
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);
+});