|
@@ -23,11 +23,6 @@
|
|
|
tr.hist .actbar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin: 2px 0 6px; }
|
|
tr.hist .actbar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin: 2px 0 6px; }
|
|
|
tr.hist .actbar button.act { padding: 2px 8px; font-size: 12px; }
|
|
tr.hist .actbar button.act { padding: 2px 8px; font-size: 12px; }
|
|
|
tr.hist .actnote { font-size: 11px; color: gray; }
|
|
tr.hist .actnote { font-size: 11px; color: gray; }
|
|
|
- tr.hist .logbox { display: flex; gap: 8px; align-items: flex-start; margin: 6px 0; }
|
|
|
|
|
- tr.hist .logbox select { font: 11px ui-monospace, monospace; }
|
|
|
|
|
- tr.hist .logbox pre { flex: 1; min-width: 0; font: 11px/1.4 ui-monospace, monospace;
|
|
|
|
|
- background: color-mix(in srgb, currentColor 5%, canvas); border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
|
|
|
|
|
- border-radius: 3px; padding: 6px; max-height: 240px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
|
|
|
a.open { color: inherit; text-decoration: none; border: 1px solid color-mix(in srgb, currentColor 30%, transparent); border-radius: 3px; padding: 0 4px; }
|
|
a.open { color: inherit; text-decoration: none; border: 1px solid color-mix(in srgb, currentColor 30%, transparent); border-radius: 3px; padding: 0 4px; }
|
|
|
.prov { font-family: ui-monospace, monospace; font-size: 10px; opacity: .8; white-space: nowrap; }
|
|
.prov { font-family: ui-monospace, monospace; font-size: 10px; opacity: .8; white-space: nowrap; }
|
|
|
a.open:hover { background: color-mix(in srgb, currentColor 12%, canvas); }
|
|
a.open:hover { background: color-mix(in srgb, currentColor 12%, canvas); }
|
|
@@ -515,22 +510,10 @@ function render() {
|
|
|
`<a class="open" href="${auth("/r/" + encodeURIComponent(r.name) + "/" + encodeURIComponent(f))}" target="_blank" title="open ${esc(f)}">${esc(f.split('.').pop())}</a>`).join(" ")}</span> ` +
|
|
`<a class="open" href="${auth("/r/" + encodeURIComponent(r.name) + "/" + encodeURIComponent(f))}" target="_blank" title="open ${esc(f)}">${esc(f.split('.').pop())}</a>`).join(" ")}</span> ` +
|
|
|
`${esc(e.date)}${e.time ? "·" + esc(e.time) : ""} ${esc(e.label)} — ${e.pct != null ? esc(e.pct) + "/100" : '<span class="dim">no score</span>'}${e.points ? " (" + esc(e.points) + ")" : ""}${ts}${chip}`;
|
|
`${esc(e.date)}${e.time ? "·" + esc(e.time) : ""} ${esc(e.label)} — ${e.pct != null ? esc(e.pct) + "/100" : '<span class="dim">no score</span>'}${e.points ? " (" + esc(e.points) + ")" : ""}${ts}${chip}`;
|
|
|
}).join("<br>");
|
|
}).join("<br>");
|
|
|
- const logbox = EMBED && LOG.recipe === r.name && LOG.opts.length
|
|
|
|
|
- ? `<div class="logbox"><select class="lognode" title="node streaming">${LOG.opts.map((o, i) => `<option value="${i}"${i === LOG.cur ? " selected" : ""}>${esc(o.cluster)}/${esc(o.node)}</option>`).join("")}</select><pre class="logpre">${esc(LOG.raw)}</pre></div>`
|
|
|
|
|
- : "";
|
|
|
|
|
- out.push(`<tr class="hist"><td colspan="${vis.length}">${actionBar(r)}${logbox}<div>${toml}${r.reports.length ? " " + lines : ' <span class="dim">no reports</span>'}</div></td></tr>`);
|
|
|
|
|
|
|
+ out.push(`<tr class="hist"><td colspan="${vis.length}">${actionBar(r)}<div>${toml}${r.reports.length ? " " + lines : ' <span class="dim">no reports</span>'}</div></td></tr>`);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
document.querySelector("tbody").innerHTML = out.join("");
|
|
document.querySelector("tbody").innerHTML = out.join("");
|
|
|
- // Rebind the live log box (if any): the DOM was rebuilt, so the <pre>/
|
|
|
|
|
- // select re-attach to the live stream state (the stream itself keeps
|
|
|
|
|
- // running in LOG.raw between renders).
|
|
|
|
|
- if (EMBED && LOG.recipe && S.openRow === LOG.recipe) {
|
|
|
|
|
- LOG.pre = document.querySelector("tr.hist .logpre");
|
|
|
|
|
- if (LOG.pre) { LOG.pre.textContent = LOG.raw; LOG.pre.scrollTop = LOG.pre.scrollHeight; }
|
|
|
|
|
- const sel = document.querySelector("tr.hist .lognode");
|
|
|
|
|
- if (sel) sel.onchange = e => { LOG.cur = +e.target.value; attachLog(); };
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
document.querySelector("thead").addEventListener("click", e => {
|
|
document.querySelector("thead").addEventListener("click", e => {
|
|
@@ -564,7 +547,6 @@ document.querySelector("tbody").addEventListener("click", e => {
|
|
|
if (row) {
|
|
if (row) {
|
|
|
const n = row.dataset.n;
|
|
const n = row.dataset.n;
|
|
|
const next = S.openRow === n ? null : n;
|
|
const next = S.openRow === n ? null : n;
|
|
|
- if (LOG.recipe && next !== LOG.recipe) closeLog(); // switching/collapsing drops the stream
|
|
|
|
|
S.openRow = next;
|
|
S.openRow = next;
|
|
|
render();
|
|
render();
|
|
|
}
|
|
}
|
|
@@ -593,86 +575,28 @@ function setNote(recipe, text) {
|
|
|
if (n) n.textContent = text;
|
|
if (n) n.textContent = text;
|
|
|
}
|
|
}
|
|
|
// The expanded row carries the controls before the toml link: serve/stop
|
|
// The expanded row carries the controls before the toml link: serve/stop
|
|
|
-// (tracked dashboard jobs), a live log tail for a running recipe (the
|
|
|
|
|
-// dashboard's /api/logs attach + the /events log stream), and a bench
|
|
|
|
|
-// launch (POST /api/bench — the standard campaign as a job; the report
|
|
|
|
|
-// lands in the shared recipe repo and re-appears in this table via the
|
|
|
|
|
-// live stream).
|
|
|
|
|
|
|
+// (tracked dashboard jobs), a logs button (opens a logs WINDOW in the parent
|
|
|
|
|
+// dashboard via postMessage — same window as the cluster overview, not an
|
|
|
|
|
+// inline stream), and a bench launch (POST /api/bench — the standard campaign
|
|
|
|
|
+// as a job; the report lands in the shared recipe repo and re-appears in this
|
|
|
|
|
+// table via the live stream).
|
|
|
function actionBar(r) {
|
|
function actionBar(r) {
|
|
|
if (!EMBED) return "";
|
|
if (!EMBED) return "";
|
|
|
const running = r.up != null || r.fleet_up;
|
|
const running = r.up != null || r.fleet_up;
|
|
|
let b = running
|
|
let b = running
|
|
|
? `<button class="act" data-act="stop" data-r="${esc(r.name)}" title="stop (dashboard job)">⏹ stop</button>`
|
|
? `<button class="act" data-act="stop" data-r="${esc(r.name)}" title="stop (dashboard job)">⏹ stop</button>`
|
|
|
: `<button class="act" data-act="serve" data-r="${esc(r.name)}" title="serve (dashboard job, cluster auto)">▶ serve</button>`;
|
|
: `<button class="act" data-act="serve" data-r="${esc(r.name)}" title="serve (dashboard job, cluster auto)">▶ serve</button>`;
|
|
|
- if (running) b += `<button class="act" data-act="logs" data-r="${esc(r.name)}" title="live container logs">▤ logs</button>`;
|
|
|
|
|
|
|
+ if (running) b += `<button class="act" data-act="logs" data-r="${esc(r.name)}" title="open a logs window (head node)">▤ logs</button>`;
|
|
|
b += `<button class="act" data-act="bench" data-r="${esc(r.name)}" title="standard bench (seed 42, hardmode, perf c1,2) as a dashboard job — the report lands in the recipe repo">⚡ bench</button>`;
|
|
b += `<button class="act" data-act="bench" data-r="${esc(r.name)}" title="standard bench (seed 42, hardmode, perf c1,2) as a dashboard job — the report lands in the recipe repo">⚡ bench</button>`;
|
|
|
return `<div class="actbar">${b}<span class="actnote" data-r="${esc(r.name)}">${esc(NOTES[r.name] || "")}</span></div>`;
|
|
return `<div class="actbar">${b}<span class="actnote" data-r="${esc(r.name)}">${esc(NOTES[r.name] || "")}</span></div>`;
|
|
|
}
|
|
}
|
|
|
-// ---- live log tail (one active attach at a time — one row expands at a time)
|
|
|
|
|
-const LOG = { attach: null, es: null, raw: "", buf: "", pre: null, opts: [], cur: 0, recipe: "" };
|
|
|
|
|
-function paintLog() {
|
|
|
|
|
- if (LOG.buf) { LOG.raw += LOG.buf; LOG.buf = ""; }
|
|
|
|
|
- const lines = LOG.raw.split("\n");
|
|
|
|
|
- if (lines.length > 2000) LOG.raw = lines.slice(-2000).join("\n");
|
|
|
|
|
- if (LOG.pre && LOG.pre.isConnected) { LOG.pre.textContent = LOG.raw; LOG.pre.scrollTop = LOG.pre.scrollHeight; }
|
|
|
|
|
-}
|
|
|
|
|
-function ensureLogStream() {
|
|
|
|
|
- if (LOG.es) return;
|
|
|
|
|
- LOG.es = new EventSource("/events");
|
|
|
|
|
- LOG.es.onmessage = e => {
|
|
|
|
|
- let ev; try { ev = JSON.parse(e.data); } catch { return; }
|
|
|
|
|
- if (ev.type !== "log" || ev.attach !== LOG.attach) return;
|
|
|
|
|
- LOG.buf += ev.line + "\n";
|
|
|
|
|
- paintLog();
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-async function attachLog() {
|
|
|
|
|
- const o = LOG.opts[LOG.cur];
|
|
|
|
|
- if (!o) return;
|
|
|
|
|
- if (LOG.attach) {
|
|
|
|
|
- fetch("/api/logs", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ attach_id: LOG.attach }) }).catch(() => {});
|
|
|
|
|
- LOG.attach = null;
|
|
|
|
|
- }
|
|
|
|
|
- const res = await fetch("/api/logs", {
|
|
|
|
|
- method: "POST",
|
|
|
|
|
- headers: { "Content-Type": "application/json" },
|
|
|
|
|
- body: JSON.stringify({ cluster: o.cluster, node: o.node, recipe: LOG.recipe, rank: o.rank }),
|
|
|
|
|
- });
|
|
|
|
|
- const j = await res.json().catch(() => ({}));
|
|
|
|
|
- if (!res.ok || !j.attach_id) { setNote(LOG.recipe, "log attach failed: " + (j.error || res.status)); return; }
|
|
|
|
|
- LOG.attach = j.attach_id;
|
|
|
|
|
- LOG.raw = (j.history || []).join("\n");
|
|
|
|
|
- ensureLogStream();
|
|
|
|
|
- paintLog();
|
|
|
|
|
-}
|
|
|
|
|
-function closeLog() {
|
|
|
|
|
- if (LOG.attach) fetch("/api/logs", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ attach_id: LOG.attach }) }).catch(() => {});
|
|
|
|
|
- LOG.attach = null;
|
|
|
|
|
- if (LOG.es) { LOG.es.close(); LOG.es = null; }
|
|
|
|
|
- LOG.raw = ""; LOG.buf = ""; LOG.pre = null; LOG.opts = []; LOG.cur = 0; LOG.recipe = "";
|
|
|
|
|
-}
|
|
|
|
|
-async function openLogs(recipe) {
|
|
|
|
|
- LOG.recipe = recipe;
|
|
|
|
|
- setNote(recipe, "opening logs…");
|
|
|
|
|
- let st;
|
|
|
|
|
- try { st = await (await fetch("/api/state")).json(); }
|
|
|
|
|
- catch (err) { setNote(recipe, "✗ " + err); return; }
|
|
|
|
|
- const opts = [];
|
|
|
|
|
- for (const c of st.clusters || [])
|
|
|
|
|
- for (const n of c.nodes || [])
|
|
|
|
|
- if ((n.containers || []).some(x => x.recipe === recipe))
|
|
|
|
|
- opts.push({ cluster: c.name, node: n.name, rank: n.rank });
|
|
|
|
|
- LOG.opts = opts; LOG.cur = 0;
|
|
|
|
|
- if (!opts.length) { setNote(recipe, "no live container found — probe lag, or the recipe just stopped"); render(); return; }
|
|
|
|
|
- render(); // the log box (select + pre) renders for LOG.recipe
|
|
|
|
|
- await attachLog();
|
|
|
|
|
-}
|
|
|
|
|
|
|
+
|
|
|
document.addEventListener("click", async (e) => {
|
|
document.addEventListener("click", async (e) => {
|
|
|
const b = e.target.closest("button.act");
|
|
const b = e.target.closest("button.act");
|
|
|
if (!b) return;
|
|
if (!b) return;
|
|
|
e.stopPropagation(); // no row toggle under the action button
|
|
e.stopPropagation(); // no row toggle under the action button
|
|
|
const kind = b.dataset.act, recipe = b.dataset.r;
|
|
const kind = b.dataset.act, recipe = b.dataset.r;
|
|
|
- if (kind === "logs") { openLogs(recipe); return; }
|
|
|
|
|
|
|
+ if (kind === "logs") { parent.postMessage({ sm121: "openLogs", recipe }, "*"); return; }
|
|
|
const path = kind === "serve" ? "/api/serve" : kind === "stop" ? "/api/stop" : "/api/bench";
|
|
const path = kind === "serve" ? "/api/serve" : kind === "stop" ? "/api/stop" : "/api/bench";
|
|
|
b.disabled = true;
|
|
b.disabled = true;
|
|
|
try {
|
|
try {
|