treefmt: apply prettier

Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
This commit is contained in:
atlas 2026-09-02 14:29:33 +02:00
commit 39b95c2ede
203 changed files with 10090 additions and 6085 deletions

View file

@ -3,35 +3,55 @@
// the OS-notification module, and the path-link / file-preview
// infrastructure for the side panel.
import { linkify as termLinkify } from '@hive/shared/terminal.js';
import { el } from '@hive/shared/dom.js';
import '@hive/shared/side-panel.js'; // registers <hive-side-panel> — side-effect import
import '@hive/shared/hive-warn.js'; // registers <hive-warn> — side-effect import
import DOMPurify from 'dompurify';
import { linkify as termLinkify } from "@hive/shared/terminal.js";
import { el } from "@hive/shared/dom.js";
import "@hive/shared/side-panel.js"; // registers <hive-side-panel> — side-effect import
import "@hive/shared/hive-warn.js"; // registers <hive-warn> — side-effect import
import DOMPurify from "dompurify";
// ─── helpers ────────────────────────────────────────────────────────────
export const $ = (id) => document.getElementById(id);
export const fmtAgeSecs = (s) => s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s/60)}m`
: s < 86400 ? `${Math.floor(s/3600)}h` : `${Math.floor(s/86400)}d`;
export const fmtAgeSecs = (s) =>
s < 60
? `${s}s`
: s < 3600
? `${Math.floor(s / 60)}m`
: s < 86400
? `${Math.floor(s / 3600)}h`
: `${Math.floor(s / 86400)}d`;
export const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;' }[c])
);
export const esc = (s) =>
String(s).replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts = {}) => {
const f = el('form', {
method: 'POST', action, class: 'inline', 'data-async': '',
...(confirmMsg ? { 'data-confirm': confirmMsg } : {}),
export const form = (
action,
btnClass,
btnLabel,
confirmMsg,
extra = {},
opts = {},
) => {
const f = el("form", {
method: "POST",
action,
class: "inline",
"data-async": "",
...(confirmMsg ? { "data-confirm": confirmMsg } : {}),
// Endpoints whose mutation fires a DashboardEvent (and whose
// derived store applies it live) opt out of the post-submit
// /api/state refetch. See the async-form handler.
...(opts.noRefresh ? { 'data-no-refresh': '' } : {}),
...(opts.noRefresh ? { "data-no-refresh": "" } : {}),
});
for (const [name, value] of Object.entries(extra)) {
f.append(el('input', { type: 'hidden', name, value }));
f.append(el("input", { type: "hidden", name, value }));
}
f.append(el('button', { type: 'submit', class: 'btn ' + btnClass }, btnLabel));
f.append(
el("button", { type: "submit", class: "btn " + btnClass }, btnLabel),
);
return f;
};
@ -55,8 +75,8 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts =
// Consumer API: assign `onmessage` / `onopen` / `onerror`; `.close()`
// drops the subscription (the worker closes the upstream when the last
// subscriber leaves).
const SHARED_WORKER_PATH = '/static/stream-worker.js';
const SHARED_WORKER_NAME = 'hyperhive-stream';
const SHARED_WORKER_PATH = "/static/stream-worker.js";
const SHARED_WORKER_NAME = "hyperhive-stream";
// One SharedWorker port per page, reused by all openStream calls on
// that page. Invalidated on `pagehide` so a bfcache restore picks up
@ -64,13 +84,16 @@ const SHARED_WORKER_NAME = 'hyperhive-stream';
// closed while this page was frozen.
let _sharedPort = null;
function makeSharedPort() {
if (typeof SharedWorker === 'undefined') return null;
if (typeof SharedWorker === "undefined") return null;
try {
const sw = new SharedWorker(SHARED_WORKER_PATH, SHARED_WORKER_NAME);
sw.port.start();
return sw.port;
} catch (err) {
console.warn('SharedWorker unavailable, falling back to direct EventSource:', err);
console.warn(
"SharedWorker unavailable, falling back to direct EventSource:",
err,
);
return null;
}
}
@ -87,19 +110,27 @@ function getSharedPort() {
const WORKER_DEAD_THRESHOLD_MS = 90_000;
const WORKER_WATCHDOG_INTERVAL_MS = 15_000;
let _lastWorkerActivityAt = 0;
function noteWorkerActivity() { _lastWorkerActivityAt = Date.now(); }
function noteWorkerActivity() {
_lastWorkerActivityAt = Date.now();
}
let _watchdogTimer = null;
function startWorkerWatchdog() {
if (_watchdogTimer != null) return;
_watchdogTimer = setInterval(() => {
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
if (
typeof document !== "undefined" &&
document.visibilityState !== "visible"
)
return;
if (!_activeSubs.size) return;
if (!_sharedPort) return;
const sinceLast = Date.now() - _lastWorkerActivityAt;
if (sinceLast < WORKER_DEAD_THRESHOLD_MS) return;
console.warn(
'hyperhive-stream worker silent for ' + Math.round(sinceLast / 1000)
+ 's, presumed dead — re-subscribing on a fresh port');
"hyperhive-stream worker silent for " +
Math.round(sinceLast / 1000) +
"s, presumed dead — re-subscribing on a fresh port",
);
rebindOnFreshPort();
}, WORKER_WATCHDOG_INTERVAL_MS);
}
@ -109,7 +140,9 @@ function rebindOnFreshPort() {
// is the source of truth for what we need to re-attach.
if (_sharedPort) {
for (const sub of _activeSubs.values()) {
try { _sharedPort.removeEventListener('message', sub.route); } catch {}
try {
_sharedPort.removeEventListener("message", sub.route);
} catch {}
}
}
_sharedPort = null;
@ -117,8 +150,10 @@ function rebindOnFreshPort() {
if (!port) return; // SharedWorker unsupported / unavailable — nothing to do
for (const [url, sub] of _activeSubs) {
sub.target.readyState = 0; // CONNECTING — worker will (re-)fire 'open'
port.addEventListener('message', sub.route);
try { port.postMessage({ kind: 'subscribe', url }); } catch {}
port.addEventListener("message", sub.route);
try {
port.postMessage({ kind: "subscribe", url });
} catch {}
}
// Reset the activity clock so the watchdog gives the fresh worker
// a full window to settle before re-triggering.
@ -143,32 +178,41 @@ function bindLifecycleOnce() {
if (_lifecycleBound) return;
_lifecycleBound = true;
startWorkerWatchdog();
window.addEventListener('pagehide', () => {
window.addEventListener("pagehide", () => {
if (!_sharedPort) return;
for (const url of _activeSubs.keys()) {
try { _sharedPort.postMessage({ kind: 'unsubscribe', url }); }
catch { /* port dead — worker side already cleaned up */ }
try {
_sharedPort.postMessage({ kind: "unsubscribe", url });
} catch {
/* port dead — worker side already cleaned up */
}
}
// Drop port routes too; the bfcache-restore path will re-add
// them on a fresh port. Leaving stale routes on a dead port
// would just keep a closure alive without cost, but cleaning
// up keeps the registry shape honest.
for (const sub of _activeSubs.values()) {
try { _sharedPort.removeEventListener('message', sub.route); }
catch { /* same */ }
try {
_sharedPort.removeEventListener("message", sub.route);
} catch {
/* same */
}
}
_sharedPort = null;
});
window.addEventListener('pageshow', (ev) => {
window.addEventListener("pageshow", (ev) => {
if (!ev.persisted) return; // cold load — openStream just bound listeners
if (!_activeSubs.size) return;
const port = getSharedPort();
if (!port) return; // SharedWorker really gone; fallback already in place
for (const [url, sub] of _activeSubs) {
sub.target.readyState = 0; // CONNECTING — the worker will fire 'open'
port.addEventListener('message', sub.route);
try { port.postMessage({ kind: 'subscribe', url }); }
catch { /* port dead immediately — skip */ }
port.addEventListener("message", sub.route);
try {
port.postMessage({ kind: "subscribe", url });
} catch {
/* port dead immediately — skip */
}
}
});
}
@ -191,10 +235,16 @@ export function openStream(url) {
close() {
const p = _sharedPort;
if (p) {
try { p.postMessage({ kind: 'unsubscribe', url }); }
catch { /* port dead */ }
try { p.removeEventListener('message', route); }
catch { /* same */ }
try {
p.postMessage({ kind: "unsubscribe", url });
} catch {
/* port dead */
}
try {
p.removeEventListener("message", route);
} catch {
/* same */
}
}
_activeSubs.delete(url);
},
@ -204,29 +254,38 @@ export function openStream(url) {
// before the URL filter, since heartbeat pings carry no URL.
noteWorkerActivity();
const m = e.data;
if (!m || m.kind === 'ping') return;
if (!m || m.kind === "ping") return;
if (m.url !== url) return;
if (m.kind === 'open') {
if (m.kind === "open") {
target.readyState = 1; // OPEN
if (target.onopen) {
try { target.onopen({ target }); }
catch (err) { console.error('openStream onopen threw', err); }
try {
target.onopen({ target });
} catch (err) {
console.error("openStream onopen threw", err);
}
}
} else if (m.kind === 'message') {
} else if (m.kind === "message") {
if (target.onmessage) {
try { target.onmessage({ data: m.data, target }); }
catch (err) { console.error('openStream onmessage threw', err); }
try {
target.onmessage({ data: m.data, target });
} catch (err) {
console.error("openStream onmessage threw", err);
}
}
} else if (m.kind === 'error') {
} else if (m.kind === "error") {
if (target.onerror) {
try { target.onerror({ target }); }
catch (err) { console.error('openStream onerror threw', err); }
try {
target.onerror({ target });
} catch (err) {
console.error("openStream onerror threw", err);
}
}
}
};
_activeSubs.set(url, { target, route });
port.addEventListener('message', route);
port.postMessage({ kind: 'subscribe', url });
port.addEventListener("message", route);
port.postMessage({ kind: "subscribe", url });
// Seed the activity clock so the watchdog has a baseline (would
// otherwise compare against 0 and trigger immediately).
noteWorkerActivity();
@ -244,33 +303,41 @@ export function openStream(url) {
// (re)connect, so a CONNECTING reconnect resets the <pre> to avoid doubling.
export function openBuildLogStream(id, pre, { onDone, onError } = {}) {
let atBottom = true;
pre.addEventListener('scroll', () => {
pre.addEventListener("scroll", () => {
atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 40;
});
let stderrSeen = false;
const es = new EventSource('/api/build-logs/id/' + id + '/stream');
const es = new EventSource("/api/build-logs/id/" + id + "/stream");
es.onmessage = (e) => {
let frame;
try { frame = JSON.parse(e.data); } catch { return; }
try {
frame = JSON.parse(e.data);
} catch {
return;
}
if (frame.stdout_append) {
pre.textContent += frame.stdout_append;
if (atBottom) pre.scrollTop = pre.scrollHeight;
}
if (frame.stderr_append) {
if (!stderrSeen) { pre.textContent += '\n--- stderr ---\n'; stderrSeen = true; }
if (!stderrSeen) {
pre.textContent += "\n--- stderr ---\n";
stderrSeen = true;
}
pre.textContent += frame.stderr_append;
if (atBottom) pre.scrollTop = pre.scrollHeight;
}
if (frame.done) {
es.close();
if (onDone) onDone(frame.status || 'done');
if (onDone) onDone(frame.status || "done");
}
};
es.onerror = () => {
// CONNECTING = the browser is auto-reconnecting; the stream replays from
// the start, so clear the <pre> to avoid duplicated output and wait.
if (es.readyState === EventSource.CONNECTING) {
pre.textContent = ''; stderrSeen = false;
pre.textContent = "";
stderrSeen = false;
return;
}
es.close();
@ -295,7 +362,7 @@ export function openBuildLogStream(id, pre, { onDone, onError } = {}) {
// slotted content via a plain `hive-side-panel .md …` tag-name selector
// (no compatibility class needed — the element's own tag name already
// uniquely identifies it in the light DOM).
export const sidePanel = document.createElement('hive-side-panel');
export const sidePanel = document.createElement("hive-side-panel");
document.body.append(sidePanel);
// ─── path linkification ─────────────────────────────────────────────────
@ -309,45 +376,54 @@ document.body.append(sidePanel);
// to translate it). Prefer `/agents/<name>/state/...` in agent
// outputs and the link will resolve.
async function fetchStateFile(path) {
const resp = await fetch('/api/state-file?path=' + encodeURIComponent(path));
const resp = await fetch("/api/state-file?path=" + encodeURIComponent(path));
const text = await resp.text();
if (!resp.ok) throw new Error(text || ('http ' + resp.status));
if (!resp.ok) throw new Error(text || "http " + resp.status);
return text;
}
// A 2-tab file preview: a "rendered" tab (default) + a raw-text tab.
// `renderRendered()` produces the rendered-tab node fresh on each
// switch; `plainText` backs the raw tab; `plainLabel` names it.
function buildTabbedPreview(renderRendered, plainText, plainLabel) {
const tabs = el('div', { class: 'diff-base-tabs' });
const host = el('div', { class: 'preview-host' });
const tabs = el("div", { class: "diff-base-tabs" });
const host = el("div", { class: "preview-host" });
function show(mode) {
for (const b of tabs.children) {
b.classList.toggle('active', b.dataset.mode === mode);
b.classList.toggle("active", b.dataset.mode === mode);
}
host.replaceChildren(mode === 'plain'
? el('pre', { class: 'path-preview-body' }, plainText)
: renderRendered());
host.replaceChildren(
mode === "plain"
? el("pre", { class: "path-preview-body" }, plainText)
: renderRendered(),
);
}
for (const [mode, label] of [['rendered', 'rendered'], ['plain', plainLabel]]) {
const b = el('button',
{ type: 'button', class: 'diff-base-tab', 'data-mode': mode }, label);
b.addEventListener('click', () => show(mode));
for (const [mode, label] of [
["rendered", "rendered"],
["plain", plainLabel],
]) {
const b = el(
"button",
{ type: "button", class: "diff-base-tab", "data-mode": mode },
label,
);
b.addEventListener("click", () => show(mode));
tabs.append(b);
}
show('rendered');
return el('div', {}, tabs, host);
show("rendered");
return el("div", {}, tabs, host);
}
// Rendered <img> for an SVG, loaded via an <img> data: URI —
// <img>-loaded SVG runs in the browser's secure static mode (no
// scripts, no external fetches), so an untrusted SVG from an
// agent's state dir can't execute code in the dashboard.
function svgImage(text) {
const img = el('img', { class: 'img-preview', alt: 'SVG preview' });
img.addEventListener('error', () => {
img.replaceWith(el('div', { class: 'meta' },
'(could not render — see the source tab)'));
const img = el("img", { class: "img-preview", alt: "SVG preview" });
img.addEventListener("error", () => {
img.replaceWith(
el("div", { class: "meta" }, "(could not render — see the source tab)"),
);
});
img.src = 'data:image/svg+xml,' + encodeURIComponent(text);
img.src = "data:image/svg+xml," + encodeURIComponent(text);
return img;
}
// Marked-rendered markdown node (raw text fallback if `marked`
@ -357,15 +433,15 @@ function svgImage(text) {
// tags that `marked` itself no longer strips (v5+ dropped the built-in
// sanitizer).
function mdNode(text) {
const div = el('div', { class: 'md' });
if (window.marked && typeof window.marked.parse === 'function') {
const div = el("div", { class: "md" });
if (window.marked && typeof window.marked.parse === "function") {
window.marked.setOptions({ breaks: true, gfm: true });
div.innerHTML = DOMPurify.sanitize(window.marked.parse(text));
// marked autolinks URLs but leaves them same-tab — open externally
// so a click never navigates away from the dashboard.
div.querySelectorAll('a[href]').forEach((a) => {
a.target = '_blank';
a.rel = 'noopener noreferrer';
div.querySelectorAll("a[href]").forEach((a) => {
a.target = "_blank";
a.rel = "noopener noreferrer";
});
} else {
div.textContent = text;
@ -381,38 +457,53 @@ const RASTER_RE = /\.(png|jpe?g|gif|webp|bmp|ico|avif)$/i;
// render as an <img>; every other file stays raw text in a <pre>.
async function openFilePanel(path) {
if (RASTER_RE.test(path)) {
const img = el('img', { class: 'img-preview', alt: path });
img.addEventListener('error', () => {
img.replaceWith(el('pre', { class: 'path-preview-body' },
'(could not load image — it may be missing or over the preview size cap)'));
const img = el("img", { class: "img-preview", alt: path });
img.addEventListener("error", () => {
img.replaceWith(
el(
"pre",
{ class: "path-preview-body" },
"(could not load image — it may be missing or over the preview size cap)",
),
);
});
img.src = '/api/state-file?path=' + encodeURIComponent(path);
sidePanel.open('↳ ' + path, img);
img.src = "/api/state-file?path=" + encodeURIComponent(path);
sidePanel.open("↳ " + path, img);
return;
}
const isMd = /\.(md|markdown)$/i.test(path);
const isSvg = /\.svg$/i.test(path);
const view = el('div');
view.textContent = '(fetching…)';
sidePanel.open('↳ ' + path, view);
const view = el("div");
view.textContent = "(fetching…)";
sidePanel.open("↳ " + path, view);
try {
const text = await fetchStateFile(path);
if (isSvg) {
view.replaceChildren(buildTabbedPreview(() => svgImage(text), text, 'source'));
view.replaceChildren(
buildTabbedPreview(() => svgImage(text), text, "source"),
);
} else if (isMd) {
view.replaceChildren(buildTabbedPreview(() => mdNode(text), text, 'plain'));
view.replaceChildren(
buildTabbedPreview(() => mdNode(text), text, "plain"),
);
} else {
view.replaceChildren(el('pre', { class: 'path-preview-body' }, text));
view.replaceChildren(el("pre", { class: "path-preview-body" }, text));
}
} catch (e) {
view.textContent = 'error: ' + (e.message || e);
view.textContent = "error: " + (e.message || e);
}
}
export function makePathLink(path) {
const anchor = el('a', {
href: '#', class: 'path-link', title: 'open ' + path + ' in panel',
}, path);
anchor.addEventListener('click', (e) => {
const anchor = el(
"a",
{
href: "#",
class: "path-link",
title: "open " + path + " in panel",
},
path,
);
anchor.addEventListener("click", (e) => {
e.preventDefault();
openFilePanel(path);
});
@ -453,7 +544,11 @@ export function appendLinkified(parent, text, refs) {
for (const t of tokens) {
const idx = str.indexOf(t, i);
if (idx === -1) continue;
if (bestStart === -1 || idx < bestStart || (idx === bestStart && t.length > bestToken.length)) {
if (
bestStart === -1 ||
idx < bestStart ||
(idx === bestStart && t.length > bestToken.length)
) {
bestStart = idx;
bestToken = t;
}
@ -479,55 +574,63 @@ export function appendLinkified(parent, text, refs) {
// localhost) — on other origins the API is unavailable and we hide
// the controls.
export const NOTIF = (() => {
const supported = typeof Notification !== 'undefined';
const MUTED_KEY = 'hyperhive.notify.muted';
const isMuted = () => localStorage.getItem(MUTED_KEY) === '1';
const setMuted = (v) => v
? localStorage.setItem(MUTED_KEY, '1')
: localStorage.removeItem(MUTED_KEY);
const supported = typeof Notification !== "undefined";
const MUTED_KEY = "hyperhive.notify.muted";
const isMuted = () => localStorage.getItem(MUTED_KEY) === "1";
const setMuted = (v) =>
v
? localStorage.setItem(MUTED_KEY, "1")
: localStorage.removeItem(MUTED_KEY);
function renderControls() {
const enable = $('notif-enable');
const mute = $('notif-mute');
const unmute = $('notif-unmute');
const status = $('notif-status');
const enable = $("notif-enable");
const mute = $("notif-mute");
const unmute = $("notif-unmute");
const status = $("notif-status");
if (!enable || !mute || !unmute || !status) return;
if (!supported) {
enable.hidden = mute.hidden = unmute.hidden = true;
status.hidden = false;
status.textContent = 'notifications unsupported in this browser';
status.textContent = "notifications unsupported in this browser";
return;
}
const perm = Notification.permission;
enable.hidden = perm === 'granted';
mute.hidden = perm !== 'granted' || isMuted();
unmute.hidden = perm !== 'granted' || !isMuted();
status.hidden = perm !== 'denied';
if (perm === 'denied') status.textContent = 'notifications blocked — grant in site settings';
enable.hidden = perm === "granted";
mute.hidden = perm !== "granted" || isMuted();
unmute.hidden = perm !== "granted" || !isMuted();
status.hidden = perm !== "denied";
if (perm === "denied")
status.textContent = "notifications blocked — grant in site settings";
}
function bind() {
const enable = $('notif-enable');
const mute = $('notif-mute');
const unmute = $('notif-unmute');
const enable = $("notif-enable");
const mute = $("notif-mute");
const unmute = $("notif-unmute");
if (!supported || !enable || !mute || !unmute) return;
enable.addEventListener('click', async () => {
enable.addEventListener("click", async () => {
await Notification.requestPermission();
renderControls();
});
mute.addEventListener('click', () => { setMuted(true); renderControls(); });
unmute.addEventListener('click', () => { setMuted(false); renderControls(); });
mute.addEventListener("click", () => {
setMuted(true);
renderControls();
});
unmute.addEventListener("click", () => {
setMuted(false);
renderControls();
});
renderControls();
}
function show(title, body, tag) {
if (!supported) {
console.debug('notify: Notification API not supported');
console.debug("notify: Notification API not supported");
return;
}
if (Notification.permission !== 'granted') {
console.debug('notify: permission not granted', Notification.permission);
if (Notification.permission !== "granted") {
console.debug("notify: permission not granted", Notification.permission);
return;
}
if (isMuted()) {
console.debug('notify: muted');
console.debug("notify: muted");
return;
}
try {
@ -537,12 +640,15 @@ export const NOTIF = (() => {
// because that one tag would replace itself on every fire.
const n = new Notification(title, {
body,
tag: tag || ('hyperhive:' + Date.now()),
tag: tag || "hyperhive:" + Date.now(),
});
n.onclick = () => { window.focus(); n.close(); };
console.debug('notify: shown', title, 'tag=', tag);
n.onclick = () => {
window.focus();
n.close();
};
console.debug("notify: shown", title, "tag=", tag);
} catch (err) {
console.warn('notification show failed', err);
console.warn("notification show failed", err);
}
}
return { bind, show, renderControls };
@ -564,11 +670,11 @@ export const NOTIF = (() => {
// page's existing chrome element; pages without a chrome (e.g. the H0M3
// hub) get a banner-only sticky region at the top of <body>.
function ensureStickyTop() {
let top = document.querySelector('.sticky-top');
let top = document.querySelector(".sticky-top");
if (top) return top;
top = document.createElement('div');
top.className = 'sticky-top';
const chrome = document.querySelector('.dashboard-chrome, .page-header');
top = document.createElement("div");
top.className = "sticky-top";
const chrome = document.querySelector(".dashboard-chrome, .page-header");
if (chrome && chrome.parentNode) {
chrome.parentNode.insertBefore(top, chrome);
top.append(chrome);
@ -579,12 +685,12 @@ function ensureStickyTop() {
}
function ensureServerWarningsBar() {
let bar = document.getElementById('server-warnings');
let bar = document.getElementById("server-warnings");
if (!bar) {
bar = document.createElement('div');
bar.id = 'server-warnings';
bar.className = 'server-warnings';
bar.setAttribute('role', 'alert');
bar = document.createElement("div");
bar.id = "server-warnings";
bar.className = "server-warnings";
bar.setAttribute("role", "alert");
bar.hidden = true;
ensureStickyTop().prepend(bar);
}
@ -601,10 +707,12 @@ export function renderServerWarnings(warnings) {
return;
}
for (const w of warnings) {
const row = el('div', {
class: 'server-warn server-warn-' + (w && w.level === 'crit' ? 'crit' : 'warn'),
const row = el("div", {
class:
"server-warn server-warn-" +
(w && w.level === "crit" ? "crit" : "warn"),
});
appendText(row, '⚠ ' + ((w && w.message) || ''));
appendText(row, "⚠ " + ((w && w.message) || ""));
bar.append(row);
}
bar.hidden = false;
@ -616,8 +724,10 @@ export function renderServerWarnings(warnings) {
/// live updates instead.
export function initServerWarnings() {
ensureServerWarningsBar();
fetch('/api/state')
fetch("/api/state")
.then((r) => (r.ok ? r.json() : null))
.then((s) => renderServerWarnings(s && s.server_warnings))
.catch(() => { /* non-fatal: no banner if the snapshot is unreachable */ });
.catch(() => {
/* non-fatal: no banner if the snapshot is unreachable */
});
}