The sidebar footer gains a status dot beside the spawn button: green while the daemon answers, amber while reconnecting, red while it is gone. Hovering it shows the release the daemon is running — commit hash and commit date. When the events socket drops, `DaemonLink` starts polling `GET /api/info`. Past a 1200 ms grace window an overlay blocks the UI (and swallows the app's shortcuts) until the daemon answers again. If it comes back on a different commit the page reloads, so a tab never drives new server code from the bundle that was just replaced; a plain restart just unlocks. docs/RESTART.md is the contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
// Blocking overlay for a daemon that went away — see docs/RESTART.md.
|
|
//
|
|
// Mounted while the link is `down` or `updating`, torn down the moment it is
|
|
// back. It owns no polling of its own: DaemonLink polls, and this only renders
|
|
// what the link reports.
|
|
|
|
import { formatRelease, type DaemonLink, type LinkStatus } from "./daemonLink.js";
|
|
|
|
export interface RestartOverlayHandle {
|
|
dispose(): void;
|
|
}
|
|
|
|
/** Mount the overlay controller. It stays subscribed for the page's lifetime. */
|
|
export function installRestartOverlay(
|
|
link: DaemonLink,
|
|
host: HTMLElement = document.body,
|
|
): RestartOverlayHandle {
|
|
let el: HTMLElement | null = null;
|
|
let title: HTMLElement | null = null;
|
|
let detail: HTMLElement | null = null;
|
|
let release: HTMLElement | null = null;
|
|
|
|
const unsubscribe = link.subscribe((status) => render(status));
|
|
|
|
function render(status: LinkStatus): void {
|
|
const blocking = status.state === "down" || status.state === "updating";
|
|
if (!blocking) {
|
|
unmount();
|
|
return;
|
|
}
|
|
if (!el) mount();
|
|
const updating = status.state === "updating";
|
|
title!.textContent = updating ? "Maestro was updated" : "Maestro is restarting";
|
|
detail!.textContent = updating
|
|
? "Reloading this page onto the new release…"
|
|
: attemptText(status.attempts);
|
|
release!.textContent = formatRelease(status.build);
|
|
}
|
|
|
|
/** The poll runs while this is up; saying how many tries have gone by is what
|
|
* separates "still restarting" from "wedged" for someone staring at it. */
|
|
function attemptText(attempts: number): string {
|
|
if (attempts <= 1) return "Waiting for the daemon to come back…";
|
|
return `Waiting for the daemon to come back… (${attempts} attempts)`;
|
|
}
|
|
|
|
function mount(): void {
|
|
el = document.createElement("div");
|
|
el.className = "modal-backdrop restart-backdrop";
|
|
const dialog = document.createElement("div");
|
|
dialog.className = "modal restart-modal";
|
|
dialog.setAttribute("role", "alertdialog");
|
|
dialog.setAttribute("aria-modal", "true");
|
|
dialog.setAttribute("aria-labelledby", "restart-title");
|
|
|
|
title = document.createElement("h2");
|
|
title.id = "restart-title";
|
|
detail = document.createElement("p");
|
|
detail.className = "restart-detail";
|
|
release = document.createElement("p");
|
|
release.className = "restart-release";
|
|
const spinner = document.createElement("div");
|
|
spinner.className = "restart-spinner";
|
|
spinner.setAttribute("aria-hidden", "true");
|
|
|
|
dialog.append(spinner, title, detail, release);
|
|
el.appendChild(dialog);
|
|
host.appendChild(el);
|
|
// The app's shortcuts live on `window`, so hiding the UI behind a backdrop
|
|
// does not stop them: `n` would still open a spawn form against a daemon
|
|
// that is not there. Swallow keys in the capture phase until the link is
|
|
// back, without `preventDefault` so the browser's own chords still work.
|
|
window.addEventListener("keydown", swallow, true);
|
|
}
|
|
|
|
function swallow(e: KeyboardEvent): void {
|
|
e.stopImmediatePropagation();
|
|
}
|
|
|
|
function unmount(): void {
|
|
if (!el) return;
|
|
window.removeEventListener("keydown", swallow, true);
|
|
el.remove();
|
|
el = null;
|
|
}
|
|
|
|
return {
|
|
dispose(): void {
|
|
unsubscribe();
|
|
unmount();
|
|
},
|
|
};
|
|
}
|