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:
parent
5d24bedd60
commit
39b95c2ede
203 changed files with 10090 additions and 6085 deletions
|
|
@ -9,10 +9,10 @@
|
|||
// (the same broker event channel the dashboard uses), maintaining its
|
||||
// own copy of the tombstones state.
|
||||
|
||||
import { $, form, openStream, initServerWarnings } from './common.js';
|
||||
import { el } from '@hive/shared/dom.js';
|
||||
import { asyncBtn, bindAsyncForms } from '@hive/shared/forms.js';
|
||||
import '@hive/shared/hive-tab-strip.js';
|
||||
import { $, form, openStream, initServerWarnings } from "./common.js";
|
||||
import { el } from "@hive/shared/dom.js";
|
||||
import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js";
|
||||
import "@hive/shared/hive-tab-strip.js";
|
||||
|
||||
// ─── derived state (own copies; this bundle has its own runtime) ──────────
|
||||
let tombstonesState = [];
|
||||
|
|
@ -26,60 +26,75 @@ function syncFromSnapshot(s) {
|
|||
|
||||
// ─── kept state (tombstones) ──────────────────────────────────────────────
|
||||
function renderTombstones(s) {
|
||||
const root = $('tombstones-section');
|
||||
const root = $("tombstones-section");
|
||||
if (!root) return;
|
||||
root.replaceChildren();
|
||||
if (!s.tombstones || !s.tombstones.length) {
|
||||
root.append(el('p', { class: 'empty' }, 'no kept state — clean'));
|
||||
root.append(el("p", { class: "empty" }, "no kept state — clean"));
|
||||
return;
|
||||
}
|
||||
const fmtBytes = (n) => {
|
||||
if (n < 1024) return n + ' B';
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
if (n < 1024) return n + " B";
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
||||
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
|
||||
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
||||
};
|
||||
const fmtAgeDays = (ts) => {
|
||||
if (!ts) return '?';
|
||||
if (!ts) return "?";
|
||||
const d = Math.floor((Date.now() / 1000 - ts) / 86400);
|
||||
if (d <= 0) return 'today';
|
||||
if (d === 1) return '1 day ago';
|
||||
return d + ' days ago';
|
||||
if (d <= 0) return "today";
|
||||
if (d === 1) return "1 day ago";
|
||||
return d + " days ago";
|
||||
};
|
||||
// Only shown alongside actual rows — a caveat over an empty list is noise.
|
||||
// Wording is deliberately about what the list *is* rather than what it
|
||||
// isn't: nothing records a destroy, so "container absent" is the only thing
|
||||
// the backend can actually tell.
|
||||
const warn = el('hive-warn', { level: 'warning' });
|
||||
const warn = el("hive-warn", { level: "warning" });
|
||||
warn.append(
|
||||
el('strong', {}, 'shows every agent whose container is absent'),
|
||||
' — not only destroyed ones. An agent part-way through being spawned ' +
|
||||
'looks identical here, because nothing records a destroy. Check it is ' +
|
||||
'really gone before you PURG3.',
|
||||
el("strong", {}, "shows every agent whose container is absent"),
|
||||
" — not only destroyed ones. An agent part-way through being spawned " +
|
||||
"looks identical here, because nothing records a destroy. Check it is " +
|
||||
"really gone before you PURG3.",
|
||||
);
|
||||
root.append(warn);
|
||||
const ul = el('ul', { class: 'containers' });
|
||||
const ul = el("ul", { class: "containers" });
|
||||
for (const t of s.tombstones) {
|
||||
const li = el('li', { class: 'container-row tombstone' });
|
||||
const head = el('div', { class: 'head' });
|
||||
const li = el("li", { class: "container-row tombstone" });
|
||||
const head = el("div", { class: "head" });
|
||||
head.append(
|
||||
el('span', { class: 'name' }, t.name),
|
||||
el("span", { class: "name" }, t.name),
|
||||
// Was `destroyed`, which the backend cannot actually know — see the
|
||||
// caveat above. `offline` is what the absence of a container proves.
|
||||
el('span', { class: 'hive-pill-sm badge-muted' }, 'offline'),
|
||||
el("span", { class: "hive-pill-sm badge-muted" }, "offline"),
|
||||
);
|
||||
if (t.has_creds)
|
||||
head.append(
|
||||
el("span", { class: "hive-pill-sm badge-muted" }, "creds kept"),
|
||||
);
|
||||
head.append(
|
||||
el(
|
||||
"span",
|
||||
{ class: "meta" },
|
||||
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`,
|
||||
),
|
||||
);
|
||||
if (t.has_creds) head.append(el('span', { class: 'hive-pill-sm badge-muted' }, 'creds kept'));
|
||||
head.append(el('span', { class: 'meta' },
|
||||
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`));
|
||||
li.append(head);
|
||||
|
||||
const actions = el('div', { class: 'actions' });
|
||||
actions.append(form(
|
||||
'/api/purge-tombstone/' + t.name, 'btn-destroy', 'PURG3',
|
||||
'PURGE ' + t.name + '? config history, claude creds, '
|
||||
+ 'and notes are all WIPED. no undo.',
|
||||
{}, { noRefresh: true },
|
||||
));
|
||||
const actions = el("div", { class: "actions" });
|
||||
actions.append(
|
||||
form(
|
||||
"/api/purge-tombstone/" + t.name,
|
||||
"btn-destroy",
|
||||
"PURG3",
|
||||
"PURGE " +
|
||||
t.name +
|
||||
"? config history, claude creds, " +
|
||||
"and notes are all WIPED. no undo.",
|
||||
{},
|
||||
{ noRefresh: true },
|
||||
),
|
||||
);
|
||||
li.append(actions);
|
||||
ul.append(li);
|
||||
}
|
||||
|
|
@ -98,37 +113,53 @@ let stalePermsLoaded = false;
|
|||
function renderStalePerms(root, ghosts) {
|
||||
root.replaceChildren();
|
||||
if (!ghosts.length) return;
|
||||
root.append(el('p', { class: 'tombstones-stale-heading' }, 'stale permission entries'));
|
||||
root.append(el('p', { class: 'meta' },
|
||||
'agents with explicit capability or tool-group entries but no live container '
|
||||
+ 'or kept state (typically renamed or manually-deleted agents whose JSON entries persisted).'));
|
||||
const errP = el('p', { class: 'tombstones-stale-err', hidden: true });
|
||||
const ul = el('ul', { class: 'tombstones-stale-list' });
|
||||
root.append(
|
||||
el("p", { class: "tombstones-stale-heading" }, "stale permission entries"),
|
||||
);
|
||||
root.append(
|
||||
el(
|
||||
"p",
|
||||
{ class: "meta" },
|
||||
"agents with explicit capability or tool-group entries but no live container " +
|
||||
"or kept state (typically renamed or manually-deleted agents whose JSON entries persisted).",
|
||||
),
|
||||
);
|
||||
const errP = el("p", { class: "tombstones-stale-err", hidden: true });
|
||||
const ul = el("ul", { class: "tombstones-stale-list" });
|
||||
for (const name of ghosts) {
|
||||
const li = el('li', { class: 'tombstones-stale-row' });
|
||||
li.append(el('span', { class: 'tombstones-stale-name' }, name));
|
||||
li.append(el('span', { class: 'hive-pill-sm badge-muted' }, 'stale perms'));
|
||||
const btn = el('button', {
|
||||
type: 'button',
|
||||
class: 'btn btn-destroy',
|
||||
title: 'remove explicit capability and tool-group entries for ' + name,
|
||||
}, '✕ clear perms');
|
||||
btn.addEventListener('click', () => asyncBtn(btn, async () => {
|
||||
errP.hidden = true;
|
||||
try {
|
||||
const resp = await fetch('/api/permissions/' + encodeURIComponent(name), { method: 'DELETE' });
|
||||
if (!resp.ok) {
|
||||
const msg = await resp.text().catch(() => String(resp.status));
|
||||
errP.textContent = 'failed to clear perms for ' + name + ': ' + msg;
|
||||
const li = el("li", { class: "tombstones-stale-row" });
|
||||
li.append(el("span", { class: "tombstones-stale-name" }, name));
|
||||
li.append(el("span", { class: "hive-pill-sm badge-muted" }, "stale perms"));
|
||||
const btn = el(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
class: "btn btn-destroy",
|
||||
title: "remove explicit capability and tool-group entries for " + name,
|
||||
},
|
||||
"✕ clear perms",
|
||||
);
|
||||
btn.addEventListener("click", () =>
|
||||
asyncBtn(btn, async () => {
|
||||
errP.hidden = true;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
"/api/permissions/" + encodeURIComponent(name),
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const msg = await resp.text().catch(() => String(resp.status));
|
||||
errP.textContent = "failed to clear perms for " + name + ": " + msg;
|
||||
errP.hidden = false;
|
||||
return;
|
||||
}
|
||||
await fetchAndRenderStalePerms();
|
||||
} catch (err) {
|
||||
errP.textContent = "failed to clear perms for " + name + ": " + err;
|
||||
errP.hidden = false;
|
||||
return;
|
||||
}
|
||||
await fetchAndRenderStalePerms();
|
||||
} catch (err) {
|
||||
errP.textContent = 'failed to clear perms for ' + name + ': ' + err;
|
||||
errP.hidden = false;
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
li.append(btn);
|
||||
ul.append(li);
|
||||
}
|
||||
|
|
@ -140,16 +171,18 @@ function renderStalePerms(root, ghosts) {
|
|||
// but are absent from both the live roster and the kept-state tombstones.
|
||||
// One call, no client-side roster cache, always authoritative.
|
||||
async function fetchAndRenderStalePerms() {
|
||||
const root = $('tombstones-stale-perms');
|
||||
const root = $("tombstones-stale-perms");
|
||||
if (!root) return;
|
||||
try {
|
||||
const resp = await fetch('/api/permissions/stale');
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const resp = await fetch("/api/permissions/stale");
|
||||
if (!resp.ok) throw new Error("http " + resp.status);
|
||||
const data = await resp.json();
|
||||
renderStalePerms(root, data.stale || []);
|
||||
} catch (err) {
|
||||
root.replaceChildren();
|
||||
root.append(el('p', { class: 'meta' }, 'failed to load stale perm data: ' + err));
|
||||
root.append(
|
||||
el("p", { class: "meta" }, "failed to load stale perm data: " + err),
|
||||
);
|
||||
}
|
||||
stalePermsLoaded = true;
|
||||
}
|
||||
|
|
@ -158,21 +191,25 @@ async function fetchAndRenderStalePerms() {
|
|||
let containerLoadTimer = null;
|
||||
|
||||
function cloadFmtBytes(n) {
|
||||
if (!Number.isFinite(n) || n <= 0) return '0';
|
||||
const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
let i = 0; let v = n;
|
||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i += 1; }
|
||||
return v.toFixed(v < 10 && i > 0 ? 1 : 0) + ' ' + u[i];
|
||||
if (!Number.isFinite(n) || n <= 0) return "0";
|
||||
const u = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
let i = 0;
|
||||
let v = n;
|
||||
while (v >= 1024 && i < u.length - 1) {
|
||||
v /= 1024;
|
||||
i += 1;
|
||||
}
|
||||
return v.toFixed(v < 10 && i > 0 ? 1 : 0) + " " + u[i];
|
||||
}
|
||||
function cloadMeter(pct) {
|
||||
const p = Math.max(0, Math.min(100, pct));
|
||||
const cls = p >= 90 ? 'hot' : (p >= 70 ? 'warn' : '');
|
||||
const m = document.createElement('span');
|
||||
m.className = 'cload-meter';
|
||||
m.title = p.toFixed(0) + '%';
|
||||
const fill = document.createElement('span');
|
||||
fill.className = 'fill' + (cls ? ' ' + cls : '');
|
||||
fill.style.width = p + '%';
|
||||
const cls = p >= 90 ? "hot" : p >= 70 ? "warn" : "";
|
||||
const m = document.createElement("span");
|
||||
m.className = "cload-meter";
|
||||
m.title = p.toFixed(0) + "%";
|
||||
const fill = document.createElement("span");
|
||||
fill.className = "fill" + (cls ? " " + cls : "");
|
||||
fill.style.width = p + "%";
|
||||
m.append(fill);
|
||||
return m;
|
||||
}
|
||||
|
|
@ -183,12 +220,14 @@ let lastLoadRows = [];
|
|||
|
||||
function renderContainerLoad(rows) {
|
||||
lastLoadRows = rows;
|
||||
const root = $('container-load-section');
|
||||
const root = $("container-load-section");
|
||||
if (!root) return;
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
root.replaceChildren();
|
||||
const p = document.createElement('p'); p.className = 'meta';
|
||||
p.textContent = 'no running agent containers'; root.append(p);
|
||||
const p = document.createElement("p");
|
||||
p.className = "meta";
|
||||
p.textContent = "no running agent containers";
|
||||
root.append(p);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -197,83 +236,105 @@ function renderContainerLoad(rows) {
|
|||
// resolved by the server) next to the live cgroup readings.
|
||||
const cvByName = new Map(containersState.map((c) => [c.name, c]));
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.className = 'hive-stats-table';
|
||||
const table = document.createElement("table");
|
||||
table.className = "hive-stats-table";
|
||||
// "cpu cap" / "mem cap" show the configured ceilings from ContainerView
|
||||
// (effective drop-in values, take effect on next start/restart).
|
||||
// "limit" remains the live cgroup memory ceiling from /api/container-resources.
|
||||
table.innerHTML = '<thead><tr><th>agent</th><th>cpu</th><th>memory</th>'
|
||||
+ '<th>peak</th><th>limit</th><th>disk</th>'
|
||||
+ '<th class="cload-cap-th" title="configured ceiling — takes effect on next start">cpu cap</th>'
|
||||
+ '<th class="cload-cap-th" title="configured ceiling — takes effect on next start">mem cap</th>'
|
||||
+ '<th></th></tr></thead>';
|
||||
const tb = document.createElement('tbody');
|
||||
table.innerHTML =
|
||||
"<thead><tr><th>agent</th><th>cpu</th><th>memory</th>" +
|
||||
"<th>peak</th><th>limit</th><th>disk</th>" +
|
||||
'<th class="cload-cap-th" title="configured ceiling — takes effect on next start">cpu cap</th>' +
|
||||
'<th class="cload-cap-th" title="configured ceiling — takes effect on next start">mem cap</th>' +
|
||||
"<th></th></tr></thead>";
|
||||
const tb = document.createElement("tbody");
|
||||
for (const r of rows) {
|
||||
const cv = cvByName.get(r.name);
|
||||
const tr = document.createElement('tr');
|
||||
const name = document.createElement('td'); name.textContent = r.name; tr.append(name);
|
||||
const cpu = document.createElement('td'); cpu.className = 'num';
|
||||
cpu.append((Number(r.cpu_pct) || 0).toFixed(1) + '%', cloadMeter(Number(r.cpu_pct) || 0));
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
name.textContent = r.name;
|
||||
tr.append(name);
|
||||
const cpu = document.createElement("td");
|
||||
cpu.className = "num";
|
||||
cpu.append(
|
||||
(Number(r.cpu_pct) || 0).toFixed(1) + "%",
|
||||
cloadMeter(Number(r.cpu_pct) || 0),
|
||||
);
|
||||
tr.append(cpu);
|
||||
const mem = document.createElement('td'); mem.className = 'num';
|
||||
const mem = document.createElement("td");
|
||||
mem.className = "num";
|
||||
const memCur = Number(r.mem_current_bytes) || 0;
|
||||
if (r.mem_max_bytes) {
|
||||
mem.append(cloadFmtBytes(memCur), cloadMeter(100 * memCur / r.mem_max_bytes));
|
||||
mem.append(
|
||||
cloadFmtBytes(memCur),
|
||||
cloadMeter((100 * memCur) / r.mem_max_bytes),
|
||||
);
|
||||
} else {
|
||||
mem.textContent = cloadFmtBytes(memCur);
|
||||
}
|
||||
tr.append(mem);
|
||||
const peak = document.createElement('td'); peak.className = 'num';
|
||||
peak.textContent = r.mem_peak_bytes ? cloadFmtBytes(Number(r.mem_peak_bytes)) : '—';
|
||||
const peak = document.createElement("td");
|
||||
peak.className = "num";
|
||||
peak.textContent = r.mem_peak_bytes
|
||||
? cloadFmtBytes(Number(r.mem_peak_bytes))
|
||||
: "—";
|
||||
tr.append(peak);
|
||||
const lim = document.createElement('td'); lim.className = 'num';
|
||||
lim.textContent = r.mem_max_bytes ? cloadFmtBytes(Number(r.mem_max_bytes)) : '∞';
|
||||
const lim = document.createElement("td");
|
||||
lim.className = "num";
|
||||
lim.textContent = r.mem_max_bytes
|
||||
? cloadFmtBytes(Number(r.mem_max_bytes))
|
||||
: "∞";
|
||||
tr.append(lim);
|
||||
// Disk: on-disk footprint (state dir + container writable rootfs, shared
|
||||
// nix store excluded). Sampled out-of-band every few minutes server-side,
|
||||
// so it's null until the first sample lands — show an em-dash then.
|
||||
const disk = document.createElement('td'); disk.className = 'num';
|
||||
disk.title = 'state dir + container writable rootfs (shared nix store excluded); sampled every few minutes';
|
||||
disk.textContent = (r.disk_bytes != null) ? cloadFmtBytes(Number(r.disk_bytes)) : '—';
|
||||
const disk = document.createElement("td");
|
||||
disk.className = "num";
|
||||
disk.title =
|
||||
"state dir + container writable rootfs (shared nix store excluded); sampled every few minutes";
|
||||
disk.textContent =
|
||||
r.disk_bytes != null ? cloadFmtBytes(Number(r.disk_bytes)) : "—";
|
||||
tr.append(disk);
|
||||
|
||||
// Configured CPU / memory caps from ContainerView (resolved effective
|
||||
// values: per-agent override when set, hive-wide default otherwise).
|
||||
const cpuCap = document.createElement('td'); cpuCap.className = 'num cload-cap';
|
||||
cpuCap.title = 'configured ceiling — takes effect on next start';
|
||||
cpuCap.textContent = cv?.cpu_quota || '—';
|
||||
const cpuCap = document.createElement("td");
|
||||
cpuCap.className = "num cload-cap";
|
||||
cpuCap.title = "configured ceiling — takes effect on next start";
|
||||
cpuCap.textContent = cv?.cpu_quota || "—";
|
||||
tr.append(cpuCap);
|
||||
const memCap = document.createElement('td'); memCap.className = 'num cload-cap';
|
||||
memCap.title = 'configured ceiling — takes effect on next start';
|
||||
memCap.textContent = cv?.memory_max || '—';
|
||||
const memCap = document.createElement("td");
|
||||
memCap.className = "num cload-cap";
|
||||
memCap.title = "configured ceiling — takes effect on next start";
|
||||
memCap.textContent = cv?.memory_max || "—";
|
||||
tr.append(memCap);
|
||||
|
||||
// S3T button toggles the inline edit row for this agent.
|
||||
const actTd = document.createElement('td');
|
||||
const setBtn = document.createElement('button');
|
||||
setBtn.type = 'button';
|
||||
setBtn.className = 'btn btn-sm cload-set-btn';
|
||||
setBtn.textContent = 'S3T';
|
||||
setBtn.title = 'set CPU / memory cap for ' + r.name;
|
||||
const actTd = document.createElement("td");
|
||||
const setBtn = document.createElement("button");
|
||||
setBtn.type = "button";
|
||||
setBtn.className = "btn btn-sm cload-set-btn";
|
||||
setBtn.textContent = "S3T";
|
||||
setBtn.title = "set CPU / memory cap for " + r.name;
|
||||
tr.append(actTd);
|
||||
actTd.append(setBtn);
|
||||
tb.append(tr);
|
||||
|
||||
// Inline edit row (hidden by default, toggled by the S3T button).
|
||||
const editRow = document.createElement('tr');
|
||||
editRow.className = 'cload-edit-row';
|
||||
const editRow = document.createElement("tr");
|
||||
editRow.className = "cload-edit-row";
|
||||
editRow.hidden = true;
|
||||
const editTd = document.createElement('td');
|
||||
const editTd = document.createElement("td");
|
||||
editTd.colSpan = 9;
|
||||
editTd.className = 'cload-edit-cell';
|
||||
editTd.className = "cload-edit-cell";
|
||||
|
||||
const editForm = document.createElement('form');
|
||||
editForm.className = 'cload-edit-form';
|
||||
editForm.addEventListener('submit', async (e) => {
|
||||
const editForm = document.createElement("form");
|
||||
editForm.className = "cload-edit-form";
|
||||
editForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const cpuInput = editForm.querySelector('.cload-cpu-input');
|
||||
const memInput = editForm.querySelector('.cload-mem-input');
|
||||
const errSpan = editForm.querySelector('.cload-edit-err');
|
||||
const cpuInput = editForm.querySelector(".cload-cpu-input");
|
||||
const memInput = editForm.querySelector(".cload-mem-input");
|
||||
const errSpan = editForm.querySelector(".cload-edit-err");
|
||||
const submitBtn = editForm.querySelector('[type="submit"]');
|
||||
errSpan.hidden = true;
|
||||
submitBtn.disabled = true;
|
||||
|
|
@ -283,11 +344,13 @@ function renderContainerLoad(rows) {
|
|||
memory_max: memInput.value.trim(),
|
||||
});
|
||||
const resp = await fetch(
|
||||
'/api/resource-limits/' + encodeURIComponent(r.name),
|
||||
{ method: 'POST', body },
|
||||
"/api/resource-limits/" + encodeURIComponent(r.name),
|
||||
{ method: "POST", body },
|
||||
);
|
||||
if (!resp.ok) {
|
||||
errSpan.textContent = await resp.text().catch(() => 'error ' + resp.status);
|
||||
errSpan.textContent = await resp
|
||||
.text()
|
||||
.catch(() => "error " + resp.status);
|
||||
errSpan.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -295,7 +358,7 @@ function renderContainerLoad(rows) {
|
|||
// ContainerView data (cpu_quota/memory_max) to containersState,
|
||||
// triggering a re-render of the cap columns via refreshContainerLoad.
|
||||
editRow.hidden = true;
|
||||
setBtn.textContent = 'S3T';
|
||||
setBtn.textContent = "S3T";
|
||||
} catch (err) {
|
||||
errSpan.textContent = String(err);
|
||||
errSpan.hidden = false;
|
||||
|
|
@ -304,44 +367,50 @@ function renderContainerLoad(rows) {
|
|||
}
|
||||
});
|
||||
|
||||
const cpuLabel = document.createElement('label');
|
||||
cpuLabel.className = 'cload-edit-label';
|
||||
cpuLabel.textContent = 'cpu quota';
|
||||
const cpuInput = document.createElement('input');
|
||||
cpuInput.type = 'text'; cpuInput.className = 'cload-cpu-input';
|
||||
cpuInput.placeholder = cv ? cv.cpu_quota : 'e.g. 200%';
|
||||
cpuInput.title = 'systemd CPUQuota= value (e.g. "400%"). empty = use hive default';
|
||||
const cpuLabel = document.createElement("label");
|
||||
cpuLabel.className = "cload-edit-label";
|
||||
cpuLabel.textContent = "cpu quota";
|
||||
const cpuInput = document.createElement("input");
|
||||
cpuInput.type = "text";
|
||||
cpuInput.className = "cload-cpu-input";
|
||||
cpuInput.placeholder = cv ? cv.cpu_quota : "e.g. 200%";
|
||||
cpuInput.title =
|
||||
'systemd CPUQuota= value (e.g. "400%"). empty = use hive default';
|
||||
cpuLabel.append(cpuInput);
|
||||
|
||||
const memLabel = document.createElement('label');
|
||||
memLabel.className = 'cload-edit-label';
|
||||
memLabel.textContent = 'mem max';
|
||||
const memInput = document.createElement('input');
|
||||
memInput.type = 'text'; memInput.className = 'cload-mem-input';
|
||||
memInput.placeholder = cv ? cv.memory_max : 'e.g. 8G';
|
||||
memInput.title = 'systemd MemoryMax= value (e.g. "8G", "50%", "infinity"). empty = use hive default';
|
||||
const memLabel = document.createElement("label");
|
||||
memLabel.className = "cload-edit-label";
|
||||
memLabel.textContent = "mem max";
|
||||
const memInput = document.createElement("input");
|
||||
memInput.type = "text";
|
||||
memInput.className = "cload-mem-input";
|
||||
memInput.placeholder = cv ? cv.memory_max : "e.g. 8G";
|
||||
memInput.title =
|
||||
'systemd MemoryMax= value (e.g. "8G", "50%", "infinity"). empty = use hive default';
|
||||
memLabel.append(memInput);
|
||||
|
||||
const submitBtn = document.createElement('button');
|
||||
submitBtn.type = 'submit'; submitBtn.className = 'btn btn-restart cload-save-btn';
|
||||
submitBtn.textContent = 'S4V3';
|
||||
const submitBtn = document.createElement("button");
|
||||
submitBtn.type = "submit";
|
||||
submitBtn.className = "btn btn-restart cload-save-btn";
|
||||
submitBtn.textContent = "S4V3";
|
||||
|
||||
const hintSpan = document.createElement('span');
|
||||
hintSpan.className = 'meta cload-edit-hint';
|
||||
hintSpan.textContent = '↺ restart to apply to a running container';
|
||||
const hintSpan = document.createElement("span");
|
||||
hintSpan.className = "meta cload-edit-hint";
|
||||
hintSpan.textContent = "↺ restart to apply to a running container";
|
||||
|
||||
const errSpan = document.createElement('span');
|
||||
errSpan.className = 'cload-edit-err'; errSpan.hidden = true;
|
||||
const errSpan = document.createElement("span");
|
||||
errSpan.className = "cload-edit-err";
|
||||
errSpan.hidden = true;
|
||||
|
||||
editForm.append(cpuLabel, memLabel, submitBtn, hintSpan, errSpan);
|
||||
editTd.append(editForm);
|
||||
editRow.append(editTd);
|
||||
tb.append(editRow);
|
||||
|
||||
setBtn.addEventListener('click', () => {
|
||||
setBtn.addEventListener("click", () => {
|
||||
const open = !editRow.hidden;
|
||||
editRow.hidden = open;
|
||||
setBtn.textContent = open ? 'S3T' : '✕';
|
||||
setBtn.textContent = open ? "S3T" : "✕";
|
||||
});
|
||||
}
|
||||
table.append(tb);
|
||||
|
|
@ -350,15 +419,17 @@ function renderContainerLoad(rows) {
|
|||
|
||||
async function refreshContainerLoad() {
|
||||
try {
|
||||
const resp = await fetch('/api/container-resources');
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const resp = await fetch("/api/container-resources");
|
||||
if (!resp.ok) throw new Error("http " + resp.status);
|
||||
renderContainerLoad(await resp.json());
|
||||
} catch (e) {
|
||||
const root = $('container-load-section');
|
||||
const root = $("container-load-section");
|
||||
if (root) {
|
||||
root.replaceChildren();
|
||||
const p = document.createElement('p'); p.className = 'meta';
|
||||
p.textContent = 'container load fetch failed: ' + e; root.append(p);
|
||||
const p = document.createElement("p");
|
||||
p.className = "meta";
|
||||
p.textContent = "container load fetch failed: " + e;
|
||||
root.append(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -368,7 +439,10 @@ function startContainerLoadPolling() {
|
|||
containerLoadTimer = setInterval(refreshContainerLoad, 5000);
|
||||
}
|
||||
function stopContainerLoadPolling() {
|
||||
if (containerLoadTimer) { clearInterval(containerLoadTimer); containerLoadTimer = null; }
|
||||
if (containerLoadTimer) {
|
||||
clearInterval(containerLoadTimer);
|
||||
containerLoadTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── render-all (cold load + any full re-render) ──────────────────────────
|
||||
|
|
@ -412,7 +486,7 @@ const SSE_HANDLERS = {
|
|||
// purge actions that opt out of `data-no-refresh`).
|
||||
async function refreshState() {
|
||||
try {
|
||||
const resp = await fetch('/api/state');
|
||||
const resp = await fetch("/api/state");
|
||||
if (resp.ok) syncFromSnapshot(await resp.json());
|
||||
} catch {
|
||||
// best-effort: the page keeps its last-rendered state
|
||||
|
|
@ -431,18 +505,18 @@ async function init() {
|
|||
// Hash-routed sub-tab strip; default K3PT ST4T3. Container-load
|
||||
// polling runs only while the LOAD sub-tab is open (cpu is a short
|
||||
// two-sample read each refresh on the server).
|
||||
document.getElementById('core-tabbar').configure({
|
||||
document.getElementById("core-tabbar").configure({
|
||||
tabs: [
|
||||
{ id: 'kept', label: 'K3PT ST4T3' },
|
||||
{ id: 'load', label: 'C0NT41N3R L04D' },
|
||||
{ id: "kept", label: "K3PT ST4T3" },
|
||||
{ id: "load", label: "C0NT41N3R L04D" },
|
||||
],
|
||||
defaultId: 'kept',
|
||||
defaultId: "kept",
|
||||
onShow: (id) => {
|
||||
if (id === 'load') startContainerLoadPolling();
|
||||
if (id === "load") startContainerLoadPolling();
|
||||
else stopContainerLoadPolling();
|
||||
// Lazy-load stale-perms on first K3PT ST4T3 activation; always
|
||||
// re-fetch on subsequent visits in case perms changed.
|
||||
if (id === 'kept') fetchAndRenderStalePerms();
|
||||
if (id === "kept") fetchAndRenderStalePerms();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -454,13 +528,17 @@ async function init() {
|
|||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
||||
// split issue).
|
||||
const es = openStream(
|
||||
'/api/dashboard/stream?kinds=tombstones_changed,container_state_changed,' +
|
||||
'capabilities_changed,tool_groups_changed',
|
||||
"/api/dashboard/stream?kinds=tombstones_changed,container_state_changed," +
|
||||
"capabilities_changed,tool_groups_changed",
|
||||
);
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
try { ev = JSON.parse(e.data); } catch { return; }
|
||||
try {
|
||||
ev = JSON.parse(e.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const h = SSE_HANDLERS[ev.kind];
|
||||
if (h) h(ev);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue