返回 DeepSeek-Reasonix
serve_html.go
根目录 / cmd / e2ebench / serve_html.go
1 package main
2
3 // The dashboard page: dark flight-recorder instrument panel, all-monospace
4 // type, recessed trace screens. The renderer is incremental (keyed DOM,
5 // in-place updates) so cells animate, numbers tween, and hover survives
6 // polling; a replay engine scrubs the run by wall-clock timestamp. Win/break
7 // polarity is carried by geometry, never by the green/red pair alone.
8 const serveHTML = `<!doctype html>
9 <html lang="en"><head><meta charset="utf-8">
10 <meta name="viewport" content="width=device-width, initial-scale=1">
11 <meta name="theme-color" content="#0d0d0d">
12 <title>e2ebench · live</title>
13 <style>
14 :root {
15 color-scheme: dark;
16 --page: #0d0d0d; --panel: #1a1a19; --screen: #141413;
17 --ink: #f2f1ec; --ink-2: #c3c2b7; --muted: #898781; --faint: #55544f;
18 --hairline: rgba(255,255,255,0.08); --tick: rgba(255,255,255,0.05);
19 --grid: #2c2c2a;
20 --explore: #3987e5; --attempt: #d95926;
21 --win: #0ca30c; --break: #d03b3b;
22 --accent: #86b6ef;
23 }
24 * { box-sizing: border-box; margin: 0; }
25 html { background: var(--page); scroll-behavior: smooth; }
26 body { color: var(--ink);
27 font: 13px/1.55 ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
28 padding: 34px clamp(18px, 4.5vw, 56px) 80px; max-width: 1280px; margin: 0 auto;
29 font-variant-numeric: tabular-nums; }
30 section, .rule, header, #runseg, #deck, #cluster { animation: rise .5s ease-out backwards; }
31 #runseg { animation-delay: .05s; } #deck { animation-delay: .1s; }
32 #cluster { animation-delay: .15s; }
33 @keyframes rise { from { opacity: 0; transform: translateY(8px); } }
34
35 .rule { border: 0; border-top: 1px solid var(--hairline); margin: 40px 0 0; }
36 .eyebrow { font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
37 color: var(--muted); margin: 10px 0 14px; }
38 .eyebrow b { color: var(--ink-2); font-weight: 600; }
39 .caption { color: var(--muted); font-size: 12px; max-width: 72ch; margin: -6px 0 18px; }
40 .caption b { color: var(--ink-2); font-weight: 600; }
41
42 header { display: flex; align-items: baseline; }
43 .wordmark { font-size: 13px; font-weight: 700; letter-spacing: 0.08em; }
44 .wordmark em { font-style: normal; color: var(--accent); }
45 .wordmark span { color: var(--muted); font-weight: 400; }
46 #clock { margin-left: auto; color: var(--muted); font-size: 12px; }
47
48 #runseg { display: flex; gap: 3px; margin-top: 22px; }
49 #runseg i { flex: 1 1 0; max-width: 22px; height: 14px; border-radius: 2px;
50 background: var(--grid); cursor: pointer;
51 transition: background .3s, box-shadow .3s, transform .12s; }
52 #runseg i:hover { transform: translateY(-2px); }
53 #runseg i.done { background: var(--accent); opacity: 0.85; }
54 #runseg i.live { background: var(--attempt);
55 box-shadow: 0 0 10px color-mix(in srgb, var(--attempt) 55%, transparent);
56 animation: pulse 1.1s ease-in-out infinite; }
57
58 #deck { display: flex; align-items: center; gap: 14px; margin-top: 16px;
59 padding: 10px 14px; background: var(--panel); border: 1px solid var(--hairline);
60 border-radius: 10px; }
61 #deck button, #deck select { font: inherit; color: var(--ink-2); background: transparent;
62 border: 1px solid var(--hairline); border-radius: 6px; padding: 4px 12px;
63 cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
64 #deck button:hover { color: var(--ink); border-color: var(--muted); }
65 #deck button.hot { color: var(--attempt); border-color: var(--attempt); }
66 #ptime { color: var(--muted); font-size: 12px; min-width: 15ch; }
67 #scrub { flex: 1 1 auto; -webkit-appearance: none; appearance: none; height: 4px;
68 background: var(--grid); border-radius: 2px; cursor: pointer; }
69 #scrub::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px;
70 border-radius: 50%; background: var(--ink-2); border: 2px solid var(--page);
71 transition: transform .12s; }
72 #scrub::-webkit-slider-thumb:hover { transform: scale(1.25); }
73
74 #cluster { display: flex; gap: 48px; margin-top: 20px; flex-wrap: wrap; }
75 .gauge b { display: block; font-size: 30px; font-weight: 300; letter-spacing: -0.01em;
76 line-height: 1.1; }
77 .gauge b small { font-size: 15px; color: var(--muted); font-weight: 300; }
78 .gauge span { display: block; font-size: 10px; letter-spacing: 0.2em;
79 text-transform: uppercase; color: var(--muted); margin-top: 5px; }
80
81 #now { display: flex; gap: 36px; align-items: stretch; min-height: 118px; }
82 #now .main { flex: 1 1 auto; min-width: 0; }
83 #now .head { display: flex; align-items: baseline; gap: 16px; }
84 #now .name { font-size: 17px; font-weight: 700; letter-spacing: 0.02em; cursor: pointer; }
85 #now .name:hover { color: var(--accent); }
86 #now .rec { color: var(--attempt); font-size: 11px; letter-spacing: 0.2em;
87 animation: pulse 1.1s ease-in-out infinite; }
88 #now .doing { color: var(--ink-2); font-size: 12px; transition: color .3s; }
89 #now .side { display: flex; flex-direction: column; gap: 14px; padding-left: 32px;
90 border-left: 1px solid var(--hairline); }
91 #now .side .gauge b { font-size: 22px; }
92 #now.idle { color: var(--muted); font-size: 12px; display: block; min-height: 0; }
93
94 .screen { background: var(--screen); border: 1px solid var(--hairline);
95 border-radius: 8px; padding: 0 12px; margin-top: 14px; position: relative;
96 background-image: repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px 75px);
97 background-origin: content-box; transition: border-color .3s, box-shadow .3s; }
98 .trace { display: flex; align-items: center; height: 56px; position: relative; }
99 .trace::before { content: ""; position: absolute; left: 0; right: 0; top: 50%;
100 border-top: 1px solid var(--grid); }
101 .cells { display: flex; align-items: center; height: 100%; overflow-x: auto; flex: 1; }
102 .cell { flex: 0 0 12px; height: 12px; border-radius: 2px; margin-right: 3px;
103 background: var(--grid); align-self: center; position: relative;
104 transition: transform .12s; }
105 .cell:hover { transform: scale(1.4); z-index: 2; }
106 .cell.e { background: var(--explore); }
107 .cell.a { background: var(--attempt); }
108 .cell.o { background: var(--win); height: 44px; align-self: flex-start;
109 border-radius: 3px 3px 0 0;
110 box-shadow: 0 0 12px color-mix(in srgb, var(--win) 50%, transparent); }
111 .cell.r { background: var(--break); height: 44px; align-self: flex-end;
112 border-radius: 0 0 3px 3px;
113 box-shadow: 0 0 12px color-mix(in srgb, var(--break) 50%, transparent); }
114 .cell.new { animation: pop .3s cubic-bezier(.2,.9,.3,1.35); }
115 .cell.o.new, .cell.r.new { animation: pop .3s cubic-bezier(.2,.9,.3,1.35),
116 flare .9s ease-out; }
117 @keyframes pop { from { transform: scale(.2); opacity: 0; } }
118 @keyframes flare { 0% { box-shadow: 0 0 2px 8px color-mix(in srgb, currentColor 40%, transparent); }
119 100% { } }
120 .cell.cursor { background: transparent; border: 1px dashed var(--muted);
121 animation: pulse 1.1s ease-in-out infinite; flex: none; }
122 .mini .cell { flex-basis: 7px; height: 7px; margin-right: 2px; border-radius: 1.5px; }
123 .mini .cell.o, .mini .cell.r { height: 21px; box-shadow: none; }
124 .mini.trace { height: 30px; }
125
126 #kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
127 .kpi { padding: 4px 28px 6px 0; }
128 .kpi + .kpi { border-left: 1px solid var(--hairline); padding-left: 28px; }
129 .kpi b { font-size: 38px; font-weight: 300; line-height: 1.05; letter-spacing: -0.01em;
130 transition: color .3s; }
131 .kpi b small { font-size: 14px; color: var(--muted); font-weight: 400; }
132 .kpi.win b { color: var(--win); text-shadow: 0 0 18px color-mix(in srgb, var(--win) 35%, transparent); }
133 .kpi.break b { color: var(--break); }
134 .kpi.zero b { color: var(--faint); text-shadow: none; }
135 .kpi .label { font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;
136 color: var(--ink-2); margin-top: 8px; }
137 .kpi .why { color: var(--muted); font-size: 11.5px; margin-top: 6px; line-height: 1.5;
138 font-family: system-ui, sans-serif; }
139 .kpi .bar { height: 3px; background: var(--grid); margin-top: 12px; border-radius: 2px;
140 overflow: hidden; }
141 .kpi .bar i { display: block; height: 100%; background: var(--attempt);
142 transition: width .6s ease; }
143
144 #legend { display: flex; flex-wrap: wrap; gap: 10px 30px; }
145 #legend span { display: inline-flex; align-items: center; gap: 9px; font-size: 12px;
146 color: var(--ink-2); }
147 #legend i { width: 10px; height: 10px; border-radius: 2px; flex: none; }
148 #legend .up, #legend .down { height: 26px; }
149 #legend .up i { height: 20px; border-radius: 3px 3px 0 0; align-self: flex-start; }
150 #legend .down i { height: 20px; border-radius: 0 0 3px 3px; align-self: flex-end; }
151
152 #wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
153 gap: 26px 30px; }
154 .task { transition: opacity .4s; }
155 .task .top { display: flex; align-items: baseline; gap: 8px; }
156 .task .name { font-size: 12px; font-weight: 700; letter-spacing: 0.02em;
157 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
158 .task .badges { margin-left: auto; display: flex; gap: 8px; font-size: 11px; flex: none; }
159 .badge.win { color: var(--win); } .badge.break { color: var(--break); }
160 .badge.guard { color: var(--attempt); }
161 .task .screen { margin-top: 8px; padding: 0 8px;
162 background-image: repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px 45px); }
163 .task:hover .screen { border-color: var(--muted); }
164 .task.live .screen { border-color: var(--attempt);
165 box-shadow: 0 0 14px color-mix(in srgb, var(--attempt) 25%, transparent); }
166 .task.flash .screen { animation: flashring 1.2s ease-out; }
167 @keyframes flashring { 0% { border-color: var(--accent);
168 box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 45%, transparent); } }
169 .task .meta { color: var(--muted); font-size: 11px; margin-top: 7px; }
170 .task .meta b { color: var(--ink-2); font-weight: 600; }
171 .task.queued { opacity: .55; }
172 .task.queued .name { color: var(--faint); font-weight: 400; }
173 .task.queued .screen { border-style: dashed; background-image: none; }
174 .qnote { color: var(--faint); font-size: 11px; letter-spacing: 0.18em;
175 text-transform: uppercase; margin: auto; }
176
177 #tip { position: fixed; z-index: 10; pointer-events: none; background: var(--panel);
178 border: 1px solid var(--hairline); border-radius: 8px; padding: 8px 12px;
179 font-size: 12px; color: var(--ink-2); max-width: 320px; line-height: 1.5;
180 box-shadow: 0 8px 30px rgba(0,0,0,.5); opacity: 0; transform: translateY(4px) scale(.97);
181 transition: opacity .15s, transform .15s; }
182 #tip.on { opacity: 1; transform: none; }
183 #tip b { color: var(--ink); }
184
185 @keyframes pulse { 50% { opacity: 0.3; } }
186 @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; }
187 html { scroll-behavior: auto; } }
188 </style></head>
189 <body>
190 <header>
191 <div class="wordmark"><em>reasonix</em> e2ebench <span>· flight recorder</span></div>
192 <span id="clock"></span>
193 </header>
194 <div id="runseg"></div>
195 <div id="deck">
196 <button id="play" title="replay the run from its trajectories">▶ replay</button>
197 <input id="scrub" type="range" min="0" max="1000" value="1000">
198 <span id="ptime"></span>
199 <select id="speed"><option value="30">30×</option><option value="120" selected>120×</option><option value="600">600×</option></select>
200 <button id="golive">● live</button>
201 </div>
202 <div id="cluster"></div>
203
204 <hr class="rule"><div class="eyebrow">now running</div>
205 <div id="now" class="idle">waiting for the first task…</div>
206
207 <hr class="rule"><div class="eyebrow">shadow scorer verdicts</div>
208 <p class="caption">A second scorer watches every tool round and only credits <b>verified</b> progress — a recognized check going from fail to pass. Everything else is motion.</p>
209 <section id="kpis"></section>
210
211 <hr class="rule"><div class="eyebrow">how to read a trace</div>
212 <div id="legend">
213 <span><i style="background:var(--grid)"></i>quiet — nothing new</span>
214 <span><i style="background:var(--explore)"></i>exploring — new files or commands</span>
215 <span><i style="background:var(--attempt)"></i>working — edits or checks, unverified</span>
216 <span class="up"><i style="background:var(--win)"></i>verified win — failing check turned green</span>
217 <span class="down"><i style="background:var(--break)"></i>broke something — passing check failed</span>
218 </div>
219
220 <hr class="rule"><div class="eyebrow" id="walllabel">all tasks</div>
221 <section id="wall"></section>
222 <div id="tip"></div>
223
224 <script>
225 "use strict";
226 var $ = function (id) { return document.getElementById(id); };
227 var LIVE_MS = 15000;
228 var state = null, fetchedAt = 0, suite = [], byID = {};
229 var segs = {}, cards = {};
230 var R = { on: false, playing: false, t: 0, speed: 120, t0: 0, t1: 0, last: 0 };
231 var nowShown = null;
232
233 function fmtDur(ms) {
234 if (!ms || ms < 0) return "0s";
235 if (ms < 90000) return (ms / 1000).toFixed(0) + "s";
236 return Math.floor(ms / 60000) + "m" + String(Math.round((ms % 60000) / 1000)).padStart(2, "0") + "s";
237 }
238 function fmtOff(ms) {
239 var s = Math.max(0, Math.round(ms / 1000));
240 return "T+" + String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
241 }
242 function catOf(r) { return r.r ? "r" : r.o ? "o" : (r.c || r.v) ? "a" : r.e ? "e" : ""; }
243 function nameOf(r) {
244 return r.r ? "broke something" : r.o ? "verified win" :
245 (r.c || r.v) ? "working" : r.e ? "exploring" : "quiet";
246 }
247 function tipOf(i, r) {
248 var p = [];
249 if (r.o) p.push(r.o + " check(s) turned green");
250 if (r.r) p.push(r.r + " check(s) broke");
251 if (r.v) p.push(r.v + " check runs");
252 if (r.c) p.push(r.c + " edits");
253 if (r.e) p.push(r.e + " new files/commands");
254 if (!p.length) p.push("repeat of earlier work");
255 return "<b>round " + (i + 1) + " — " + nameOf(r) + "</b><br>" + p.join(", ");
256 }
257 function cellNode(i, r, animate) {
258 var d = document.createElement("div");
259 d.className = "cell " + catOf(r) + (animate ? " new" : "");
260 d.dataset.tip = tipOf(i, r);
261 return d;
262 }
263 function countVis(rounds, cut) {
264 var n = 0;
265 while (n < rounds.length && (rounds[n].t || 0) <= cut) n++;
266 return n;
267 }
268 function goTo(id) {
269 var c = cards[id];
270 if (!c) return;
271 c.el.scrollIntoView({ behavior: "smooth", block: "center" });
272 c.el.classList.remove("flash");
273 void c.el.offsetWidth;
274 c.el.classList.add("flash");
275 }
276
277 function ensure() {
278 suite.forEach(function (id) {
279 if (!segs[id]) {
280 var i = document.createElement("i");
281 i.dataset.tip = "<b>" + id + "</b><br>click to jump to its card";
282 i.addEventListener("click", function () { goTo(id); });
283 $("runseg").appendChild(i);
284 segs[id] = i;
285 }
286 if (!cards[id]) {
287 var el = document.createElement("div");
288 el.className = "task queued";
289 el.innerHTML = '<div class="top"><span class="name">' + id +
290 '</span><div class="badges"></div></div>' +
291 '<div class="screen"><div class="trace mini"><div class="cells"></div>' +
292 '<span class="qnote">queued</span></div></div><div class="meta"></div>';
293 $("wall").appendChild(el);
294 cards[id] = { el: el, cells: el.querySelector(".cells"), badges: el.querySelector(".badges"),
295 meta: el.querySelector(".meta"), qnote: el.querySelector(".qnote"), built: 0, queued: true };
296 }
297 });
298 }
299
300 function updateCard(id, t, cut, liveNow) {
301 var c = cards[id];
302 var rounds = t ? t.rounds : [];
303 var vis = t ? (cut === Infinity ? rounds.length : countVis(rounds, cut)) : 0;
304 if (!t || vis === 0) {
305 c.el.classList.add("queued"); c.el.classList.remove("live");
306 c.qnote.style.display = ""; c.cells.innerHTML = ""; c.built = 0;
307 c.badges.innerHTML = ""; c.meta.textContent = "";
308 return { o: 0, r: 0 };
309 }
310 c.el.classList.remove("queued");
311 c.qnote.style.display = "none";
312 if (vis < c.built) { c.cells.innerHTML = ""; c.built = 0; }
313 var animate = vis - c.built <= 12;
314 for (var i = c.built; i < vis; i++) c.cells.appendChild(cellNode(i, rounds[i], animate));
315 c.built = vis;
316 var o = 0, rg = 0;
317 for (var j = 0; j < vis; j++) { o += rounds[j].o || 0; rg += rounds[j].r || 0; }
318 c.badges.innerHTML =
319 (o ? '<span class="badge win" data-tip="verified wins">↑' + o + "</span>" : "") +
320 (rg ? '<span class="badge break" data-tip="regressions">↓' + rg + "</span>" : "") +
321 (t.no_progress ? '<span class="badge guard" data-tip="progress guard interventions">g' + t.no_progress + "</span>" : "");
322 var span = cut === Infinity ? t.span_ms
323 : Math.max(0, (rounds[vis - 1].t || 0) - ((rounds[0].t || 0)));
324 c.meta.innerHTML = "<b>" + vis + "</b> rounds · " + fmtDur(span) +
325 (liveNow ? ' · <span class="badge guard">running</span>' : "");
326 c.el.classList.toggle("live", liveNow);
327 return { o: o, r: rg };
328 }
329
330 function updateNow(liveIDs, cut) {
331 var el = $("now");
332 if (!liveIDs.length) {
333 nowShown = null;
334 el.className = "idle";
335 var complete = suite.length > 0 && suite.every(function (id) { return byID[id]; });
336 el.textContent = R.on ? "…" :
337 complete ? "run complete — press ▶ replay to watch it back" :
338 "between tasks — grading the last one or starting the next…";
339 return;
340 }
341 var id = liveIDs[liveIDs.length - 1];
342 var t = byID[id];
343 var rounds = t.rounds;
344 var vis = cut === Infinity ? rounds.length : countVis(rounds, cut);
345 if (nowShown !== id) {
346 nowShown = id;
347 el.className = "";
348 el.innerHTML = '<div class="main"><div class="head"><span class="rec">● rec</span>' +
349 '<span class="name" data-goto="' + id + '">' + id + "</span>" +
350 '<span class="doing"></span></div>' +
351 '<div class="screen"><div class="trace"><div class="cells"></div>' +
352 '<div class="cell cursor" data-tip="next round"></div></div></div></div>' +
353 '<div class="side"><div class="gauge"><b class="g-round"></b><span>round</span></div>' +
354 '<div class="gauge"><b class="g-elapsed"></b><span>elapsed</span></div>' +
355 '<div class="gauge"><b class="g-tools"></b><span>in tools</span></div></div>';
356 el.querySelector(".name").addEventListener("click", function () { goTo(id); });
357 el._built = 0;
358 }
359 var cells = el.querySelector(".cells");
360 if (vis < el._built) { cells.innerHTML = ""; el._built = 0; }
361 var animate = vis - el._built <= 12;
362 for (var i = el._built; i < vis; i++) cells.appendChild(cellNode(i, rounds[i], animate));
363 el._built = vis;
364 var last = rounds[vis - 1] || {};
365 el.querySelector(".doing").textContent = nameOf(last) + "…";
366 el.querySelector(".g-round").textContent = vis;
367 var elapsed = cut === Infinity
368 ? t.span_ms + (state ? Date.now() - fetchedAt : 0)
369 : Math.max(0, (last.t || 0) - (rounds[0] ? rounds[0].t || 0 : 0));
370 el.querySelector(".g-elapsed").textContent = fmtDur(elapsed);
371 el.querySelector(".g-tools").textContent = fmtDur(t.tool_ms);
372 }
373
374 function gauge(v, label) {
375 return '<div class="gauge"><b>' + v + "</b><span>" + label + "</span></div>";
376 }
377 function kpiHTML(cls, value, unit, label, why, ratio) {
378 return '<div class="kpi ' + cls + '"><b>' + value + (unit ? "<small> " + unit + "</small>" : "") +
379 '</b><div class="label">' + label + '</div><div class="why">' + why + "</div>" +
380 (ratio !== undefined ? '<div class="bar"><i style="width:' + Math.min(100, ratio) + '%"></i></div>' : "") +
381 "</div>";
382 }
383
384 function applyAll() {
385 if (!state) return;
386 var cut = R.on ? R.t : Infinity;
387 ensure();
388 var doneN = 0, liveIDs = [], obj = 0, reg = 0;
389 suite.forEach(function (id) {
390 var t = byID[id];
391 var liveNow = false, isDone = false;
392 if (t) {
393 if (R.on) {
394 var vis = countVis(t.rounds, cut);
395 liveNow = vis > 0 && vis < t.rounds.length;
396 isDone = t.rounds.length > 0 && vis >= t.rounds.length;
397 } else {
398 liveNow = t.ago_ms >= 0 && t.ago_ms < LIVE_MS;
399 isDone = !liveNow;
400 }
401 }
402 if (isDone) doneN++;
403 if (liveNow) liveIDs.push(id);
404 segs[id].className = liveNow ? "live" : isDone ? "done" : "";
405 var c = updateCard(id, t, cut, liveNow);
406 obj += c.o; reg += c.r;
407 });
408
409 var spans = [];
410 suite.forEach(function (id) {
411 var t = byID[id];
412 if (t && t.span_ms && (!R.on || countVis(t.rounds, cut) >= t.rounds.length)) spans.push(t.span_ms);
413 });
414 var avg = spans.length ? spans.reduce(function (a, b) { return a + b; }, 0) / spans.length : 0;
415 var left = suite.length - doneN - liveIDs.length;
416 $("cluster").innerHTML =
417 gauge(doneN + "<small> / " + suite.length + "</small>", "tasks recorded") +
418 (avg ? gauge(fmtDur(avg), "per task") : "") +
419 (R.on ? gauge(fmtOff(R.t - R.t0), "playhead") :
420 left + liveIDs.length > 0 && avg
421 ? gauge("~" + fmtDur(avg * (left + liveIDs.length * 0.5)), "time left")
422 : gauge("done", "status")) +
423 (liveIDs.length ? gauge(liveIDs.length, R.on ? "on screen" : "running now") : "") +
424 (left > 0 ? gauge(left, "queued") : "");
425
426 updateNow(liveIDs, cut);
427
428 var atEnd = !R.on || R.t >= R.t1;
429 var fp = 0, pr = 0, stall = 0, regressed = 0;
430 state.tasks.forEach(function (t) {
431 var o = t.outcome; if (!o) return;
432 fp += o.false_progress_rounds || 0; pr += o.progress_rounds || 0;
433 stall = Math.max(stall, o.solution_stall_max || 0);
434 if (o.regressed_from_best) regressed++;
435 });
436 $("kpis").innerHTML =
437 kpiHTML(obj ? "win" : "zero", obj, "", "verified wins",
438 "Rounds where a failing check turned green — the only progress the shadow scorer trusts.") +
439 kpiHTML(reg ? "break" : "zero", reg + (regressed && atEnd ? " / " + regressed : ""), "",
440 "regressions" + (regressed && atEnd ? " / peaked early" : ""),
441 "A previously passing check broke" + (regressed && atEnd ? "; some runs ended below their best state." : ".")) +
442 kpiHTML("", atEnd && pr ? Math.round(100 * fp / pr) + "%" : "–", atEnd && pr ? fp + " of " + pr : "",
443 "false progress",
444 "Rounds the current scorer counted as progress that never turned into a verified win.",
445 atEnd && pr ? 100 * fp / pr : 0) +
446 kpiHTML("", atEnd && stall ? stall : "–", atEnd && stall ? "rounds" : "", "longest stall",
447 "Most consecutive rounds of work with no verified win — where an agent burns time.");
448
449 $("walllabel").innerHTML = "all tasks · <b>" + suite.length + "</b>";
450 $("scrub").value = R.on && R.t1 > R.t0 ? Math.round(1000 * (R.t - R.t0) / (R.t1 - R.t0)) : 1000;
451 $("ptime").textContent = R.on
452 ? fmtOff(R.t - R.t0) + " / " + fmtOff(R.t1 - R.t0)
453 : "live tail";
454 $("play").textContent = R.playing ? "⏸ pause" : "▶ replay";
455 $("play").classList.toggle("hot", R.playing);
456 }
457
458 function bounds() {
459 var t0 = Infinity, t1 = 0;
460 state.tasks.forEach(function (t) {
461 if (t.rounds.length) {
462 t0 = Math.min(t0, t.rounds[0].t || Infinity);
463 t1 = Math.max(t1, t.rounds[t.rounds.length - 1].t || 0);
464 }
465 });
466 R.t0 = t0 === Infinity ? 0 : t0;
467 R.t1 = t1;
468 }
469
470 function frame(now) {
471 if (!R.playing) return;
472 var dt = R.last ? now - R.last : 16;
473 R.last = now;
474 R.t += dt * R.speed;
475 if (R.t >= R.t1) { R.t = R.t1; R.playing = false; }
476 applyAll();
477 if (R.playing) requestAnimationFrame(frame);
478 }
479 $("play").addEventListener("click", function () {
480 if (!state) return;
481 bounds();
482 if (R.playing) { R.playing = false; applyAll(); return; }
483 if (!R.on || R.t >= R.t1) { R.on = true; R.t = R.t0; resetBuilt(); }
484 R.playing = true; R.last = 0;
485 requestAnimationFrame(frame);
486 });
487 $("golive").addEventListener("click", function () {
488 R.on = false; R.playing = false; resetBuilt(); applyAll();
489 });
490 $("scrub").addEventListener("input", function () {
491 if (!state) return;
492 bounds();
493 R.on = true;
494 R.t = R.t0 + (R.t1 - R.t0) * (this.value / 1000);
495 applyAll();
496 });
497 $("speed").addEventListener("change", function () { R.speed = +this.value; });
498 function resetBuilt() {
499 Object.keys(cards).forEach(function (id) { cards[id].cells.innerHTML = ""; cards[id].built = 0; });
500 nowShown = null;
501 }
502
503 var tip = $("tip");
504 document.addEventListener("mouseover", function (e) {
505 var n = e.target.closest ? e.target.closest("[data-tip]") : null;
506 if (n) { tip.innerHTML = n.dataset.tip; tip.classList.add("on"); }
507 else tip.classList.remove("on");
508 });
509 document.addEventListener("mousemove", function (e) {
510 if (!tip.classList.contains("on")) return;
511 var x = Math.min(e.clientX + 14, window.innerWidth - tip.offsetWidth - 10);
512 var y = e.clientY + 16;
513 if (y + tip.offsetHeight > window.innerHeight - 8) y = e.clientY - tip.offsetHeight - 10;
514 tip.style.left = x + "px"; tip.style.top = y + "px";
515 });
516
517 setInterval(function () {
518 if (state && !R.on) applyAll();
519 }, 1000);
520
521 function tick() {
522 fetch("/api/state").then(function (r) { return r.json(); }).then(function (st) {
523 state = st; fetchedAt = Date.now();
524 byID = {};
525 st.tasks.forEach(function (t) { byID[t.id] = t; });
526 suite = st.suite && st.suite.length ? st.suite : st.tasks.map(function (t) { return t.id; });
527 $("clock").textContent = new Date(st.now).toLocaleTimeString();
528 if (!R.on) applyAll();
529 if (location.hash === "#replay" && !R.on) { location.hash = ""; $("play").click(); }
530 }).catch(function () {
531 $("clock").textContent = "poll failed — is the server still up?";
532 });
533 }
534 tick();
535 setInterval(tick, 2000);
536 </script>
537 </body></html>
538 `
539
539 lines GO