| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- <!doctype html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>sm121 — tested recipes</title>
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%235ee0a0'/%3E%3Cpath d='M16 5.5l2.7 7.8 7.8 2.7-7.8 2.7L16 26.5l-2.7-7.8L5.5 16l7.8-2.7z' fill='%23141a21'/%3E%3C/svg%3E">
- <style>
- :root { color-scheme: light dark; }
- body { font: 13px/1.45 system-ui, sans-serif; margin: 16px; }
- h1 { font-size: 18px; margin: 0 0 10px; }
- #bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
- #q { flex: 1 1 220px; max-width: 380px; padding: 4px 8px; }
- select, button, #cols summary { padding: 3px 8px; cursor: pointer; }
- #cols { position: relative; display: inline-block; }
- #colbox { position: absolute; z-index: 5; background: canvas; border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
- padding: 6px 10px; max-height: 60vh; overflow: auto; white-space: nowrap; }
- #colbox label { display: block; }
- table { border-collapse: collapse; width: 100%; }
- th, td { border: 1px solid color-mix(in srgb, currentColor 20%, transparent); padding: 3px 8px; text-align: left; vertical-align: top; }
- th { cursor: pointer; user-select: none; white-space: nowrap; position: sticky; top: 0; background: canvas; }
- tr.g1 td { font-weight: 700; background: color-mix(in srgb, currentColor 8%, canvas); cursor: pointer; }
- tr.g2 td { font-style: italic; background: color-mix(in srgb, currentColor 4%, canvas); cursor: pointer; }
- tr.hist td { font-family: ui-monospace, monospace; font-size: 12px; background: color-mix(in srgb, currentColor 3%, canvas); }
- 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 .actnote { font-size: 11px; color: gray; }
- 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; }
- a.open:hover { background: color-mix(in srgb, currentColor 12%, canvas); }
- tr.row { cursor: pointer; }
- /* hover-revealed cell buttons sit at the right edge, so both cells
- reserve room for them instead of letting the text slide under */
- td.recipe, td.model { position: relative; }
- td.recipe { padding-right: 26px; }
- td.model { padding-right: 64px; }
- td.recipe .copy, td.model .copy, td.model .hf { position: absolute; top: 50%; transform: translateY(-50%);
- visibility: hidden; cursor: pointer; padding: 0 4px; border-radius: 3px; border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
- background: canvas; color: inherit; font: inherit; line-height: 1.3; text-decoration: none; }
- td.recipe .copy, td.model .copy { right: 3px; }
- td.model .hf { right: 32px; font-size: 10px; font-weight: 600; letter-spacing: .3px; }
- td.recipe:hover .copy, td.model:hover .copy, td.model:hover .hf { visibility: visible; }
- td.recipe .copy:hover, td.model .copy:hover, td.model .hf:hover { background: color-mix(in srgb, currentColor 12%, canvas); }
- td.recipe .copied, td.model .copied { color: green; font-weight: 600; }
- td.recipe .medals { letter-spacing: 1px; }
- .half { -webkit-background-clip: text; background-clip: text; color: transparent; }
- td.num { text-align: right; white-space: nowrap; }
- #count { color: gray; }
- #live { color: #2a9d2a; font-size: 11px; white-space: nowrap; }
- #live.dim { color: gray; }
- #bell { position: relative; padding: 3px 8px; cursor: pointer; background: none; border: 1px solid color-mix(in srgb, currentColor 25%, transparent); border-radius: 4px; }
- #bell svg { vertical-align: -3px; }
- #bellcount { position: absolute; top: -7px; right: -7px; min-width: 17px; height: 17px; line-height: 17px; text-align: center;
- font-size: 10px; font-weight: 700; color: #fff; background: #c0392b; border-radius: 9px; padding: 0 3px; }
- #notifbox { position: absolute; z-index: 6; right: 0; top: 100%; width: min(460px, 90vw); max-height: 60vh; overflow: auto;
- background: canvas; border: 1px solid color-mix(in srgb, currentColor 25%, transparent); border-radius: 5px; padding: 6px; }
- #bar { position: relative; }
- .noti { display: flex; gap: 8px; padding: 5px 6px; border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent); cursor: pointer; }
- .noti:hover { background: color-mix(in srgb, currentColor 6%, canvas); }
- .noti:last-child { border-bottom: none; }
- .nic { font-weight: 700; }
- @keyframes ring { 0% { transform: rotate(0) } 20% { transform: rotate(13deg) } 45% { transform: rotate(-11deg) } 70% { transform: rotate(6deg) } 100% { transform: rotate(0) } }
- #bell.ring svg { animation: ring 0.6s ease; transform-origin: 50% 0; }
- .dim { color: gray; }
- /* Model-settings dialog (⚙ settings): per-agent client config for the
- recipe's served model. */
- #msdlg { position: fixed; inset: 0; z-index: 40; display: none; align-items: center; justify-content: center;
- background: rgba(0, 0, 0, .45); }
- #msdlg.on { display: flex; }
- #msdlg .panel { background: canvas; color: canvastext; border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
- border-radius: 8px; padding: 14px 16px; width: min(760px, 94vw); max-height: 88vh; overflow: auto;
- box-shadow: 0 12px 40px rgba(0, 0, 0, .4); }
- #msdlg h2 { font-size: 15px; margin: 0 0 4px; }
- #msdlg .ep { font-family: ui-monospace, monospace; font-size: 11px; color: gray; margin-bottom: 10px; word-break: break-all; }
- #msdlg .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
- #msdlg label.inc { font-size: 12px; display: inline-flex; gap: 5px; align-items: center; cursor: pointer; }
- #msdlg .tgt { font-family: ui-monospace, monospace; font-size: 11px; color: gray; margin: 0 0 4px; }
- #msdlg pre { margin: 0 0 10px; padding: 10px 12px; border-radius: 6px; font-size: 12px; line-height: 1.4; overflow: auto; max-height: 46vh;
- background: color-mix(in srgb, currentColor 6%, canvas); border: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
- #msdlg .foot { display: flex; gap: 8px; justify-content: flex-end; }
- #msdlg .err { color: #c0392b; font-size: 12px; }
- #msdlg .meta { font-size: 11px; color: gray; margin-bottom: 8px; }
- /* Embedded in the dashboard's Recipes window: the window bar carries the
- title and the dashboard header carries the connection indicator. Both
- SSE streams come from the same server (only the paths differ), so the
- panel's own chip can never disagree with it — it is pure duplication. */
- body.embed h1 { display: none; }
- body.embed #bar { margin-top: 8px; }
- body.embed #live { display: none; }
- </style>
- </head>
- <body>
- <h1>sm121 — tested recipes & benchmark history</h1>
- <div id="bar">
- <input id="q" type="search" placeholder="Search recipes, models, images, labels…">
- <label>Group:
- <select id="group">
- <option value="pm">Parent → Model</option>
- <option value="parent">Parent</option>
- <option value="mq">Model → Quant</option>
- <option value="model">Model</option>
- <option value="quant">Quant</option>
- <option value="engine">Engine</option>
- <option value="" selected>None (flat)</option>
- </select>
- </label>
- <label>TP:
- <select id="tp"><option value="">All</option></select>
- </label>
- <label>Medals:
- <select id="medal">
- <option value="" selected>All</option>
- <option value="m">🥇🥈🥉</option>
- <option value="1">🥇 gold</option>
- <option value="2">🥈 silver</option>
- <option value="3">🥉 bronze</option>
- </select>
- </label>
- <label><input type="checkbox" id="uponly"> ● running</label>
- <details id="cols"><summary>Columns ▾</summary><div id="colbox"></div></details>
- <label>Rows/page:
- <select id="size"><option>25</option><option selected>50</option><option>100</option><option value="0">All</option></select>
- </label>
- <button id="prev">‹</button><span id="count"></span><button id="next">›</button>
- <button id="syncbtn" title="sync the recipe repos now (dashboard job; new recipes appear via the live stream)" style="display:none">⟳ sync</button>
- <span id="repostat" class="dim" style="display:none"></span>
- <span id="live" class="dim" title="live updates">● off</span>
- <button id="bell" title="updates" aria-label="updates">
- <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
- <span id="bellcount" style="display:none"></span>
- </button>
- <div id="notifbox" style="display:none"></div>
- </div>
- <table id="tbl"><thead></thead><tbody></tbody></table>
- <div id="msdlg" role="dialog" aria-modal="true" aria-labelledby="msdtitle">
- <div class="panel">
- <h2 id="msdtitle">Model settings</h2>
- <div class="ep" id="msdep"></div>
- <div class="row">
- <select id="msagent" title="pick the agent you want settings for"></select>
- <label class="inc" title="include the endpoint/provider section (base URL, api key, wire api) as well as the model itself">
- <input type="checkbox" id="msprov"> include provider
- </label>
- </div>
- <div class="meta" id="msmeta"></div>
- <div class="tgt" id="mstgt"></div>
- <pre id="mscode"></pre>
- <div class="err" id="mserr"></div>
- <div class="foot">
- <button id="mscopy">copy</button>
- <button id="msclose">close</button>
- </div>
- </div>
- </div>
- <script>
- "use strict";
- // Auth (dashboard embedding): the same-origin dashboard page stores the
- // `sm121 web --token` value in localStorage — the SSE stream needs it as a
- // query param (EventSource cannot set headers). `sm121 report --serve`
- // (standalone) never sets a token, so auth() is a no-op there.
- // Auth: when embedded in the sm121 web dashboard's recipes panel, the
- // parent has already exchanged the dashboard token for a HttpOnly session
- // cookie — every same-origin request (SSE, the /r/ links) carries it. The
- // standalone `report --serve` is ungated. The legacy ?token= form is gone
- // (URLs must never carry the token).
- const auth = (p) => p;
- let DATA = {{DATA}};
- const COLS = [
- ["name", "Recipe"], ["state", "State"], ["uptime", "Uptime"], ["runs", "Launches"],
- ["title", "Title"], ["model", "Model"], ["quant", "Quant"],
- ["engine", "Engine"], ["image", "Image"], ["nodes", "Nodes"], ["tp", "TP"],
- ["deploy", "Deploy"], ["port", "Port"], ["n", "Reports"], ["last", "Last bench"],
- ["points", "Points"], ["pct", "Score /100"], ["stars", "Bench"],
- ["tps1", "Tok/s c1"], ["tps2", "Tok/s c2"], ["parent", "Parent"],
- ];
- const NUM = new Set(["nodes", "tp", "port", "n", "pct", "tps1", "tps2", "stars", "state", "uptime", "runs"]);
- const MEDAL = { 1: "🥇", 2: "🥈", 3: "🥉" };
- // three-tier medal badge: family | model | tp (slots shown only when earned)
- function medalBadge(r) {
- const M = r.medals || {};
- const g = v => MEDAL[v] || "";
- const parts = [g(M.p), g(M.m), g(M.t)].filter(Boolean);
- if (!parts.length) return "";
- return `<span class="medals" title="family ${g(M.p) || "–"} · model ${g(M.m) || "–"} · tp${r.tp} ${g(M.t) || "–"}">${parts.join("|")}</span> `;
- }
- // run-ledger helpers: who is up right now, for how long (live — a 30 s tick
- // re-renders while anything is running)
- const nowSecs = () => Math.floor(Date.now() / 1000);
- const fmtUp = s => {
- s = Math.max(0, s | 0);
- const d = (s / 86400) | 0, h = ((s % 86400) / 3600) | 0, m = ((s % 3600) / 60) | 0;
- return d ? d + "d " + h + "h" : h ? h + "h " + m + "m" : m ? m + "m" : s + "s";
- };
- const fmtSince = u => new Date(u * 1000).toISOString().slice(0, 16).replace("T", " ");
- // star grade of the latest completed bench. The 10-color palette covers
- // 0..100 a decade per color (color = which decade the score is in); the
- // star count shows the position WITHIN that decade (1 star = 2 points of
- // the band, half star = 1) — 95 → green, 2.5 stars. Sort key is the
- // ABSOLUTE score.
- const PALETTE = [
- "red", "firebrick", "brown", "sienna", "chocolate",
- "darkorange", "gold", "yellowgreen", "greenyellow", "green",
- ];
- function starInfo(r) {
- const e = latest(r, x => x.done && x.pct != null);
- if (!e) return null;
- const band = Math.min(9, Math.floor(e.pct / 10));
- return { pct: e.pct, band, s: Math.round(e.pct - band * 10) / 2 };
- }
- function starGlyphs(si) {
- const col = PALETTE[si.band];
- let out = "";
- for (let i = 1; i <= 5; i++) {
- if (si.s >= i) out += `<span style="color:${col}">★</span>`;
- else if (si.s >= i - 0.5)
- out += `<span class="half" style="background-image:linear-gradient(90deg,${col} 50%,color-mix(in srgb,${col} 30%,transparent) 50%)">★</span>`;
- else out += `<span style="color:${col};opacity:.3">☆</span>`;
- }
- return out;
- }
- // decode speed (tg t/s) from a run's llama-benchy perf cells
- function tg(e, d, c) { const p = e && e.perf ? e.perf.find(p => p.d === d && p.c === c) : null; return p ? p.tg : null; }
- // latest report matching a predicate, for the points / score columns
- function latest(r, f) { for (let i = r.reports.length - 1; i >= 0; i--) if (f(r.reports[i])) return r.reports[i]; return null; }
- function cell(r, k) {
- if (k === "n") return r.reports.length;
- if (k === "last") return r.reports.length ? r.reports[r.reports.length - 1].date : "";
- if (k === "points") { const e = latest(r, e => e.points); return e ? e.points : ""; }
- if (k === "pct") { const e = latest(r, e => e.pct != null); return e ? e.pct : ""; }
- if (k === "tps1" || k === "tps2") { const e = latest(r, e => e.perf && e.perf.length); return tg(e, 0, k === "tps1" ? 1 : 2) ?? ""; }
- if (k === "stars") { const si = starInfo(r); return si ? si.pct : -1; } // sort by the absolute score
- if (k === "state" || k === "uptime") return r.up != null ? nowSecs() - r.up : r.fleet_up ? 0 : -1; // live uptime secs (fleet-up: unknown start)
- if (k === "runs") return r.runs ?? 0;
- return r[k];
- }
- // deep-link state: the full view (filter, grouping, sort, columns, paging,
- // collapsed groups) lives in the URL — survives any reload and is shareable
- // Update notifications: pure diff between two rows snapshots → the list of
- // what changed (new bench, new/removed recipe, changed entry or row config).
- // Newest first. Probed by the node test, extracted between the markers.
- /*NOTIFDIFF:START*/
- function notifDiff(prev, next) {
- const out = [];
- const byName = new Map(prev.map(r => [r.name, r]));
- const meta = r => JSON.stringify({ ...r, reports: [] });
- const item = (t, r, e) => ({
- t, name: r.name,
- label: e ? e.label : (t === "recipe" ? (r.model || r.title) : "recipe config"),
- date: e ? e.date : "", time: e ? e.time : null,
- score: e ? e.pct : null, points: e ? e.points : null,
- files: e ? (e.files || [e.file]) : [],
- });
- for (const r of next) {
- const old = byName.get(r.name);
- if (!old) { out.push(item("recipe", r, null)); continue; }
- if (meta(old) !== meta(r)) out.push(item("config", r, null));
- const oldR = new Map(old.reports.map(e => [e.date + "\u0000" + e.label, e]));
- for (const e of r.reports) {
- const oe = oldR.get(e.date + "\u0000" + e.label);
- if (!oe) out.push(item("bench", r, e));
- else if (JSON.stringify(oe) !== JSON.stringify(e)) out.push(item("change", r, e));
- }
- }
- const nextNames = new Set(next.map(r => r.name));
- for (const r of prev)
- if (!nextNames.has(r.name)) out.push(item("removed", r, null));
- return out.reverse();
- }
- /*NOTIFDIFF:END*/
- // LIVE: the server re-scans the reports/ dirs on file-system events and
- // pushes the rows JSON over SSE whenever something changed (new bench,
- // finishing run, new report file). The view state (filter, sort, paging,
- // columns, collapsed groups) is untouched — render() re-derives everything
- // from DATA.
- const liveEl = document.getElementById("live");
- function syncTp() {
- const vals = [...new Set(DATA.map(r => r.tp))].sort((a, b) => a - b);
- const sel = document.getElementById("tp");
- if (vals.map(String).join(",") !== [...sel.options].map(o => o.value).join(","))
- sel.innerHTML = '<option value="">All</option>' + vals.map(v => `<option>${v}</option>`).join("");
- if (S.tp && ![...sel.options].some(o => o.value === S.tp)) S.tp = "";
- sel.value = S.tp;
- }
- // Bell + notifications: UNREAD counts updates since the panel was last
- // opened; NOTIFS (capped) is the list the panel shows. Clicking an item
- // expands the matching row in the table.
- const bellEl = document.getElementById("bell");
- const notifBox = document.getElementById("notifbox");
- let UNREAD = 0;
- let NOTIFS = [];
- const NICON = { bench: "+", change: "↻", recipe: "★", config: "✎", removed: "−" };
- function renderBell() {
- const c = document.getElementById("bellcount");
- c.textContent = String(UNREAD);
- c.style.display = UNREAD ? "" : "none";
- }
- function renderNotifs() {
- if (!NOTIFS.length) {
- notifBox.innerHTML = '<div class="dim" style="padding:8px">no updates yet</div>';
- return;
- }
- notifBox.innerHTML = NOTIFS.map(n => {
- const when = n.date ? `${n.date}${n.time ? "·" + n.time : ""}` : "";
- const score = n.score != null ? `${n.score} /100` : n.points || "";
- const links = n.files.map(f =>
- `<a class="open" target="_blank" href="${auth("/r/" + encodeURIComponent(n.name) + "/" + encodeURIComponent(f))}" title="open ${esc(f)}">${esc(f.split(".").pop())}</a>`).join(" ");
- return `<div class="noti" data-name="${esc(n.name)}" title="show in table">
- <span class="nic">${NICON[n.t] || "•"}</span>
- <div><b>${esc(n.name)}</b> <span class="dim">${esc(n.label)}</span><br>
- <span class="dim">${esc(when)}</span> ${score ? esc(score) : ""} ${links}</div>
- </div>`;
- }).join("");
- }
- bellEl.addEventListener("click", e => {
- e.stopPropagation();
- const open = notifBox.style.display === "block";
- notifBox.style.display = open ? "none" : "block";
- if (!open) { UNREAD = 0; renderBell(); }
- });
- document.addEventListener("click", () => { notifBox.style.display = "none"; });
- notifBox.addEventListener("click", e => {
- const it = e.target.closest(".noti");
- if (!it) return;
- S.openRow = it.dataset.name;
- notifBox.style.display = "none";
- render();
- });
- let es = null;
- function connect() {
- if (es) es.close();
- es = new EventSource(auth("/events"));
- es.addEventListener("open", () => { liveEl.textContent = "● live"; liveEl.classList.remove("dim"); });
- es.addEventListener("error", () => {
- liveEl.textContent = "● off"; liveEl.classList.add("dim");
- // readyState CLOSED = EventSource gave up for good — recreate it
- if (es.readyState === 2) setTimeout(connect, 1000);
- });
- es.onmessage = ev => {
- let rows; try { rows = JSON.parse(ev.data); } catch { return; }
- if (JSON.stringify(rows) === JSON.stringify(DATA)) return;
- const upd = notifDiff(DATA, rows);
- DATA = rows; syncTp(); render();
- liveEl.title = "updated " + new Date().toLocaleTimeString();
- if (upd.length) {
- NOTIFS.unshift(...upd);
- if (NOTIFS.length > 100) NOTIFS.length = 100;
- UNREAD += upd.length;
- renderBell(); renderNotifs();
- bellEl.classList.remove("ring"); void bellEl.offsetWidth; bellEl.classList.add("ring");
- }
- };
- }
- connect();
- renderBell(); renderNotifs();
- const P = new URLSearchParams(location.search);
- // Persistence: URL params (deep links) win; without them the last view is
- // restored from localStorage — bare reloads (server restart, new tab,
- // binary update) keep the column/settings state.
- const LS = (() => {
- try { return JSON.parse(localStorage.getItem("report-view") || "{}"); } catch { return {}; }
- })();
- const pick = (k, dflt) => P.get(k) ?? (LS[k] ?? dflt);
- const size0 = +(pick("size", 50));
- // distinct TP values from the data, for the exact-TP filter
- const TPVALS = [...new Set(DATA.map(r => r.tp))].sort((a, b) => a - b);
- for (const v of TPVALS) {
- const o = document.createElement("option");
- o.value = String(v);
- o.textContent = String(v);
- document.getElementById("tp").appendChild(o);
- }
- const S = {
- q: pick("q", ""),
- group: pick("group", ""),
- sort: pick("sort", "name"),
- dir: pick("dir", 1) * 1 || 1,
- size: [25, 50, 100, 0].includes(size0) ? size0 : 50,
- page: Math.max(0, +(P.get("page") ?? 0)), // page stays URL-only (fresh view = top)
- hidden: new Set(String(pick("hidden", "title,image,port,parent")).split(",").filter(Boolean)),
- closed: new Set(String(pick("closed", "")).split("|").map(decodeURIComponent).filter(Boolean)),
- openRow: pick("open", null),
- medal: pick("medal", ""),
- uponly: pick("uponly", "") === "1",
- tp: TPVALS.includes(+(pick("tp", ""))) ? String(pick("tp", "")) : "",
- };
- document.getElementById("q").value = S.q;
- document.getElementById("group").value = S.group;
- document.getElementById("size").value = String(S.size);
- document.getElementById("tp").value = S.tp;
- document.getElementById("medal").value = S.medal;
- document.getElementById("uponly").checked = S.uponly;
- function syncUrl() {
- const p = new URLSearchParams();
- if (S.q) p.set("q", S.q);
- if (S.group !== "") p.set("group", S.group);
- if (S.sort !== "name") p.set("sort", S.sort);
- if (S.dir !== 1) p.set("dir", String(S.dir));
- if (S.size !== 50) p.set("size", String(S.size));
- if (S.page) p.set("page", String(S.page));
- const hidden = [...S.hidden].sort();
- if (hidden.join(",") !== ["title", "image", "port", "parent"].sort().join(",")) p.set("hidden", hidden.join(","));
- const closed = [...S.closed].map(encodeURIComponent);
- if (closed.length) p.set("closed", closed.join("|"));
- if (S.openRow) p.set("open", S.openRow);
- if (S.medal) p.set("medal", S.medal);
- if (S.uponly) p.set("uponly", "1");
- if (S.tp) p.set("tp", S.tp);
- const qs = p.toString();
- history.replaceState(null, "", qs ? "?" + qs : location.pathname);
- try {
- localStorage.setItem("report-view", JSON.stringify({
- q: S.q, group: S.group, sort: S.sort, dir: S.dir, size: S.size,
- hidden: [...S.hidden], closed: [...S.closed], openRow: S.openRow,
- medal: S.medal, tp: S.tp, uponly: S.uponly ? "1" : "",
- }));
- } catch { /* private mode etc. — URL still carries the state */ }
- }
- const esc = s => String(s).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c]));
- function filtered() {
- const q = S.q.toLowerCase();
- return DATA.filter(r => (!S.tp || String(r.tp) === S.tp) &&
- (!S.uponly || r.up != null || r.fleet_up) &&
- (!S.medal || (S.medal === "m" ? (r.medals && (r.medals.p || r.medals.m || r.medals.t)) : r.medals && String(r.medals.p) === S.medal)) &&
- (!q || (r.name + " " + r.title + " " + r.model + " " + r.parent + " " + r.quant + " " +
- r.engine + " " + r.image + " " + r.deploy + " " +
- r.reports.map(e => e.date + " " + e.label).join(" ")).toLowerCase().includes(q)));
- }
- function sorted(rows) {
- const gk = { mq: r => r.parent + "\0" + r.quant, pm: r => r.parent + "\0" + r.model,
- parent: r => r.parent, model: r => r.parent,
- quant: r => r.quant, engine: r => r.engine }[S.group];
- const k = S.sort, d = S.dir;
- const val = r => {
- if (k === "points") { const e = latest(r, e => e.ratio != null); return e ? e.ratio : -1; }
- if (k === "tps1" || k === "tps2") { const e = latest(r, e => e.perf && e.perf.length); return tg(e, 0, k === "tps1" ? 1 : 2) ?? -1; }
- const v = cell(r, k); return NUM.has(k) || k === "last" ? v : String(v).toLowerCase();
- };
- return rows.slice().sort((a, b) => {
- if (gk) { const ga = gk(a), gb = gk(b); if (ga !== gb) return ga.localeCompare(gb); }
- let x = val(a), y = val(b);
- if (x === y) return a.name.localeCompare(b.name);
- if (typeof x === "number" && typeof y === "number") return (x - y) * d;
- return String(x).localeCompare(String(y)) * d;
- });
- }
- function gkey(r, level) {
- if (S.group === "pm") return level === 1 ? r.parent : r.parent + "\u0001" + r.model;
- if (S.group === "parent") return level === 1 ? r.parent : null;
- if (S.group === "mq") return level === 1 ? r.model : r.model + "\u0001" + r.quant;
- const f = { model: "model", quant: "quant", engine: "engine" }[S.group];
- return level === 1 ? r[f] : null;
- }
- function render() {
- syncUrl();
- // visible columns
- const vis = COLS.filter(c => !S.hidden.has(c[0]));
- document.querySelector("thead").innerHTML = "<tr>" + vis.map(c => {
- const arrow = S.sort === c[0] ? (S.dir > 0 ? " ▲" : " ▼") : "";
- return `<th data-k="${c[0]}">${esc(c[1])}${arrow}</th>`;
- }).join("") + "</tr>";
- const all = sorted(filtered());
- const size = +document.getElementById("size").value;
- const pages = size ? Math.max(1, Math.ceil(all.length / size)) : 1;
- if (S.page >= pages) S.page = pages - 1;
- const rows = size ? all.slice(S.page * size, S.page * size + size) : all;
- document.getElementById("count").textContent = size
- ? `${all.length ? S.page * size + 1 : 0}–${Math.min(all.length, S.page * size + size)} / ${all.length}` : `${all.length}`;
- // group tree over the current page slice
- const out = [];
- let g1 = null, g2 = null;
- for (const r of rows) {
- if (S.group) {
- const k1 = gkey(r, 1);
- if (k1 !== g1) {
- g1 = k1; g2 = null;
- const closed = S.closed.has("1\0" + k1);
- out.push(`<tr class="g1" data-g="${encodeURIComponent("1\0" + k1)}"><td colspan="${vis.length}">${closed ? "▶" : "▼"} ${esc(k1)} <span class="dim">(${all.filter(x => gkey(x, 1) === k1).length})</span></td></tr>`);
- }
- if (S.closed.has("1\0" + k1)) continue;
- const k2 = gkey(r, 2);
- if (k2 !== null) {
- if (k2 !== g2) {
- g2 = k2;
- const closed = S.closed.has("2\0" + k2);
- out.push(`<tr class="g2" data-g="${encodeURIComponent("2\0" + k2)}"><td colspan="${vis.length}">${closed ? "▶" : "▼"} ${esc(S.group === "pm" ? r.model : r.quant)} <span class="dim">(${all.filter(x => gkey(x, 2) === k2).length})</span></td></tr>`);
- }
- if (S.closed.has("2\0" + k2)) continue;
- }
- }
- out.push("<tr class=\"row\" data-n=\"" + esc(r.name) + "\">" + vis.map(c => {
- const v = cell(r, c[0]);
- const lr = r.reports.length ? r.reports[r.reports.length - 1] : null;
- const tip = c[0] === "last" && lr
- ? ` title="${esc(lr.date + (lr.time ? " " + lr.time : ""))}"` : "";
- const cls = (NUM.has(c[0]) ? "num" : "") + (c[0] === "name" ? " recipe" : "") +
- (c[0] === "model" ? " model" : "");
- let body;
- if (c[0] === "stars") {
- const si = starInfo(r);
- const pe = si && latest(r, e => e.points); // both scores in the hint
- body = !si
- ? r.incomplete
- ? '<span class="dim" title="bench started, not finished">◐</span>'
- : '<span class="dim" title="no bench reports">—</span>'
- : `<span title="${si.pct}/100${pe && pe.points ? " (" + pe.points + ")" : ""}">${starGlyphs(si)}</span>`;
- } else if (c[0] === "state") {
- body = r.up != null
- ? (r.stale
- ? `<span title="ledger says running but no live container found (node down? killed serve?) — verify and close: sm121 runlock close ${esc(r.name)}"><span style="color:#d2822a">●</span><span class="dim"> stale</span></span>`
- : `<span title="up since ${fmtSince(r.up)} UTC"><span style="color:green">●</span><span class="dim"> up</span></span>`)
- : (r.fleet_up
- ? `<span title="running in the fleet — a container was found on a cluster node, but this machine has no local run ledger (served by another launcher)"><span style="color:green">●</span><span class="dim"> up</span><span class="dim" style="font-size:.8em">(fleet)</span></span>`
- : '<span class="dim" title="not running">○</span>');
- } else if (c[0] === "uptime") {
- body = r.up != null ? fmtUp(v) : '<span class="dim">—</span>';
- } else if (c[0] === "runs") {
- body = v > 0 ? esc(v)
- : '<span class="dim" title="no launch ledger on this machine (or never launched)">—</span>';
- } else if (v === "" || v == null) {
- body = '<span class="dim">—</span>';
- } else if (c[0] === "name") {
- body = medalBadge(r) + esc(v) +
- `<button class="copy" data-copy="${esc(r.name)}" title="copy recipe name">⧉</button>`;
- } else if (c[0] === "model") {
- // model id: copy it, and jump to the checkpoint's page on the Hub
- // (a bare org/name is a Hub repo id; local paths are not linked)
- const m = String(v);
- const repo = /^[\w.-]+\/[\w.-]+$/.test(m);
- body = esc(m) +
- `<button class="copy" data-copy="${esc(m)}" title="copy model id">⧉</button>` +
- (repo
- ? `<a class="hf" href="https://huggingface.co/${encodeURI(m)}" target="_blank" rel="noopener"` +
- ` title="open ${esc(m)} on Hugging Face">HF</a>`
- : "");
- } else {
- body = esc(v);
- }
- return `<td${cls ? ` class="${cls}"` : ""}${tip}>${body}</td>`;
- }).join("") + "</tr>");
- if (S.openRow === r.name) {
- // one toml section per recipe (the recipe.toml is not versioned);
- // per-run report files stay on each line
- const toml = `<a class="open" href="${auth("/r/" + encodeURIComponent(r.name) + "/recipe.toml")}" target="_blank" title="recipe.toml (highlighted)">⚙ toml</a>`;
- const lines = r.reports.map(e => {
- const fl = e.files && e.files.length ? e.files : [e.file];
- const ts = (e.perf && e.perf.length)
- ? ` <span title="${esc(e.perf.map(p => "d" + p.d + " c" + p.c + ": " + p.tg + " t/s").join(" · "))}"> ${tg(e, 0, 1) ?? "—"} / ${tg(e, 0, 2) ?? "—"} t/s</span>`
- : "";
- // run package provenance chip: image build-checksum · recipe sha
- // (⚠ = the recipe has since changed — this run's copy is pinned)
- const pr = e.provenance;
- const chip = pr && (pr.image_sum || pr.recipe_sha)
- ? `<span class="prov" title="recipe ${esc(pr.recipe_sha)}${pr.recipe_drift ? " (CHANGED SINCE THIS RUN — see the run's recipe.toml copy)" : ""} · image ${esc(pr.image || "—")} @ ${esc(pr.image_sum || "—")} (docker id ${esc(pr.image_id || "—")}) · tool ${esc(pr.tool || "—")} · launcher ${esc(pr.launcher || "—")}">⚙ ${esc((pr.image_sum || "–").slice(0, 12))}·${esc(pr.recipe_sha || "–")}${pr.recipe_drift ? "⚠" : ""}</span> `
- : "";
- return `<span>${fl.map(f =>
- `<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}`;
- }).join("<br>");
- 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("thead").addEventListener("click", e => {
- const th = e.target.closest("th"); if (!th) return;
- const k = th.dataset.k;
- if (S.sort === k) S.dir = -S.dir; else { S.sort = k; S.dir = 1; }
- render();
- });
- // copy a recipe name to the clipboard (clipboard API, textarea fallback)
- function fallbackCopy(text) {
- const ta = document.createElement("textarea");
- ta.value = text; document.body.appendChild(ta); ta.select();
- let ok = false; try { ok = document.execCommand("copy"); } catch {}
- ta.remove(); return ok;
- }
- function copyName(btn) {
- const done = ok => {
- btn.textContent = ok ? "✓" : "✗";
- btn.classList.toggle("copied", ok);
- setTimeout(() => { btn.textContent = "⧉"; btn.classList.remove("copied"); }, 900);
- };
- if (navigator.clipboard) navigator.clipboard.writeText(btn.dataset.copy).then(() => done(true), () => done(fallbackCopy(btn.dataset.copy)));
- else done(fallbackCopy(btn.dataset.copy));
- }
- document.querySelector("tbody").addEventListener("click", e => {
- const cp = e.target.closest("button.copy");
- if (cp) { copyName(cp); return; } // no row toggle under the copy button
- if (e.target.closest("a.hf")) return; // open the Hub page, don't toggle the row
- const g = e.target.closest("tr.g1,tr.g2");
- if (g) { const k = decodeURIComponent(g.dataset.g); S.closed.has(k) ? S.closed.delete(k) : S.closed.add(k); render(); return; }
- const row = e.target.closest("tr.row");
- if (row) {
- const n = row.dataset.n;
- const next = S.openRow === n ? null : n;
- S.openRow = next;
- render();
- }
- });
- document.getElementById("q").addEventListener("input", e => { S.q = e.target.value; S.page = 0; render(); });
- document.getElementById("group").addEventListener("change", e => { S.group = e.target.value; S.closed.clear(); S.page = 0; render(); });
- document.getElementById("tp").addEventListener("change", e => { S.tp = e.target.value; S.page = 0; render(); });
- document.getElementById("medal").addEventListener("change", e => { S.medal = e.target.value; S.page = 0; render(); });
- document.getElementById("uponly").addEventListener("change", e => { S.uponly = e.target.checked; S.page = 0; render(); });
- // live uptime: re-render while anything is running (the cell values are
- // computed at render time)
- setInterval(() => { if (DATA.some(r => r.up != null || r.fleet_up)) render(); }, 30000);
- document.getElementById("size").addEventListener("change", () => { S.page = 0; render(); });
- document.getElementById("prev").addEventListener("click", () => { if (S.page > 0) { S.page--; render(); } });
- document.getElementById("next").addEventListener("click", () => { S.page++; render(); });
- // ---- dashboard actions (hosted/embed mode only) --------------------------
- // The standalone `report --serve` has no job/repo API — the controls exist
- // only when the page is mounted by the web dashboard (body.embed), and they
- // call the dashboard's same-origin endpoints (the session cookie rides
- // along). State flips come back through the fleet-probe live stream.
- const EMBED = document.body.classList.contains("embed");
- const NOTES = {};
- function setNote(recipe, text) {
- NOTES[recipe] = text;
- const n = document.querySelector(`.actbar .actnote[data-r="${CSS.escape(recipe)}"]`);
- if (n) n.textContent = text;
- }
- // The expanded row carries the controls before the toml link: serve/stop
- // (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). The settings button opens the model-settings
- // dialog (per-agent client config, endpoint included on request).
- function actionBar(r) {
- if (!EMBED) return "";
- const running = r.up != null || r.fleet_up;
- 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="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="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="settings" data-r="${esc(r.name)}" title="client settings for this recipe's model — pick your agent (pi, Claude Code, Codex, Cline, generic) and copy the block">⚙ settings</button>`;
- return `<div class="actbar">${b}<span class="actnote" data-r="${esc(r.name)}">${esc(NOTES[r.name] || "")}</span></div>`;
- }
- document.addEventListener("click", async (e) => {
- const b = e.target.closest("button.act");
- if (!b) return;
- e.stopPropagation(); // no row toggle under the action button
- const kind = b.dataset.act, recipe = b.dataset.r;
- if (kind === "logs") { parent.postMessage({ sm121: "openLogs", recipe }, "*"); return; }
- if (kind === "settings") { msOpen(recipe); return; }
- const path = kind === "serve" ? "/api/serve" : kind === "stop" ? "/api/stop" : "/api/bench";
- b.disabled = true;
- try {
- const res = await fetch(path, { // "/api/serve" | "/api/stop" | "/api/bench"
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ cluster: "auto", recipe }),
- });
- const j = await res.json().catch(() => ({}));
- if (j.job != null) setNote(recipe, `${kind} job ${j.job} running — the state updates when the fleet probe sees it`);
- else setNote(recipe, `✗ ${kind}: ` + (j.error || res.status));
- } catch (err) { setNote(recipe, `✗ ${kind}: ` + err); }
- setTimeout(() => { b.disabled = false; }, 3000);
- });
- // ---- model settings dialog (⚙ settings) -----------------------------------
- // Client configuration for the recipe's served model, per agent. The server
- // derives everything from the recipe file (served name, context window,
- // reasoning/tools flags) plus the endpoint (the node currently hosting it,
- // else the recipe's cluster head node), so the dialog opens instantly.
- const MS = { recipe: null, data: null, cache: {} };
- const msdlg = document.getElementById("msdlg");
- function msRender(agentId) {
- const d = MS.data; if (!d) return;
- const withProv = document.getElementById("msprov").checked;
- const a = (d.agents || []).find(x => x.id === agentId) || (d.agents || [])[0];
- if (!a) return;
- document.getElementById("mstgt").textContent = a.target ? a.target : "";
- document.getElementById("mscode").textContent = withProv ? a.full : a.model;
- const m = d.model, e = d.endpoint;
- document.getElementById("msdep").textContent =
- `${e.api_url} · ${e.host}${e.node ? " (" + e.node + ")" : ""}${e.cluster ? " · " + e.cluster : ""}` +
- (e.running ? " · running" : " · not running");
- document.getElementById("msmeta").textContent =
- `${m.served} · ${m.engine} · context ${m.context}${m.context_known ? "" : " (assumed)"}` +
- `${m.reasoning ? " · reasoning" : ""}${m.tools ? " · tools" : ""} · repo ${m.repo}`;
- document.getElementById("msagent").value = a.id;
- }
- async function msOpen(recipe) {
- MS.recipe = recipe;
- document.getElementById("msdlg").classList.add("on");
- document.getElementById("mserr").textContent = "";
- document.getElementById("mscode").textContent = "loading…";
- document.getElementById("msdtitle").textContent = `Model settings — ${recipe}`;
- let d = MS.cache[recipe];
- if (!d) {
- try {
- const res = await fetch("/api/model-settings?recipe=" + encodeURIComponent(recipe));
- const j = await res.json().catch(() => ({}));
- if (!res.ok) throw new Error(j.error || j.message || String(res.status));
- d = MS.cache[recipe] = j;
- } catch (err) {
- document.getElementById("mscode").textContent = "";
- document.getElementById("mserr").textContent = "✗ " + err;
- return;
- }
- }
- MS.data = d;
- const sel = document.getElementById("msagent");
- sel.innerHTML = d.agents.map(a => `<option value="${esc(a.id)}">${esc(a.label)}</option>`).join("");
- document.getElementById("msprov").checked = true;
- msRender(d.agents[0] && d.agents[0].id);
- }
- document.getElementById("msagent").addEventListener("change", e => msRender(e.target.value));
- document.getElementById("msprov").addEventListener("change", () => msRender(document.getElementById("msagent").value));
- document.getElementById("msclose").addEventListener("click", () => msdlg.classList.remove("on"));
- msdlg.addEventListener("click", e => { if (e.target === msdlg) msdlg.classList.remove("on"); });
- document.addEventListener("keydown", e => { if (e.key === "Escape") msdlg.classList.remove("on"); });
- document.getElementById("mscopy").addEventListener("click", async (e) => {
- const t = document.getElementById("mscode").textContent;
- try { await navigator.clipboard.writeText(t); e.target.textContent = "copied ✓";
- setTimeout(() => { e.target.textContent = "copy"; }, 1200); } catch { /* clipboard blocked */ }
- });
- const syncBtn = document.getElementById("syncbtn");
- const repoStat = document.getElementById("repostat");
- // An empty recipe table must say WHY: no repo configured, an archived repo
- // and a repo whose clone keeps failing all render the same otherwise.
- function showRepoStat(text, bad) {
- if (!repoStat) return;
- repoStat.style.display = text ? "" : "none";
- repoStat.textContent = text;
- repoStat.style.color = bad ? "#e8833a" : "";
- repoStat.title = text;
- }
- async function loadRepoStatus() {
- try {
- const j = await (await fetch("/api/repos")).json();
- if (!j.configured) {
- // Cached clones still serve recipes even with an empty [[recipe_repos]].
- if (j.cached_roots) showRepoStat(`repos: ${j.cached_roots} cached (none configured)`, false);
- else showRepoStat("⚠ no recipe repos configured", true);
- return;
- }
- const arch = (j.repos || []).filter(r => r.archived).length;
- const bad = (j.repos || []).filter(r => !r.cached && !r.archived).length;
- if (bad) { showRepoStat(`⚠ ${bad}/${j.configured} repo(s) not synced`, true); return; }
- if (arch) { showRepoStat(`repos: ${j.configured - arch}/${j.configured} synced (${arch} archived)`, false); return; }
- showRepoStat(`repos: ${j.configured} synced`, false);
- } catch { /* hosted mode has no /api/repos (standalone report) */ }
- }
- if (EMBED) { syncBtn.style.display = ""; loadRepoStatus(); }
- syncBtn.addEventListener("click", async () => {
- syncBtn.disabled = true; syncBtn.textContent = "⟳ …";
- try {
- const res = await fetch("/api/repos/sync", { method: "POST" });
- const j = await res.json().catch(() => ({}));
- const bad = !j.ok;
- syncBtn.textContent = bad ? "⟳ ✗" : `⟳ ${j.repos} ok`;
- syncBtn.title = j.detail || (bad ? String(res.status) : "recipe repos synced");
- showRepoStat(j.detail || "", bad);
- if (!bad) loadRepoStatus();
- } catch (err) { syncBtn.textContent = "⟳ ✗"; syncBtn.title = String(err); showRepoStat(String(err), true); }
- setTimeout(() => { syncBtn.disabled = false; syncBtn.textContent = "⟳ sync"; }, 6000);
- });
- // column checkboxes
- document.getElementById("colbox").innerHTML = COLS.map(c =>
- `<label><input type="checkbox" data-k="${c[0]}" ${S.hidden.has(c[0]) ? "" : "checked"}> ${esc(c[1])}</label>`).join("");
- document.getElementById("colbox").addEventListener("change", e => {
- const k = e.target.dataset.k; if (!k) return;
- e.target.checked ? S.hidden.delete(k) : S.hidden.add(k);
- render();
- });
- render();
- </script>
- </body>
- </html>
|