Skip to content

Commit a4136d8

Browse files
dpsideriusclaude
andcommitted
chore: release v1.0.4 — update banner clears after a successful self-update
The "Updating… Installing vX" banner stayed up forever after an update that had already completed (the header showed the new version, the banner did not). Cause: dpkg restarts the gateway *mid-install*, so the daemon reads .update-status.json at boot while it still says "installing" — the value the updater wrote before apt-get returned. The update oneshot (own cgroup) writes the terminal "success"/"error" a moment later, but the WebSocket is gone and the daemon never re-reads the file, so "success" is never broadcast and the UI's `updating` flag never clears. Fix: when the daemon boots into an in-flight (downloading/installing) state, poll the status file once a second (up to 2 min) until it settles, then broadcast the terminal status. The timer is unref()'d so it never holds the process open, and it's a no-op on every normal boot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1404e49 commit a4136d8

3 files changed

Lines changed: 26 additions & 3 deletions

File tree

gateway/server.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@ function writeUpdateStatus(s: UpdateStatus) {
182182
}
183183
lastUpdateStatus = readUpdateStatus(); // pick up the outcome of an update that just restarted us
184184

185+
// dpkg restarts us *mid-install*, so the status we just read is the non-terminal
186+
// "installing" the updater wrote before apt-get returned. The update oneshot runs
187+
// in its own cgroup and writes the terminal success/error a moment later, but the
188+
// WebSocket is gone so it can't push it to us — and we'd otherwise never re-read
189+
// the file, leaving the "Updating…" banner stuck forever. Poll until it settles,
190+
// then broadcast. No-op unless we actually booted into an in-flight update.
191+
function pollUpdateCompletion(): void {
192+
const inflight = lastUpdateStatus?.state;
193+
if (inflight !== 'downloading' && inflight !== 'installing') return;
194+
const deadline = Date.now() + 120_000;
195+
const tick = () => {
196+
const s = readUpdateStatus();
197+
if (s && JSON.stringify(s) !== JSON.stringify(lastUpdateStatus)) {
198+
lastUpdateStatus = s;
199+
broadcast({ type: 'event', event: 'updateStatus', payload: s });
200+
}
201+
const settled = s?.state === 'success' || s?.state === 'error';
202+
if (!settled && Date.now() < deadline) setTimeout(tick, 1000).unref();
203+
};
204+
setTimeout(tick, 1000).unref();
205+
}
206+
pollUpdateCompletion();
207+
185208
/** True if a plot is running or paused mid-plot — used to refuse a self-update. */
186209
function isPlotting(): boolean {
187210
const sd = ctrl.streamDebug;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "penplotter271",
33
"private": true,
4-
"version": "1.0.3",
4+
"version": "1.0.4",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

0 commit comments

Comments
 (0)