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