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>
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
// The sidebar footer's daemon-status dot, sat next to the spawn button.
|
|
//
|
|
// Colour is the link state; hovering (or focusing — it is a button, so the
|
|
// keyboard reaches it) reveals the release the daemon is running: commit hash
|
|
// and commit date.
|
|
|
|
import { formatRelease, type DaemonLink, type LinkState, type LinkStatus } from "./daemonLink.js";
|
|
|
|
/** One human-readable phrase per link state, used for both the tooltip's first
|
|
* line and the button's accessible name. */
|
|
const LABELS: Record<LinkState, string> = {
|
|
connecting: "Connecting…",
|
|
online: "Connected",
|
|
reconnecting: "Reconnecting…",
|
|
down: "Daemon unreachable",
|
|
updating: "Updated — reloading",
|
|
};
|
|
|
|
export interface StatusIndicatorHandle {
|
|
el: HTMLElement;
|
|
dispose(): void;
|
|
}
|
|
|
|
export function createStatusIndicator(link: DaemonLink): StatusIndicatorHandle {
|
|
const el = document.createElement("button");
|
|
el.type = "button";
|
|
el.className = "btn btn-ghost btn-icon status-chip";
|
|
el.setAttribute("aria-describedby", "status-tip");
|
|
|
|
const dot = document.createElement("span");
|
|
dot.className = "status-dot";
|
|
dot.setAttribute("aria-hidden", "true");
|
|
el.appendChild(dot);
|
|
|
|
const tip = document.createElement("span");
|
|
tip.className = "status-tip";
|
|
tip.id = "status-tip";
|
|
tip.setAttribute("role", "tooltip");
|
|
el.appendChild(tip);
|
|
|
|
const unsubscribe = link.subscribe((status) => render(status));
|
|
|
|
function render(status: LinkStatus): void {
|
|
const label = LABELS[status.state];
|
|
const release = formatRelease(status.build);
|
|
el.dataset.state = status.state;
|
|
el.setAttribute("aria-label", `${label} — ${release}`);
|
|
tip.textContent = "";
|
|
tip.append(line("status-tip-state", status.hostname ? `${label} · ${status.hostname}` : label));
|
|
tip.append(line("status-tip-release", release));
|
|
}
|
|
|
|
function line(cls: string, text: string): HTMLElement {
|
|
const span = document.createElement("span");
|
|
span.className = cls;
|
|
span.textContent = text;
|
|
return span;
|
|
}
|
|
|
|
return {
|
|
el,
|
|
dispose(): void {
|
|
unsubscribe();
|
|
el.remove();
|
|
},
|
|
};
|
|
}
|