bareloop 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/bareloop.context.md +52 -13
- package/package.json +2 -2
- package/src/authorflow.js +26 -11
- package/src/authoring.js +1 -1
- package/src/authorjob.js +27 -15
- package/src/authorreadout.js +7 -6
- package/src/declaredclose.js +4 -1
- package/src/draftspend.js +20 -2
- package/src/facts.js +54 -9
- package/src/factwords.js +58 -6
- package/src/inputlook.js +38 -3
- package/src/job.js +1 -1
- package/src/kinds.js +5 -0
- package/src/pagedropwords.js +64 -0
- package/src/pagemap.js +264 -0
- package/src/pagerules.js +319 -0
- package/src/panel/authorroutes.js +34 -30
- package/src/panel/authorsession.js +29 -5
- package/src/panel/index.html +465 -65
- package/src/panel/jobplan.js +4 -2
- package/src/panel/runroutes.js +46 -22
- package/src/panel/server.js +791 -28
- package/src/panel/status.js +33 -0
- package/src/panelmoney.js +29 -0
- package/src/planrun.js +53 -4
- package/src/promptregisters.js +4 -0
- package/src/replay.js +118 -7
- package/src/rubricstep.js +123 -11
- package/src/runlist.js +2 -1
- package/src/schedulewords.js +6 -3
- package/src/source.js +18 -0
- package/src/thinking.js +63 -2
- package/src/userrun.js +28 -14
- package/types/authorflow.d.ts +42 -6
- package/types/authorjob.d.ts +10 -2
- package/types/declaredclose.d.ts +2 -0
- package/types/draftspend.d.ts +27 -1
- package/types/facts.d.ts +15 -0
- package/types/factwords.d.ts +26 -3
- package/types/inputlook.d.ts +15 -0
- package/types/kinds.d.ts +5 -0
- package/types/pagedropwords.d.ts +26 -0
- package/types/pagemap.d.ts +154 -0
- package/types/pagerules.d.ts +86 -0
- package/types/panel/authorroutes.d.ts +17 -14
- package/types/panel/runroutes.d.ts +2 -2
- package/types/panel/server.d.ts +155 -2
- package/types/panel/status.d.ts +51 -0
- package/types/panelmoney.d.ts +5 -0
- package/types/replay.d.ts +21 -1
- package/types/rubricstep.d.ts +57 -1
- package/types/runlist.d.ts +4 -1
- package/types/schedulewords.d.ts +4 -2
- package/types/source.d.ts +11 -0
- package/types/thinking.d.ts +39 -0
- package/types/userrun.d.ts +4 -0
package/src/panel/index.html
CHANGED
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
are already both on screen there, nothing to jump back to). */
|
|
273
273
|
.back-to-list-link{display:none;font-size:12px;color:var(--accent);text-decoration:underline;background:none;border:none;padding:0;cursor:pointer;}
|
|
274
274
|
@media (max-width: 899px){ .back-to-list-link{display:inline-block;} }
|
|
275
|
-
.rp-tabrow{display:flex;border-bottom:1px solid var(--border-strong);background:var(--panel2);counter-reset:rtabnum;}
|
|
275
|
+
.rp-tabrow{display:flex;flex-wrap:wrap;border-bottom:1px solid var(--border-strong);background:var(--panel2);counter-reset:rtabnum;}
|
|
276
276
|
.rp-tabrow button{padding:10px 14px;min-height:44px;background:transparent;border:none;border-right:1px dotted var(--border-strong);font-size:13px;color:var(--text-dim);counter-increment:rtabnum;}
|
|
277
277
|
.rp-tabrow button::before{content:"[" counter(rtabnum) "] ";color:var(--text-faint);font-weight:700;}
|
|
278
278
|
.tabrow button[aria-selected="true"],.rp-tabrow button[aria-selected="true"]{background:var(--panel);color:var(--accent);font-weight:700;box-shadow:inset 0 -2px 0 var(--accent);}
|
|
@@ -394,6 +394,27 @@
|
|
|
394
394
|
one) covers every table inside that wrapper, Flat's included. */
|
|
395
395
|
.audit-table-scroll table th{position:sticky;top:0;z-index:1;}
|
|
396
396
|
.attempt-check{margin-top:4px;font-size:12px;}
|
|
397
|
+
/* PRD v1.107 Part 6: the Result tab. Boxes are the Job tab's own (.job-card, .field, .ro-value, .field-row); text wraps, nothing scrolls the page. */
|
|
398
|
+
#result-body{min-width:0;overflow-wrap:anywhere;}
|
|
399
|
+
.rs-ok{color:var(--green);font-weight:700;}
|
|
400
|
+
#result-body mark{background:var(--rl-accent-bg-strong);color:var(--text);border-radius:2px;padding:0 1px;}
|
|
401
|
+
#result-door-card{border-color:var(--amber);}
|
|
402
|
+
#result-door-card .st-word{color:var(--amber);}
|
|
403
|
+
#result-door-card textarea{width:100%;min-height:60px;margin:6px 0;background:var(--bg);color:var(--text);border:1px solid var(--text-dim);font:inherit;padding:6px;}
|
|
404
|
+
#result-body details>summary{cursor:pointer;color:var(--text-dim);min-height:28px;}
|
|
405
|
+
.rs-link{background:none;border:none;padding:0;min-height:44px;color:var(--accent);text-decoration:underline;font:inherit;cursor:pointer;}
|
|
406
|
+
.rs-warn{color:var(--amber);overflow-wrap:anywhere;}
|
|
407
|
+
#result-door-card .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
408
|
+
/* PRD v1.106 Part 5 C: the Audit step fold (tries, their numbered checks, the Round table). Every line wraps; nothing forces a page-wide scroll. */
|
|
409
|
+
.actor-mark{color:var(--text-faint);font-size:12px;margin-left:6px;white-space:nowrap;}
|
|
410
|
+
.step-tally{font-size:12px;color:var(--text-dim);overflow-wrap:anywhere;margin:2px 0 6px;}
|
|
411
|
+
.try-line{margin:10px 0 2px;overflow-wrap:anywhere;font-weight:700;}
|
|
412
|
+
.try-check,.try-judged{margin:2px 0 2px 12px;font-size:13px;overflow-wrap:anywhere;}
|
|
413
|
+
.try-judged{font-size:12px;color:var(--text-dim);}
|
|
414
|
+
.try-reasons{margin:2px 0 4px 12px;font-size:12px;color:var(--text-dim);}
|
|
415
|
+
.try-reasons summary{cursor:pointer;}
|
|
416
|
+
.try-reasons div{margin:2px 0 2px 8px;overflow-wrap:anywhere;}
|
|
417
|
+
.step-rounds-note{font-size:12px;color:var(--text-faint);margin:8px 0 2px;overflow-wrap:anywhere;}
|
|
397
418
|
.rounds-page-info{margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
398
419
|
|
|
399
420
|
/* build item C: Audit tab grouped-by-part rows */
|
|
@@ -670,6 +691,7 @@
|
|
|
670
691
|
.topbar{flex-direction:column;align-items:flex-start;}
|
|
671
692
|
.topbar-right{width:100%;justify-content:space-between;}
|
|
672
693
|
.btn.small{height:44px;min-height:44px;padding:0 12px;}
|
|
694
|
+
.rs-link{min-height:44px;}
|
|
673
695
|
.chip{min-height:44px;}
|
|
674
696
|
.chip.fchip{min-height:40px;min-width:40px;}
|
|
675
697
|
.rp-header{margin:8px 8px 0;}
|
|
@@ -844,8 +866,9 @@
|
|
|
844
866
|
</div>
|
|
845
867
|
<div id="rl-top" data-testid="rl-top"></div>
|
|
846
868
|
<div class="rl-dash"></div>
|
|
847
|
-
<div class="rl-faint" data-testid="rl-filter-note">
|
|
869
|
+
<div class="rl-faint" data-testid="rl-filter-note">search also finds Scheduled; ◷ shows only Scheduled</div>
|
|
848
870
|
<div id="runs-filterbar-anchor"></div>
|
|
871
|
+
<div id="rl-sched" data-testid="rl-sched"></div>
|
|
849
872
|
<div id="wf-list" data-testid="wf-list"></div>
|
|
850
873
|
<div id="import-list" data-testid="import-list"></div>
|
|
851
874
|
</section>
|
|
@@ -867,6 +890,7 @@
|
|
|
867
890
|
<button role="tab" id="tab-run" aria-controls="panel-run" aria-selected="true" data-testid="right-tab-run">Run</button>
|
|
868
891
|
<button role="tab" id="tab-audit" aria-controls="panel-audit" aria-selected="false" data-testid="right-tab-audit">Audit / logs</button>
|
|
869
892
|
<button role="tab" id="tab-details" aria-controls="panel-details" aria-selected="false" data-testid="right-tab-details">Job</button>
|
|
893
|
+
<button role="tab" id="tab-result" aria-controls="panel-result" aria-selected="false" data-testid="right-tab-result" hidden>Result</button>
|
|
870
894
|
</div>
|
|
871
895
|
|
|
872
896
|
<div class="pane-body" id="right-pane-body">
|
|
@@ -880,6 +904,7 @@
|
|
|
880
904
|
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
881
905
|
</div>
|
|
882
906
|
<div class="run-actions" id="run-actions" data-testid="run-actions" hidden></div>
|
|
907
|
+
<div class="hint" id="result-pointer" data-testid="result-pointer" hidden></div>
|
|
883
908
|
<div class="counters" id="run-counters" data-testid="live-counters"></div>
|
|
884
909
|
<div class="summary-box" id="run-summary" data-testid="run-summary"></div>
|
|
885
910
|
<div class="map-box" data-testid="step-map" id="map-box"><div id="map-mount"></div></div>
|
|
@@ -905,6 +930,7 @@
|
|
|
905
930
|
<div id="audit-flat-wrap" hidden>
|
|
906
931
|
<div id="audit-empty" class="hint" data-testid="audit-empty" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
907
932
|
<div id="audit-content" data-testid="audit-content" hidden>
|
|
933
|
+
<div class="hint" data-testid="audit-flat-roundnote" style="overflow-wrap:anywhere;margin-bottom:6px;">Round numbers here count the whole run. In the Grouped view each step counts its own rounds from 1.</div>
|
|
908
934
|
<div class="audit-table-scroll">
|
|
909
935
|
<table data-testid="audit-table">
|
|
910
936
|
<thead><tr><th>Round</th><th>Step</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead>
|
|
@@ -925,7 +951,10 @@
|
|
|
925
951
|
<div class="job-card" id="job-card-readonly" hidden data-testid="job-card-readonly">
|
|
926
952
|
<h4 id="details-title">Job card — signed</h4>
|
|
927
953
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
928
|
-
<div class="field
|
|
954
|
+
<div class="field-row">
|
|
955
|
+
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
956
|
+
<div class="field" id="details-thinking-field"><label>Thinking</label><div class="ro-value" id="details-thinking" data-testid="details-thinking">—</div></div>
|
|
957
|
+
</div>
|
|
929
958
|
<div class="field" id="details-started-field" hidden><label>Started</label><div class="ro-value" id="details-started" data-testid="details-started" style="overflow-wrap:anywhere;">—</div></div>
|
|
930
959
|
<div class="field" id="details-schedule-field" hidden><label>Schedule</label><div class="ro-value" id="details-schedule" data-testid="details-schedule" style="overflow-wrap:anywhere;">—</div></div>
|
|
931
960
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
@@ -942,6 +971,11 @@
|
|
|
942
971
|
</div>
|
|
943
972
|
</section>
|
|
944
973
|
|
|
974
|
+
<section id="panel-result" class="tabpanel" role="tabpanel" aria-labelledby="tab-result" hidden data-testid="panel-result">
|
|
975
|
+
<div id="result-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
976
|
+
<div id="result-body" data-testid="result-body" hidden></div>
|
|
977
|
+
</section>
|
|
978
|
+
|
|
945
979
|
</div>
|
|
946
980
|
</section>
|
|
947
981
|
|
|
@@ -1067,7 +1101,7 @@
|
|
|
1067
1101
|
});
|
|
1068
1102
|
}
|
|
1069
1103
|
wireTabs([document.getElementById("tab-chat"), document.getElementById("tab-runs")]);
|
|
1070
|
-
wireTabs([document.getElementById("tab-run"), document.getElementById("tab-audit"), document.getElementById("tab-details")]);
|
|
1104
|
+
wireTabs([document.getElementById("tab-run"), document.getElementById("tab-audit"), document.getElementById("tab-details"), document.getElementById("tab-result")]);
|
|
1071
1105
|
|
|
1072
1106
|
// ---- fetch helpers ----
|
|
1073
1107
|
function getJSON(path){
|
|
@@ -1167,7 +1201,7 @@
|
|
|
1167
1201
|
// result when nothing was ever judged there; `step`/`fix` (real attempts)
|
|
1168
1202
|
// and `run` (its own real, non-null outcome) are the only kinds worth an
|
|
1169
1203
|
// inline attempt/result glyph.
|
|
1170
|
-
function partHasNoVerdict(part){ return part.kind === "scout" || part.kind === "plan" || part.kind === "replan" || part.kind === "judge"; }
|
|
1204
|
+
function partHasNoVerdict(part){ return part.kind === "scout" || part.kind === "plan" || part.kind === "replan" || part.kind === "judge" || part.kind === "door"; }
|
|
1171
1205
|
// ONE shared result-glyph rule for both the Run tab's cards and the Audit
|
|
1172
1206
|
// tab's grouped-row headers — `null` (never a fabricated "?") for a
|
|
1173
1207
|
// no-verdict part kind; a multi-attempt part (a step with retries, or the
|
|
@@ -1383,7 +1417,7 @@
|
|
|
1383
1417
|
// manual fold memory, keyed by job ("__quiet" is the quiet fold): a per-viewer convenience, reset on reload. A manual click wins over an automatic open.
|
|
1384
1418
|
var rlOpen = {};
|
|
1385
1419
|
// the last markup written into each box: an identical repaint (every poll) writes nothing, so focus, scroll and a half-read card survive it
|
|
1386
|
-
var rlLast = { top: null, jobs: null };
|
|
1420
|
+
var rlLast = { top: null, sched: null, jobs: null };
|
|
1387
1421
|
// The ONE writer of `rlData`: the whole /api/runs payload, as the server sent it.
|
|
1388
1422
|
function setRunsData(r){
|
|
1389
1423
|
rlData = { runs: r.runs || [], jobs: r.jobs || [], scheduled: r.scheduled || [], limits: r.limits || rlData.limits };
|
|
@@ -1404,7 +1438,7 @@
|
|
|
1404
1438
|
function rlRunParts(r){
|
|
1405
1439
|
var rn = runLabel(r.runNo, r.runid);
|
|
1406
1440
|
var p = { rn: rn, name: r.job + " (" + rn + ")", date: runAtText(r), model: r.model || "", word: "", why: "", spend: "", wall: "" };
|
|
1407
|
-
if(r.starting){ p.word = "running"; p.why = "starting…"; return p; }
|
|
1441
|
+
if(r.starting){ p.word = "running"; p.why = r.startingWord || "starting…"; return p; }
|
|
1408
1442
|
if(r.fileMissing){ p.word = ""; p.why = "file missing"; return p; }
|
|
1409
1443
|
p.word = r.status && r.status.word ? r.status.word : "";
|
|
1410
1444
|
p.why = r.endedLine || (p.word === "passed" ? "goal met" : "");
|
|
@@ -1473,8 +1507,19 @@
|
|
|
1473
1507
|
));
|
|
1474
1508
|
}
|
|
1475
1509
|
|
|
1510
|
+
// PRD v1.106 Part 2 (fwd M11 am17-am20): the one search box narrows Scheduled cards by JOB NAME only. A run number (run-3) or a date (10/13, 2026-10-13, 10/)
|
|
1511
|
+
// narrows Jobs only, so those words are skipped here; every other word must be a substring of the job name. The result and time chips never touch Scheduled.
|
|
1512
|
+
function rlScheduledMatches(card, query){
|
|
1513
|
+
var name = String(card && card.job || "").toLowerCase();
|
|
1514
|
+
var words = String(query || "").toLowerCase().replace(/[()]/g, "").split(/\s+/).filter(function(w){ return w; });
|
|
1515
|
+
return words.every(function(w){
|
|
1516
|
+
if(/^run-\d+$/.test(w) || (/^[\d\/-]+$/.test(w) && /[\/-]/.test(w))) return true;
|
|
1517
|
+
return name.indexOf(w) !== -1;
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1476
1520
|
function rlModel(data, groups, filters, limits, sel, open){
|
|
1477
1521
|
var active = filtersActive(filters);
|
|
1522
|
+
var schedOnly = !!(filters && filters.scheduledOnly);
|
|
1478
1523
|
var place = {};
|
|
1479
1524
|
(data.jobs || []).forEach(function(j, i){ place[j.job] = { quiet: j.quiet === true, at: i }; });
|
|
1480
1525
|
var cards = groups.slice().sort(function(a, b){
|
|
@@ -1504,10 +1549,11 @@
|
|
|
1504
1549
|
return {
|
|
1505
1550
|
running: data.runs.filter(function(r){ return r.live === true; }),
|
|
1506
1551
|
needs: data.runs.filter(function(r){ return typeof r.needs === "string" && r.needs !== ""; }),
|
|
1507
|
-
scheduled: data.scheduled || [],
|
|
1552
|
+
scheduled: (data.scheduled || []).filter(function(c){ return rlScheduledMatches(c, filters && filters.search); }),
|
|
1553
|
+
schedOnly: schedOnly,
|
|
1508
1554
|
gaveUp: data.gaveUp || [],
|
|
1509
|
-
jobs: cards.filter(function(c){ return !c.quiet; }),
|
|
1510
|
-
quiet: quietCards,
|
|
1555
|
+
jobs: schedOnly ? [] : cards.filter(function(c){ return !c.quiet; }),
|
|
1556
|
+
quiet: schedOnly ? [] : quietCards,
|
|
1511
1557
|
quietOpen: Object.prototype.hasOwnProperty.call(open, "__quiet") ? open.__quiet : quietCards.some(function(c){ return c.selected; }),
|
|
1512
1558
|
limits: limits,
|
|
1513
1559
|
noRows: data.runs.length === 0,
|
|
@@ -1545,7 +1591,7 @@
|
|
|
1545
1591
|
}
|
|
1546
1592
|
// Scheduled card (T Part 7): `[·] job today 15:00` / `next in 3h · Every day at 15:00`, all from the server. A file trigger has no clock time:
|
|
1547
1593
|
// `at` / `in` are null and nothing is made up. A click does what Running now does (rlJump to the job's HEAD run, `runId` from the server and
|
|
1548
|
-
// scrolls to its job). A job with no run to open stays a plain box.
|
|
1594
|
+
// scrolls to its job). A job with no run to open stays a plain box. Drawn BELOW the search box (rlSchedHtml); the search and the \u25F7 chip narrow it by job name.
|
|
1549
1595
|
function rlScheduledCardHtml(s){
|
|
1550
1596
|
var body = '<span class="rl-l1"><span class="dot grey"></span><span class="rl-nm" title="' + escapeXml(s.job) + '">' + escapeXml(s.job) + '</span>' + rlRunTagHtml(s) +
|
|
1551
1597
|
(s.at ? ' <span class="rl-rest">· ' + escapeXml(s.at) + '</span>' : '') + '</span>' +
|
|
@@ -1553,17 +1599,25 @@
|
|
|
1553
1599
|
if(typeof s.runId !== "string" || s.runId === "") return '<div class="rl-card" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</div>';
|
|
1554
1600
|
return '<button class="rl-card" type="button" data-rl-jump="' + escapeXml(s.runId) + '" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</button>';
|
|
1555
1601
|
}
|
|
1556
|
-
// The sections above the
|
|
1602
|
+
// The sections above the search box, from ONE ordered list: only never-filtered sections live here (Scheduled is searched, so rlSchedHtml draws it below).
|
|
1557
1603
|
function rlTopHtml(m){
|
|
1558
1604
|
var sections = [
|
|
1559
1605
|
{ label: "Running now", items: m.running, draw: function(r){ return rlJumpCardHtml(r, false); } },
|
|
1560
1606
|
{ label: "Needs you", items: m.needs.concat(m.gaveUp || []), draw: function(r){ return r.pill ? rlGaveUpCardHtml(r) : rlJumpCardHtml(r, true); } },
|
|
1561
|
-
{ label: "Scheduled", items: m.scheduled, draw: rlScheduledCardHtml },
|
|
1562
1607
|
];
|
|
1563
1608
|
return sections.map(function(s){
|
|
1564
1609
|
return s.items.length ? rlSectHtml(s.label, s.items.length) + s.items.map(s.draw).join("") : "";
|
|
1565
1610
|
}).join("");
|
|
1566
1611
|
}
|
|
1612
|
+
// Scheduled sits BELOW the search box (PRD v1.106 Part 2). No match draws nothing, header included; with the \u25F7 chip pressed it says so in words.
|
|
1613
|
+
function rlSchedHtml(m){
|
|
1614
|
+
if(m.scheduled.length) return rlSectHtml("Scheduled", m.scheduled.length) + m.scheduled.map(rlScheduledCardHtml).join("");
|
|
1615
|
+
return m.schedOnly ? '<div class="hint" data-testid="rl-sched-none">no scheduled job matches</div>' : "";
|
|
1616
|
+
}
|
|
1617
|
+
// the count line under the chips: only the \u25F7 view has one; "showing N scheduled job(s)". Empty otherwise.
|
|
1618
|
+
function rlCountText(m){
|
|
1619
|
+
return m.schedOnly ? "showing " + m.scheduled.length + " scheduled job" + (m.scheduled.length === 1 ? "" : "s") : "";
|
|
1620
|
+
}
|
|
1567
1621
|
// a run card inside a job: `[sign] run-N MM/DD HH:MM · model` / `word — why · $cost · wall`
|
|
1568
1622
|
function rlRunCardHtml(r, selected, hit){
|
|
1569
1623
|
var p = rlRunParts(r);
|
|
@@ -1622,6 +1676,7 @@
|
|
|
1622
1676
|
return out + '</div></div>';
|
|
1623
1677
|
}
|
|
1624
1678
|
function rlJobsHtml(m){
|
|
1679
|
+
if(m.schedOnly) return "";
|
|
1625
1680
|
var h = "";
|
|
1626
1681
|
if(m.jobs.length) h += rlSectHtml("Jobs", m.jobs.length, "· run in the last " + m.limits.quietDays + " days") + m.jobs.map(rlJobCardHtml).join("");
|
|
1627
1682
|
if(m.quiet.length){
|
|
@@ -1660,9 +1715,14 @@
|
|
|
1660
1715
|
var m = rlModel(rlData, groups, filters, rlData.limits, { runid: currentRunid }, rlOpen);
|
|
1661
1716
|
var topEl = document.getElementById("rl-top");
|
|
1662
1717
|
var listEl = document.getElementById("wf-list");
|
|
1718
|
+
var schedEl = document.getElementById("rl-sched");
|
|
1719
|
+
var countEl = document.getElementById("runs-filter-count");
|
|
1720
|
+
var sched = rlSchedHtml(m);
|
|
1663
1721
|
var top = rlTopHtml(m);
|
|
1664
1722
|
var jobs = rlJobsHtml(m);
|
|
1665
1723
|
if(top !== rlLast.top){ rlLast.top = top; withFocusPreserved(topEl, function(){ topEl.innerHTML = top; }); }
|
|
1724
|
+
if(sched !== rlLast.sched){ rlLast.sched = sched; withFocusPreserved(schedEl, function(){ schedEl.innerHTML = sched; }); }
|
|
1725
|
+
if(countEl && (rlCountText(m) !== "" || /^showing /.test(countEl.textContent || ""))) countEl.textContent = rlCountText(m);
|
|
1666
1726
|
if(jobs !== rlLast.jobs){ rlLast.jobs = jobs; withFocusPreserved(listEl, function(){ listEl.innerHTML = jobs; }); }
|
|
1667
1727
|
tickSince();
|
|
1668
1728
|
}
|
|
@@ -1715,6 +1775,7 @@
|
|
|
1715
1775
|
{ group: "result", value: "\u2717", title: "fail", id: "res-fail", label: "✗" },
|
|
1716
1776
|
{ group: "result", value: "?", title: "died", id: "res-died", label: "?" },
|
|
1717
1777
|
{ group: "result", value: "\u25B6", title: "running", id: "res-running", label: "▶" },
|
|
1778
|
+
{ group: "view", value: "scheduled", title: "scheduled jobs only", id: "view-scheduled", label: "◷", aria: "scheduled jobs only" },
|
|
1718
1779
|
{ group: "time", value: "7d", title: "last 7 days", id: "time-7d", label: "7d" },
|
|
1719
1780
|
{ group: "time", value: "30d", title: "last 30 days", id: "time-30d", label: "30d" },
|
|
1720
1781
|
{ group: "time", value: "all", title: "all time", id: "time-all", label: "all", pressed: true },
|
|
@@ -1728,7 +1789,7 @@
|
|
|
1728
1789
|
'<div class="chips" role="group" aria-label="Filter">' +
|
|
1729
1790
|
RL_CHIPS.map(function(c){
|
|
1730
1791
|
if(c.clear) return '<button class="chip fchip" type="button" id="' + scope + '-filter-clear" data-testid="' + scope + '-filter-clear">clear</button>';
|
|
1731
|
-
return '<button class="chip fchip" type="button" aria-pressed="' + (c.pressed ? 'true' : 'false') + '" data-filter-group="' + c.group + '" data-filter-value="' + escapeXml(c.value) + '" title="' + c.title + '" data-testid="chip-' + scope + '-' + c.id + '">' + c.label + '</button>';
|
|
1792
|
+
return '<button class="chip fchip" type="button" aria-pressed="' + (c.pressed ? 'true' : 'false') + '" data-filter-group="' + c.group + '" data-filter-value="' + escapeXml(c.value) + '" title="' + c.title + '"' + (c.aria ? ' aria-label="' + c.aria + '"' : '') + ' data-testid="chip-' + scope + '-' + c.id + '">' + c.label + '</button>';
|
|
1732
1793
|
}).join("") +
|
|
1733
1794
|
'</div>' +
|
|
1734
1795
|
'<div class="hint" id="' + scope + '-filter-count" data-testid="' + scope + '-filter-count"></div>' +
|
|
@@ -1769,7 +1830,7 @@
|
|
|
1769
1830
|
function createFilterBar(opts){
|
|
1770
1831
|
var scope = opts.scope;
|
|
1771
1832
|
var storageKey = opts.storageKey;
|
|
1772
|
-
var state = { checkTypes: [], results: [], time: "all", search: "" };
|
|
1833
|
+
var state = { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
|
|
1773
1834
|
|
|
1774
1835
|
function load(){
|
|
1775
1836
|
try{
|
|
@@ -1782,6 +1843,7 @@
|
|
|
1782
1843
|
results: Array.isArray(parsed.results) ? parsed.results : [],
|
|
1783
1844
|
time: typeof parsed.time === "string" ? parsed.time : "all",
|
|
1784
1845
|
search: typeof parsed.search === "string" ? parsed.search : "",
|
|
1846
|
+
scheduledOnly: parsed.scheduledOnly === true,
|
|
1785
1847
|
};
|
|
1786
1848
|
}
|
|
1787
1849
|
}
|
|
@@ -1796,6 +1858,7 @@
|
|
|
1796
1858
|
var value = chip.getAttribute("data-filter-value");
|
|
1797
1859
|
var active;
|
|
1798
1860
|
if(group === "time"){ active = state.time === value; }
|
|
1861
|
+
else if(group === "view"){ active = state.scheduledOnly === true; }
|
|
1799
1862
|
else if(group === "checkType"){ active = state.checkTypes.indexOf(value) !== -1; }
|
|
1800
1863
|
else { active = state.results.indexOf(value) !== -1; }
|
|
1801
1864
|
chip.setAttribute("aria-pressed", active ? "true" : "false");
|
|
@@ -1812,6 +1875,8 @@
|
|
|
1812
1875
|
var value = chip.getAttribute("data-filter-value");
|
|
1813
1876
|
if(group === "time"){
|
|
1814
1877
|
state.time = value; // single-select within time
|
|
1878
|
+
} else if(group === "view"){
|
|
1879
|
+
state.scheduledOnly = !state.scheduledOnly; // the \u25F7 chip: Scheduled only, a toggle
|
|
1815
1880
|
} else if(group === "checkType"){
|
|
1816
1881
|
var idx = state.checkTypes.indexOf(value);
|
|
1817
1882
|
if(idx === -1) state.checkTypes.push(value); else state.checkTypes.splice(idx, 1);
|
|
@@ -1827,7 +1892,7 @@
|
|
|
1827
1892
|
var clearBtn = document.getElementById(scope + "-filter-clear");
|
|
1828
1893
|
if(clearBtn){
|
|
1829
1894
|
clearBtn.addEventListener("click", function(){
|
|
1830
|
-
state = { checkTypes: [], results: [], time: "all", search: "" };
|
|
1895
|
+
state = { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
|
|
1831
1896
|
paint();
|
|
1832
1897
|
save();
|
|
1833
1898
|
apply();
|
|
@@ -1859,7 +1924,7 @@
|
|
|
1859
1924
|
onChange: function(){ paintRunsList(); },
|
|
1860
1925
|
});
|
|
1861
1926
|
function currentRunsFilters(){
|
|
1862
|
-
return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "" };
|
|
1927
|
+
return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
|
|
1863
1928
|
}
|
|
1864
1929
|
|
|
1865
1930
|
// item 4: phone-only "scroll the run view into view" on row select, and
|
|
@@ -2145,38 +2210,30 @@
|
|
|
2145
2210
|
document.getElementById("tab-runs").addEventListener("click", function(){ loadImports(); });
|
|
2146
2211
|
|
|
2147
2212
|
// ---- Run tab ----
|
|
2148
|
-
// panelMoney —
|
|
2149
|
-
//
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
// (
|
|
2153
|
-
// browser-script sibling of `src/panel/authorroutes.js`'s own
|
|
2154
|
-
// `panelMoney2` (same rule, can't share code — one runs in Node, one
|
|
2155
|
-
// inlined in the page).
|
|
2213
|
+
// panelMoney — the panel's ONE money render; every money display in THIS page (Run tab summary/
|
|
2214
|
+
// counters, Runs list rows, Result/Audit/Job tabs, chat cost lines) goes through it, no per-site
|
|
2215
|
+
// rounding. Browser-script copy of `src/panelmoney.js` `panelMoney2` (a page cannot import a
|
|
2216
|
+
// module); tests/panel-page.test.js pins the two to identical output. hamr's ruling 2026-10-19
|
|
2217
|
+
// (PRD "Part 6 rulings" (3)), superseding the 2026-09-28 "<$0.01" spelling:
|
|
2156
2218
|
// - null/undefined/non-finite -> "unknown" (never a fabricated $0).
|
|
2157
|
-
// - a REAL positive amount
|
|
2158
|
-
//
|
|
2159
|
-
// -
|
|
2160
|
-
//
|
|
2161
|
-
//
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
// sides: dollar figures here are derived from at most 6-decimal-precision
|
|
2165
|
-
// money (the same precision `src/text.js`'s `tallyCalls`/`priceOf` use),
|
|
2166
|
-
// so rounding to 6 decimals first erases that noise (e.g. 0.815 really
|
|
2167
|
-
// stored as 0.81499999999999995), and ONLY THEN is the value rounded/
|
|
2168
|
-
// floored to cents — a value already exactly on a cent boundary (e.g.
|
|
2169
|
-
// 0.82) can be stored as 81.99999999999999 cents post-multiply, and a
|
|
2170
|
-
// floor there would wrongly drop a whole cent; the tiny `+1e-6` nudge (at
|
|
2171
|
-
// dollar-cents scale, far below anything a real half-cent tie could be)
|
|
2172
|
-
// corrects that without ever crossing a true half-cent boundary the wrong
|
|
2173
|
-
// way.
|
|
2219
|
+
// - a REAL positive amount under a cent -> 4 decimals ("$0.0054"); one too small for 4
|
|
2220
|
+
// decimals reads "<$0.0001", never "$0.00"; one that rounds up to a cent reads "$0.01".
|
|
2221
|
+
// - $0.01 or more -> 2 decimals, half-up; an exact zero prints "$0.00".
|
|
2222
|
+
// - `floor` (a FLOOR/"at least" figure) rounds DOWN, at 2 decimals, or 4 under a cent.
|
|
2223
|
+
// Rounding to 6 decimals first erases float noise (0.815 stored as 0.81499999999999995); the tiny
|
|
2224
|
+
// `+1e-6` nudge corrects a value on an exact boundary stored just under it, without ever
|
|
2225
|
+
// crossing a true half-unit tie the wrong way.
|
|
2174
2226
|
function panelMoney(n, floor){
|
|
2175
2227
|
if(n === null || n === undefined) return "unknown";
|
|
2176
2228
|
var v = Number(n);
|
|
2177
2229
|
if(!isFinite(v)) return "unknown";
|
|
2178
|
-
if(v > 0 && v < 0.01) return "<$0.01";
|
|
2179
2230
|
var clean = Math.round(v * 1e6) / 1e6;
|
|
2231
|
+
if(clean > 0 && clean < 0.01){
|
|
2232
|
+
var units = floor ? Math.floor(clean * 1e4 + 1e-6) : Math.round(clean * 1e4 + 1e-6);
|
|
2233
|
+
if(units <= 0) return "<$0.0001";
|
|
2234
|
+
if(units >= 100) return "$0.01";
|
|
2235
|
+
return "$" + (units / 1e4).toFixed(4);
|
|
2236
|
+
}
|
|
2180
2237
|
var cents = floor ? Math.floor(clean * 100 + 1e-6) : Math.round(clean * 100 + 1e-6);
|
|
2181
2238
|
if(cents < 0) cents = 0;
|
|
2182
2239
|
return "$" + (cents / 100).toFixed(2);
|
|
@@ -2199,8 +2256,16 @@
|
|
|
2199
2256
|
var core = (spendCoreVal === null || spendCoreVal === undefined)
|
|
2200
2257
|
? "unknown"
|
|
2201
2258
|
: ((spendCoreFloor || draftFloor) ? "at least " + panelMoney(spendCoreVal) : panelMoney(spendCoreVal));
|
|
2202
|
-
var draftPart = hasDraft ? (shortForm ? " (drafting " : " (") + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2203
2259
|
var capPart = (budgetUsd === undefined) ? "" : (" of " + panelMoney(budgetUsd));
|
|
2260
|
+
if(hasDraft && !shortForm){
|
|
2261
|
+
// PRD v1.106 Part 5 Q4, the panel's spelling of src/replay.js moneyWithDraft: "$0.18 run + $0.0035 drafting = $0.18". The run's figure
|
|
2262
|
+
// EXCLUDES drafting; each part keeps the panel's money rule (panelMoney) and the TOTAL is that rule applied to the true sum (so
|
|
2263
|
+
// two shown parts can round to a total that differs by a cent). Without the run's number the sum is left out.
|
|
2264
|
+
var haveRun = typeof spendCoreVal === "number";
|
|
2265
|
+
var totalPart = haveRun ? " = " + ((spendCoreFloor || draftFloor) ? "at least " : "") + panelMoney(spendCoreVal + draftSpentUsd) : "";
|
|
2266
|
+
return core + " run + " + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + " drafting" + totalPart + capPart;
|
|
2267
|
+
}
|
|
2268
|
+
var draftPart = hasDraft ? " (drafting " + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + ")" : "";
|
|
2204
2269
|
return core + draftPart + capPart;
|
|
2205
2270
|
}
|
|
2206
2271
|
// P5 item 6: a LIVE row (no job-end yet, runner alive) reads the same running floors the
|
|
@@ -2478,6 +2543,19 @@
|
|
|
2478
2543
|
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2479
2544
|
});
|
|
2480
2545
|
}
|
|
2546
|
+
// ONE Accept handler for the Run tab's action bar and the Result tab's door (PRD v1.107 Part 6): same route, same words. `host` is where a refusal is written.
|
|
2547
|
+
function acceptDoor(detail, accBtn, host){
|
|
2548
|
+
accBtn.disabled = true;
|
|
2549
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/accept", {}).then(function(res){
|
|
2550
|
+
if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
|
|
2551
|
+
if(res.status === 202 && res.body && res.body.pending){
|
|
2552
|
+
host.insertAdjacentHTML("beforeend", '<span class="stopping" data-testid="accept-pending">still re-checking the tree; look again in a moment</span>');
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
accBtn.disabled = false;
|
|
2556
|
+
host.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
|
|
2557
|
+
}).catch(function(x){ accBtn.disabled = false; host.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
|
|
2558
|
+
}
|
|
2481
2559
|
function renderRunActions(detail){
|
|
2482
2560
|
var bar = document.getElementById("run-actions");
|
|
2483
2561
|
var html = "";
|
|
@@ -2544,20 +2622,7 @@
|
|
|
2544
2622
|
});
|
|
2545
2623
|
}
|
|
2546
2624
|
var accBtn = bar.querySelector('[data-testid="btn-accept-run"]');
|
|
2547
|
-
if(accBtn){
|
|
2548
|
-
accBtn.addEventListener("click", function(){
|
|
2549
|
-
accBtn.disabled = true;
|
|
2550
|
-
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/accept", {}).then(function(res){
|
|
2551
|
-
if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
|
|
2552
|
-
if(res.status === 202 && res.body && res.body.pending){
|
|
2553
|
-
bar.insertAdjacentHTML("beforeend", '<span class="stopping" data-testid="accept-pending">still re-checking the tree; look again in a moment</span>');
|
|
2554
|
-
return;
|
|
2555
|
-
}
|
|
2556
|
-
accBtn.disabled = false;
|
|
2557
|
-
bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
|
|
2558
|
-
}).catch(function(x){ accBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
|
|
2559
|
-
});
|
|
2560
|
-
}
|
|
2625
|
+
if(accBtn) accBtn.addEventListener("click", function(){ acceptDoor(detail, accBtn, bar); });
|
|
2561
2626
|
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2562
2627
|
if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
|
|
2563
2628
|
}
|
|
@@ -2590,6 +2655,7 @@
|
|
|
2590
2655
|
|
|
2591
2656
|
function renderRun(detail){
|
|
2592
2657
|
if(detail.fileMissing){
|
|
2658
|
+
syncResultTab({});
|
|
2593
2659
|
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
2594
2660
|
document.getElementById("run-empty").hidden = false;
|
|
2595
2661
|
document.getElementById("run-empty").textContent = "spine file no longer on disk";
|
|
@@ -2600,13 +2666,29 @@
|
|
|
2600
2666
|
return;
|
|
2601
2667
|
}
|
|
2602
2668
|
|
|
2669
|
+
if(detail.refused){
|
|
2670
|
+
// refused before it started (e.g. the page could not be fetched): the server's plain words, status word from the table
|
|
2671
|
+
syncResultTab({});
|
|
2672
|
+
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, "") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
|
|
2673
|
+
document.getElementById("run-empty").hidden = false;
|
|
2674
|
+
document.getElementById("run-empty").textContent = detail.endedLine || "";
|
|
2675
|
+
document.getElementById("run-content").hidden = true;
|
|
2676
|
+
document.getElementById("audit-select-empty").hidden = false;
|
|
2677
|
+
document.getElementById("audit-select-empty").textContent = detail.endedLine || "";
|
|
2678
|
+
document.getElementById("audit-body").hidden = true;
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2603
2682
|
if(detail.starting){
|
|
2604
|
-
|
|
2683
|
+
syncResultTab({});
|
|
2684
|
+
// the words come from the server (startingWord / startingNote), never typed here
|
|
2685
|
+
var startWords = (detail.startingWord || "starting…") + " " + (detail.startingNote || "the run has not written its log yet");
|
|
2686
|
+
setRunHeader("\u25B6", runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, detail.startingWord || "starting\u2026") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
|
|
2605
2687
|
document.getElementById("run-empty").hidden = false;
|
|
2606
|
-
document.getElementById("run-empty").textContent =
|
|
2688
|
+
document.getElementById("run-empty").textContent = startWords;
|
|
2607
2689
|
document.getElementById("run-content").hidden = true;
|
|
2608
2690
|
document.getElementById("audit-select-empty").hidden = false;
|
|
2609
|
-
document.getElementById("audit-select-empty").textContent =
|
|
2691
|
+
document.getElementById("audit-select-empty").textContent = startWords;
|
|
2610
2692
|
document.getElementById("audit-body").hidden = true;
|
|
2611
2693
|
return;
|
|
2612
2694
|
}
|
|
@@ -2684,10 +2766,13 @@
|
|
|
2684
2766
|
'<div class="summary-row"><span class="sum-label">why:</span><span class="sum-val">' + escapeXml(detail.stopReason || (detail.glyph === "✓" ? "run ended clean" : "not recorded")) + '</span></div>' +
|
|
2685
2767
|
'<div class="summary-row"><span class="sum-label">close:</span><span class="sum-val">' + escapeXml(detail.close && detail.close.verdict ? detail.close.verdict : "none — the run ended before any close ran") + '</span></div>' +
|
|
2686
2768
|
'<div class="summary-row" data-testid="summary-tools"><span class="sum-label">tools:</span><span class="sum-val">' + escapeXml(detail.fromBridge === true ? bridgeToolsLine(detail.toolsUsed) : toolsLine(detail.behaviour)) + '</span></div>' +
|
|
2769
|
+
(detail.fromBridge !== true && typeof detail.callsLine === "string" ? '<div class="summary-row" data-testid="summary-calls"><span class="sum-label">calls:</span><span class="sum-val">' + escapeXml(detail.callsLine) + '</span></div>' : '') +
|
|
2687
2770
|
'<div class="summary-row" data-testid="summary-offered" id="summary-offered-row" hidden><span class="sum-label">offered:</span><span class="sum-val" id="summary-offered-val"></span></div>' +
|
|
2771
|
+
(detail.fromBridge !== true && typeof detail.checksLine === "string" ? '<div class="summary-row" data-testid="summary-checks"><span class="sum-label">checks:</span><span class="sum-val">' + escapeXml(detail.checksLine) + '</span></div>' : '') +
|
|
2688
2772
|
'<div class="summary-row" data-testid="summary-cache"><span class="sum-label">cache:</span><span class="sum-val">' + escapeXml(cacheLine(detail.memoryCache)) + '</span></div>';
|
|
2689
2773
|
|
|
2690
2774
|
paintOfferedRow();
|
|
2775
|
+
syncResultTab(detail);
|
|
2691
2776
|
|
|
2692
2777
|
// build item B (2026-09-26 rewrite): the map AND the card list are both
|
|
2693
2778
|
// built from ONE list — `detail.parts` (src/replay.js's own ordered part
|
|
@@ -2722,9 +2807,11 @@
|
|
|
2722
2807
|
'<div class="step-head"><h4>' + titleText + '</h4>' +
|
|
2723
2808
|
stepStateHTML(box, 'part-state-' + idx) +
|
|
2724
2809
|
(box.blocked > 0 ? '<span class="badge red" data-testid="part-blocked-badge">' + box.blocked + ' blocked</span>' : '') +
|
|
2810
|
+
actorMarkHtml(part) +
|
|
2725
2811
|
'</div>' +
|
|
2726
2812
|
'<div class="step-meta" title="a part is one piece of the run (scout, plan, a step, replan, fix, judge); a call is one model round">' + line1Bits.join(" · ") + '</div>' +
|
|
2727
|
-
'<div class="step-meta">' + escapeXml(partLine2Text(box.part)) + '</div>'
|
|
2813
|
+
'<div class="step-meta">' + escapeXml(partLine2Text(box.part)) + '</div>' +
|
|
2814
|
+
(typeof part.triesLine === "string" ? '<div class="step-meta" data-testid="part-tries-' + idx + '">' + escapeXml(part.triesLine) + '</div>' : '');
|
|
2728
2815
|
listEl.appendChild(card);
|
|
2729
2816
|
var jumpToAudit = function(){
|
|
2730
2817
|
document.getElementById("tab-audit").click();
|
|
@@ -2746,7 +2833,11 @@
|
|
|
2746
2833
|
// ONE spelling of a step's state: its dot + the bold state word — read by the Run tab's step cards AND the Audit tab's grouped rows
|
|
2747
2834
|
function stepStateHTML(box, testid){
|
|
2748
2835
|
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? (box.status && box.status.sign === "\u25A0" ? "stopped" : "red") : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2749
|
-
return
|
|
2836
|
+
return stateDotHTML(cls, boxStatusWord(box), testid);
|
|
2837
|
+
}
|
|
2838
|
+
// ONE spelling of the dot + bold word every state header wears (part headers and the schedule header).
|
|
2839
|
+
function stateDotHTML(cls, word, testid){
|
|
2840
|
+
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + (cls === "amber" ? " pulse" : "") + '"></span><b class="st-word">' + escapeXml(word) + '</b></span>';
|
|
2750
2841
|
}
|
|
2751
2842
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2752
2843
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
@@ -2766,6 +2857,7 @@
|
|
|
2766
2857
|
row.innerHTML =
|
|
2767
2858
|
'<button class="btn small attempt-toggle" type="button" aria-expanded="false">' +
|
|
2768
2859
|
'attempt ' + a.n + ' ' + attemptGlyph(a.outcome, boxIsLive) +
|
|
2860
|
+
(a.thinking ? ' · ' + escapeXml(a.thinking) : '') +
|
|
2769
2861
|
(checksText ? ' · ' + checksText : '') +
|
|
2770
2862
|
'</button>' +
|
|
2771
2863
|
(checksList ? '<div class="checks-list" data-testid="audit-attempt-checks-' + partIndex + '-' + a.n + '" hidden>' + checksList + '</div>' : '') +
|
|
@@ -2788,6 +2880,90 @@
|
|
|
2788
2880
|
});
|
|
2789
2881
|
}
|
|
2790
2882
|
|
|
2883
|
+
// PRD v1.106 Part 5: who did a part's work, in the server's own words (`part.marker`: [model] or [human · ask]); nothing typed here
|
|
2884
|
+
function actorMarkHtml(part){
|
|
2885
|
+
return part && typeof part.marker === "string" && part.marker !== "" ? ' <span class="actor-mark" data-testid="actor-mark">' + escapeXml(part.marker) + '</span>' : "";
|
|
2886
|
+
}
|
|
2887
|
+
// PRD v1.106 Part 5 C: the review door's fold: one human row for the ask, one for the answer once there is one
|
|
2888
|
+
function doorTableHtml(part){
|
|
2889
|
+
var rows = (part.doorView && part.doorView.rows ? part.doorView.rows : []).map(function(r){
|
|
2890
|
+
return "<tr><td>—</td><td>—</td><td>human</td><td>" + (r.path ? escapeXml(r.path) : "—") + "</td><td>" + escapeXml(r.text) + "</td><td>" +
|
|
2891
|
+
auditTimeCellHtml({ time: r.time }) + "</td></tr>";
|
|
2892
|
+
}).join("");
|
|
2893
|
+
return '<div class="audit-table-scroll"><table class="rounds-table" data-testid="door-table"><thead><tr><th>Round</th><th>Try</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead><tbody>' + rows + "</tbody></table></div>";
|
|
2894
|
+
}
|
|
2895
|
+
// PRD v1.106 Part 5 C: one try, in the server's words: its line (result glyph, thinking, the first reason and how many more), its numbered checks
|
|
2896
|
+
// in the order they ran (each with its result, a count or a folded reasons list, the job line it serves) and the file it judged
|
|
2897
|
+
function tryBlockHtml(part, t, boxIsLive, partIndex){
|
|
2898
|
+
var attempt = (part.attempts || []).filter(function(a){ return a.n === t.n; })[0] || null;
|
|
2899
|
+
var testid = "audit-try-" + partIndex + "-" + t.n;
|
|
2900
|
+
if(t.cutOff){
|
|
2901
|
+
return '<div class="try-line" data-testid="' + testid + '">try ' + t.n + (t.thinking ? " · " + escapeXml(t.thinking) : "") + " · " + escapeXml(t.words || "") + "</div>";
|
|
2902
|
+
}
|
|
2903
|
+
var checks = attempt && Array.isArray(attempt.checks) ? attempt.checks : [];
|
|
2904
|
+
var firstBad = checks.filter(function(c){ return !c.pass && c.gap; })[0] || null;
|
|
2905
|
+
var html = '<div class="try-line" data-testid="' + testid + '">try ' + t.n + " " + glyphSpan(attemptGlyph(t.outcome, boxIsLive)) +
|
|
2906
|
+
(t.thinking ? " · " + escapeXml(t.thinking) : "") +
|
|
2907
|
+
(firstBad ? " · " + escapeXml(firstBad.gap) + (firstBad.moreText ? " " + escapeXml(firstBad.moreText) : "") : "") + "</div>";
|
|
2908
|
+
checks.forEach(function(c){
|
|
2909
|
+
var label = c.question || c.name || "unknown";
|
|
2910
|
+
html += '<div class="try-check" data-testid="' + testid + '-check-' + c.n + '">' + c.n + " " + glyphSpan(c.pass ? "\u2713" : "\u2717") + " " + escapeXml(label) +
|
|
2911
|
+
(c.note ? " (" + escapeXml(c.note) + ")" : "") + (c.from ? " · " + escapeXml(c.from) : "") + "</div>";
|
|
2912
|
+
if(!c.pass && c.reasonsLabel){
|
|
2913
|
+
html += '<details class="try-reasons" data-testid="' + testid + '-reasons-' + c.n + '"><summary>' + escapeXml(c.reasonsLabel) + "</summary>" +
|
|
2914
|
+
c.reasons.map(function(r){ return "<div>" + escapeXml(r) + "</div>"; }).join("") + "</details>";
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
(attempt && Array.isArray(attempt.judged) ? attempt.judged : []).forEach(function(j){
|
|
2918
|
+
html += '<div class="try-judged" data-testid="' + testid + '-judged">' + escapeXml(j) + "</div>";
|
|
2919
|
+
});
|
|
2920
|
+
return html;
|
|
2921
|
+
}
|
|
2922
|
+
// PRD v1.106 Part 5 C: a plan step's fold — tools tally, every try with its checks, then ONE Round table for the whole step (20 rows a page)
|
|
2923
|
+
function buildStepFold(body, partIndex, part, boxIsLive){
|
|
2924
|
+
var wrap = document.createElement("div");
|
|
2925
|
+
wrap.className = "step-fold";
|
|
2926
|
+
wrap.setAttribute("data-testid", "audit-step-fold-" + partIndex);
|
|
2927
|
+
var html = part.tally ? '<div class="step-tally" data-testid="audit-tally-' + partIndex + '">' + escapeXml(part.tally) + "</div>" : "";
|
|
2928
|
+
part.tries.forEach(function(t){ html += tryBlockHtml(part, t, boxIsLive, partIndex); });
|
|
2929
|
+
html += '<div class="rounds-list" data-testid="audit-step-rounds-' + partIndex + '"></div>';
|
|
2930
|
+
wrap.innerHTML = html;
|
|
2931
|
+
body.appendChild(wrap);
|
|
2932
|
+
loadStepRows(wrap.querySelector(".rounds-list"), partIndex, 0, false);
|
|
2933
|
+
}
|
|
2934
|
+
function stepRowHtml(r){
|
|
2935
|
+
if(r.kind === "check"){
|
|
2936
|
+
return "<tr><td>—</td><td>" + r.try + "</td><td>check</td><td>" + (r.path ? escapeXml(r.path) : "—") + "</td><td>" + glyphSpan(r.sign) + " " + escapeXml(r.text) + "</td><td>" + auditTimeCellHtml(r) + "</td></tr>";
|
|
2937
|
+
}
|
|
2938
|
+
if(r.kind === "model"){
|
|
2939
|
+
var pseudo = { kind: "model-call", round: r.round, time: r.time, costUsd: r.costUsd, tokens: r.tokens, durationMs: r.durationMs };
|
|
2940
|
+
return '<tr class="round-header-row"><td>' + r.round + "</td><td>" + r.try + "</td><td>" + auditActionCellHtml(pseudo) + "</td><td>" + auditPathCellHtml(pseudo) + "</td><td>" + auditDecisionCellHtml(pseudo) + "</td><td>" + auditTimeCellHtml(pseudo) + "</td></tr>";
|
|
2941
|
+
}
|
|
2942
|
+
var cls = auditRowClassName(r);
|
|
2943
|
+
return "<tr" + (cls ? ' class="' + cls + '"' : "") + "><td>" + auditRoundCellHtml(r) + "</td><td>" + r.try + "</td><td>model · " + auditActionCellHtml(r) + "</td><td>" + auditPathCellHtml(r) + "</td><td>" + auditDecisionCellHtml(r) + "</td><td>" + auditTimeCellHtml(r) + "</td></tr>";
|
|
2944
|
+
}
|
|
2945
|
+
function loadStepRows(el, partIndex, offset, appendMode){
|
|
2946
|
+
if(!appendMode) el.innerHTML = '<div class="hint">loading…</div>';
|
|
2947
|
+
getJSON("/api/runs/" + encodeURIComponent(currentRunid) + "/rounds?part=" + encodeURIComponent(partIndex) + "&all=1&offset=" + offset).then(function(data){
|
|
2948
|
+
var rowsHtml = data.rows.map(stepRowHtml).join("");
|
|
2949
|
+
var shownTo = data.offset + data.rows.length;
|
|
2950
|
+
var info = '<div class="hint">showing ' + (data.offset + 1) + "–" + shownTo + " of " + data.totalRows + "</div>";
|
|
2951
|
+
if(appendMode){
|
|
2952
|
+
var oldInfo = el.querySelector(".rounds-page-info");
|
|
2953
|
+
if(oldInfo) oldInfo.remove();
|
|
2954
|
+
el.querySelector(".rounds-table tbody").insertAdjacentHTML("beforeend", rowsHtml);
|
|
2955
|
+
} else {
|
|
2956
|
+
el.innerHTML = (data.toolLogSaved ? "" : '<div class="hint">tool calls: no log saved</div>') +
|
|
2957
|
+
'<div class="audit-table-scroll"><table class="rounds-table" data-testid="rounds-table"><thead><tr><th>Round</th><th>Try</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead><tbody>' + rowsHtml + "</tbody></table></div>";
|
|
2958
|
+
}
|
|
2959
|
+
el.insertAdjacentHTML("beforeend", '<div class="rounds-page-info">' + info + (shownTo < data.totalRows ? '<button class="btn small rounds-load-more" type="button">load more</button>' : "") + "</div>");
|
|
2960
|
+
var more = el.querySelector(".rounds-load-more");
|
|
2961
|
+
if(more) more.addEventListener("click", function(){ loadStepRows(el, partIndex, shownTo, true); });
|
|
2962
|
+
}).catch(function(e){
|
|
2963
|
+
el.innerHTML = '<div class="hint">failed to load rounds: ' + escapeXml(e.message) + "</div>";
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2791
2967
|
function roundsUrl(partIndex, attempt, offset){
|
|
2792
2968
|
var params = "part=" + encodeURIComponent(partIndex) + "&attempt=" + encodeURIComponent(attempt.n) + "&offset=" + offset;
|
|
2793
2969
|
return "/api/runs/" + encodeURIComponent(currentRunid) + "/rounds?" + params;
|
|
@@ -2897,6 +3073,7 @@
|
|
|
2897
3073
|
}
|
|
2898
3074
|
// line 2 of a part's card / Audit body: the drafting step names, or a worker part's tool breakdown
|
|
2899
3075
|
function partLine2Text(part){
|
|
3076
|
+
if(part && part.kind === "door") return part.doorView ? "last answer: " + part.doorView.last : "";
|
|
2900
3077
|
if(part && part.kind === "drafting") return (part.draftSteps || []).map(function(x){ return x.label; }).join(" · ");
|
|
2901
3078
|
return toolBreakdownLine(part.byTool);
|
|
2902
3079
|
}
|
|
@@ -2919,6 +3096,10 @@
|
|
|
2919
3096
|
rows += "<tr><td>" + escapeXml(st.label) + " (" + escapeXml(c.label) + ")</td><td>–</td><td>1</td><td>" +
|
|
2920
3097
|
(c.model ? escapeXml(c.model) : "–") + "</td><td>" + draftingCallCost(c) + "</td></tr>";
|
|
2921
3098
|
});
|
|
3099
|
+
// PRD v1.106 Part 5 Q1: each question the person answered, under its step, in the server's own words (asked, then said); wraps
|
|
3100
|
+
(Array.isArray(st.humans) ? st.humans : []).forEach(function(h){
|
|
3101
|
+
rows += '<tr class="human-row" data-testid="drafting-human"><td colspan="5" style="overflow-wrap:anywhere;white-space:pre-wrap">' + escapeXml(h.askedText) + "<br>" + escapeXml(h.saidText) + "</td></tr>";
|
|
3102
|
+
});
|
|
2922
3103
|
});
|
|
2923
3104
|
rows += '<tr class="drafting-total"><td><b>total</b></td><td>' + (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) +
|
|
2924
3105
|
"</td><td>" + (typeof part.rounds === "number" ? part.rounds : 0) + "</td><td>–</td><td>" + draftingMoneyText(part) + "</td></tr>";
|
|
@@ -2955,6 +3136,7 @@
|
|
|
2955
3136
|
*/
|
|
2956
3137
|
function partLine1Text(part, detail){
|
|
2957
3138
|
if(part && part.kind === "drafting") return draftingLine1Text(part);
|
|
3139
|
+
if(part && part.kind === "door") return part.doorView ? escapeXml(part.doorView.head) : "";
|
|
2958
3140
|
var isLiveRunPart = part && part.kind === "run" && detail && !detail.died && detail.spentUsd === null;
|
|
2959
3141
|
var wallText = (isLiveRunPart && part.wallMs === null) ? liveWallPhrase(detail.wallFloorMs) : duration(part.wallMs);
|
|
2960
3142
|
var spendText = (isLiveRunPart && part.spentUsd === null)
|
|
@@ -2970,6 +3152,8 @@
|
|
|
2970
3152
|
// MOST RECENT attempt's own declared-close grading (the opening
|
|
2971
3153
|
// outer-close grade counts as attempt 1, same list `buildAttemptsList`
|
|
2972
3154
|
// reads), in the withdrawn headline's place.
|
|
3155
|
+
// PRD v1.106 Part 3: a part with one call (plan, scout, replan, a one-attempt step) says the thinking it was sent here; the server worded it
|
|
3156
|
+
if(typeof part.thinking === "string" && part.thinking !== "") bits.push(escapeXml(part.thinking));
|
|
2973
3157
|
var latestAttempt = Array.isArray(part.attempts) && part.attempts.length ? part.attempts[part.attempts.length - 1] : null;
|
|
2974
3158
|
var headline = latestAttempt ? attemptChecksLine(latestAttempt) : "";
|
|
2975
3159
|
if(headline) bits.push(headline);
|
|
@@ -3161,7 +3345,7 @@
|
|
|
3161
3345
|
gBtn.className = "btn audit-part-toggle";
|
|
3162
3346
|
gBtn.setAttribute("aria-expanded", "false");
|
|
3163
3347
|
gBtn.setAttribute("data-testid", "audit-schedule-toggle");
|
|
3164
|
-
var secHead = function(open){ return (open ? "\u25BE " : "\u25B8 ") +
|
|
3348
|
+
var secHead = function(open){ return (open ? "\u25BE " : "\u25B8 ") + stateDotHTML(glyphClass(sec.sign), sec.word, "audit-schedule-state") + " " + escapeXml(sec.title) + " · " + escapeXml(sec.head); };
|
|
3165
3349
|
gBtn.innerHTML = secHead(false);
|
|
3166
3350
|
var gBody = document.createElement("div");
|
|
3167
3351
|
gBody.className = "audit-part-body";
|
|
@@ -3213,7 +3397,8 @@
|
|
|
3213
3397
|
headBtn.innerHTML =
|
|
3214
3398
|
stepStateHTML(box, 'audit-part-state-' + idx) + ' ' + titleText + ' · ' + partLine1Text(part, detail) +
|
|
3215
3399
|
(groupResultGlyph !== null ? ' · ' + groupResultGlyph : '') +
|
|
3216
|
-
(box.blocked > 0 ? ' <span class="badge red" data-testid="audit-part-blocked-' + idx + '">' + box.blocked + ' blocked</span>' : '')
|
|
3400
|
+
(box.blocked > 0 ? ' <span class="badge red" data-testid="audit-part-blocked-' + idx + '">' + box.blocked + ' blocked</span>' : '') +
|
|
3401
|
+
actorMarkHtml(part);
|
|
3217
3402
|
var body = document.createElement("div");
|
|
3218
3403
|
body.className = "audit-part-body";
|
|
3219
3404
|
body.hidden = !startExpanded;
|
|
@@ -3232,6 +3417,12 @@
|
|
|
3232
3417
|
var draftingEl = document.createElement("div");
|
|
3233
3418
|
draftingEl.innerHTML = draftingTableHtml(part);
|
|
3234
3419
|
body.appendChild(draftingEl);
|
|
3420
|
+
} else if(part.kind === "door"){
|
|
3421
|
+
var doorEl = document.createElement("div");
|
|
3422
|
+
doorEl.innerHTML = doorTableHtml(part);
|
|
3423
|
+
body.appendChild(doorEl);
|
|
3424
|
+
} else if(part.kind === "step" && Array.isArray(part.tries) && part.tries.length > 0){
|
|
3425
|
+
buildStepFold(body, idx, part, box.state === "running");
|
|
3235
3426
|
} else if(attempts.length > 1){
|
|
3236
3427
|
// parts WITH an attempt sub-loop (a multi-attempt step, or the fix
|
|
3237
3428
|
// loop) expand to attempt rows first — each attempt expands to its
|
|
@@ -3424,6 +3615,8 @@
|
|
|
3424
3615
|
var verdictEl = document.getElementById("details-verdict");
|
|
3425
3616
|
if(job.checkTypeTitle){ verdictEl.title = job.checkTypeTitle; } else { verdictEl.removeAttribute("title"); }
|
|
3426
3617
|
document.getElementById("details-model").textContent = job.model;
|
|
3618
|
+
// PRD v1.106 Part 3: the server words the Thinking row (src/thinking.js); it is always shown
|
|
3619
|
+
document.getElementById("details-thinking").textContent = typeof job.thinking === "string" ? job.thinking : "";
|
|
3427
3620
|
// RULED 2026-10-15: how the run began and the job's schedule NOW, both worded on the server (absent on an imported job)
|
|
3428
3621
|
[["started", job.started], ["schedule", job.schedule]].forEach(function(p){
|
|
3429
3622
|
var f = document.getElementById("details-" + p[0] + "-field");
|
|
@@ -3450,6 +3643,211 @@
|
|
|
3450
3643
|
paintOfferedRow();
|
|
3451
3644
|
}
|
|
3452
3645
|
|
|
3646
|
+
// ---- Result tab (PRD v1.107 Part 6, rubric jobs only; signed visual design/result-tab-mock.html variant F) ----
|
|
3647
|
+
// The server decides everything: `detail.hasResult` shows the tab, `/result` carries the signed lines, the facts' own sentences
|
|
3648
|
+
// (src/facts.js factSentence), the rows of the delivered file (sha256-checked against the run's own record) and `passed` (read from the
|
|
3649
|
+
// run's own check-run). The page only draws; it never decides a check and never searches a quote for a word.
|
|
3650
|
+
var resultDetail = null;
|
|
3651
|
+
var resultData = null;
|
|
3652
|
+
function resultTabShown(){ return document.getElementById("tab-result").getAttribute("aria-selected") === "true"; }
|
|
3653
|
+
function clearResult(){
|
|
3654
|
+
resultDetail = null; resultData = null;
|
|
3655
|
+
document.getElementById("result-body").hidden = true;
|
|
3656
|
+
document.getElementById("result-body").innerHTML = "";
|
|
3657
|
+
document.getElementById("result-empty").hidden = false;
|
|
3658
|
+
}
|
|
3659
|
+
// called by renderRun for every detail it draws: shows or removes the tab button, and the Run tab's pointer while the door is open
|
|
3660
|
+
function syncResultTab(detail){
|
|
3661
|
+
var btn = document.getElementById("tab-result");
|
|
3662
|
+
var show = detail.hasResult === true;
|
|
3663
|
+
if(!show && resultTabShown()) document.getElementById("tab-run").click();
|
|
3664
|
+
btn.hidden = !show;
|
|
3665
|
+
resultDetail = show ? detail : null;
|
|
3666
|
+
var ptr = document.getElementById("result-pointer");
|
|
3667
|
+
var waiting = show && detail.accept === true && !detail.live;
|
|
3668
|
+
ptr.hidden = !waiting;
|
|
3669
|
+
ptr.innerHTML = waiting ? 'the result is on the <button class="rs-link" type="button" data-testid="result-pointer-link">Result tab</button>' : "";
|
|
3670
|
+
var link = ptr.querySelector("button");
|
|
3671
|
+
if(link) link.addEventListener("click", function(){ btn.click(); });
|
|
3672
|
+
if(show && resultData && resultData.runid === detail.runid) renderResult();
|
|
3673
|
+
if(!show) clearResult();
|
|
3674
|
+
}
|
|
3675
|
+
function loadResult(runid){
|
|
3676
|
+
return getJSON("/api/runs/" + encodeURIComponent(runid) + "/result").then(function(data){
|
|
3677
|
+
if(runid !== currentRunid || !resultDetail || resultDetail.runid !== runid) return;
|
|
3678
|
+
resultData = data;
|
|
3679
|
+
renderResult();
|
|
3680
|
+
}).catch(function(){ /* the tab keeps what it last drew */ });
|
|
3681
|
+
}
|
|
3682
|
+
document.getElementById("tab-result").addEventListener("click", function(){ if(currentRunid) loadResult(currentRunid); });
|
|
3683
|
+
|
|
3684
|
+
// a RED run's own file words (the last file its check judged: "the file is gone" / "the file changed after the check") and the one note that says
|
|
3685
|
+
// what those rows are. A DELIVERED file's gone/changed words are said once, in the Saved card, never here.
|
|
3686
|
+
function resultFileWords(file){
|
|
3687
|
+
var note = file && file.judged && file.note ? '<div class="hint" data-testid="result-judged-note">' + escapeXml(file.note) + '</div>' : "";
|
|
3688
|
+
return note + (file && file.judged && file.words ? '<div class="rs-warn" data-testid="result-file-words">' + escapeXml(file.words) + '</div>' : "");
|
|
3689
|
+
}
|
|
3690
|
+
function resultField(label, inner){
|
|
3691
|
+
return '<div class="field"><label>' + escapeXml(label) + '</label>' + inner + '</div>';
|
|
3692
|
+
}
|
|
3693
|
+
// the numbered job line and its ~ rules, one box (the Job tab's "N. text" spelling, a space after the number)
|
|
3694
|
+
function resultLineBox(line){
|
|
3695
|
+
return resultField("The job line", '<div class="ro-value" data-testid="result-linetext">' + escapeXml(line.n + ". " + line.text) +
|
|
3696
|
+
(line.rules || []).map(function(r){ return '<br>~ ' + escapeXml(r); }).join("") + '</div>');
|
|
3697
|
+
}
|
|
3698
|
+
// no saved result to show: one line, never the file-state words (those are in Saved)
|
|
3699
|
+
var RESULT_NONE = "no saved result to show";
|
|
3700
|
+
// the proof quote, with the words the check read marked. The server sends the check's own text and offsets (`r.proof`: bareguard's
|
|
3701
|
+
// `full.evidence`, spans already chosen and merged); the page only cuts the text at those offsets, escapes every piece and marks.
|
|
3702
|
+
// It never searches a quote for a word. No proof (an old run, a quote the check did not read): the plain quote, unmarked.
|
|
3703
|
+
function resultProofHtml(r){
|
|
3704
|
+
var p = r.proof;
|
|
3705
|
+
if(!p || typeof p.text !== "string" || !Array.isArray(p.marks)) return escapeXml(r.quote);
|
|
3706
|
+
var h = "", at = 0;
|
|
3707
|
+
p.marks.forEach(function(m){
|
|
3708
|
+
h += escapeXml(p.text.slice(at, m[0])) + "<mark>" + escapeXml(p.text.slice(m[0], m[1])) + "</mark>";
|
|
3709
|
+
at = m[1];
|
|
3710
|
+
});
|
|
3711
|
+
return h + escapeXml(p.text.slice(at));
|
|
3712
|
+
}
|
|
3713
|
+
function resultFactsLine(line){
|
|
3714
|
+
var facts = line.facts || [];
|
|
3715
|
+
var n = facts.length;
|
|
3716
|
+
var ok = line.passed === true ? ' <span class="rs-ok" data-testid="result-ok">\u2713</span>' : "";
|
|
3717
|
+
var h = resultFileWords(line.file);
|
|
3718
|
+
if(Array.isArray(line.rows) && line.rows.length){
|
|
3719
|
+
h += resultField("Results (" + line.rows.length + ")", '<div class="ro-value" data-testid="result-results">' + line.rows.map(function(r){
|
|
3720
|
+
return '<div class="ro-value" data-testid="result-row" style="margin-bottom:8px">' +
|
|
3721
|
+
'<div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:6px"><b style="color:var(--text)">Result ' + r.n + '</b>' +
|
|
3722
|
+
(line.passed === true ? '<span class="rs-ok" data-testid="result-count">' + n + ' of ' + n + ' \u2713</span>' : "") + '</div>' +
|
|
3723
|
+
'<div class="field-row" style="margin-top:6px">' + r.cells.map(function(c, i){
|
|
3724
|
+
return '<div class="field"><label>' + escapeXml(facts[i] ? facts[i].name : "") + '</label><div class="ro-value" data-testid="result-cell">' + escapeXml(c) + ok + '</div></div>';
|
|
3725
|
+
}).join("") + '</div>' +
|
|
3726
|
+
'<details data-testid="result-proof"><summary>proof \u2014 the page\'s own words</summary><div class="ro-value" data-testid="result-quote" style="overflow-wrap:anywhere">' + resultProofHtml(r) + '</div></details></div>';
|
|
3727
|
+
}).join("") + '</div>');
|
|
3728
|
+
} else if(!(line.file && line.file.judged && line.file.words)){
|
|
3729
|
+
h += '<div class="hint" data-testid="result-nofile">' + RESULT_NONE + '</div>';
|
|
3730
|
+
}
|
|
3731
|
+
if(facts.length){
|
|
3732
|
+
h += resultField("The checks", '<div class="ro-value" data-testid="result-rules">' +
|
|
3733
|
+
facts.map(function(f){ return escapeXml(f.sentence) + (f.note ? ' ' + escapeXml(f.note) : ""); }).join("<br>") +
|
|
3734
|
+
(line.passed === true && Array.isArray(line.rows) && line.rows.length ? '<br><span class="hint">\u2713 = code checked it against the saved page</span>' : "") + '</div>');
|
|
3735
|
+
}
|
|
3736
|
+
return h;
|
|
3737
|
+
}
|
|
3738
|
+
function resultTextLine(line){
|
|
3739
|
+
var h = resultFileWords(line.file);
|
|
3740
|
+
if(typeof line.answer === "string") h += resultField("Answer", '<div class="ro-value" data-testid="result-answer" style="color:var(--text);white-space:pre-wrap">' + escapeXml(line.answer) + '</div>');
|
|
3741
|
+
else if(!(line.file && line.file.judged && line.file.words)) h += '<div class="hint" data-testid="result-nofile">' + RESULT_NONE + '</div>';
|
|
3742
|
+
(line.cites || []).forEach(function(c){
|
|
3743
|
+
h += resultField("Quotes from " + c.label + " (" + c.items.length + ")", '<details data-testid="result-cites"><summary>show</summary><div class="ro-value" style="white-space:pre-wrap">' +
|
|
3744
|
+
c.items.map(function(it){ return '<b>' + escapeXml(it.claim) + '</b>\n' + escapeXml(it.quote); }).join("\n\n") + '</div></details>');
|
|
3745
|
+
});
|
|
3746
|
+
return h;
|
|
3747
|
+
}
|
|
3748
|
+
// a list, one row per item (the Job tab lists the same items one per row)
|
|
3749
|
+
function resultNotChecked(label, items, testid){
|
|
3750
|
+
if(!items || !items.length) return "";
|
|
3751
|
+
return resultField(label, '<div class="ro-value" data-testid="' + testid + '"><ul style="margin:0;padding-left:18px">' +
|
|
3752
|
+
items.map(function(t){ return '<li>' + escapeXml(t) + '</li>'; }).join("") + '</ul></div>');
|
|
3753
|
+
}
|
|
3754
|
+
// the web page a result was read from: a link (http/https only, new tab) and the server's words that the page may have moved on
|
|
3755
|
+
function resultPageLink(url, note, testid){
|
|
3756
|
+
if(typeof url !== "string" || !/^https?:\/\//i.test(url)) return "";
|
|
3757
|
+
return resultField("Page read", '<div class="ro-value" data-testid="' + testid + '" style="overflow-wrap:anywhere"><a href="' + escapeXml(url) +
|
|
3758
|
+
'" target="_blank" rel="noopener noreferrer">' + escapeXml(url) + '</a><div class="hint" data-testid="result-page-note">' + escapeXml(note || "") + '</div></div>');
|
|
3759
|
+
}
|
|
3760
|
+
// `own` = the Result tab read 2+ pages, so this line's box shows its own page
|
|
3761
|
+
function resultLineCard(line, own, note){
|
|
3762
|
+
return '<div class="job-card" data-testid="result-line"><h4>Line ' + escapeXml(String(line.n)) + '</h4>' + resultLineBox(line) +
|
|
3763
|
+
(own ? resultPageLink(line.page, note, "result-page-line") : "") +
|
|
3764
|
+
(line.kind === "facts" ? resultFactsLine(line) : resultTextLine(line)) +
|
|
3765
|
+
resultNotChecked("Not checked", line.notChecked, "result-line-notchecked") + '</div>';
|
|
3766
|
+
}
|
|
3767
|
+
function resultDoorHtml(door){
|
|
3768
|
+
return '<div class="job-card" id="result-door-card" data-testid="result-door"><h4>Your call</h4><div class="field"><div class="ro-value"><b class="st-word">' + escapeXml(door.head) + '</b> \u2014 ' + escapeXml(door.ask) + '</div></div>' +
|
|
3769
|
+
'<div class="run-actions" style="margin:6px 0 0;"><button class="btn primary" type="button" data-testid="result-accept">Accept</button>' +
|
|
3770
|
+
'<button class="btn" type="button" data-testid="result-rerun-open">Rerun with my words</button>' +
|
|
3771
|
+
'<button class="btn" type="button" data-testid="result-pause">Pause</button></div>' +
|
|
3772
|
+
'<div id="result-rerun-box" data-testid="result-rerun-box" hidden><textarea id="result-rerun-text" data-testid="result-rerun-text" aria-label="your words for the rerun" placeholder="e.g. only morning flights, and show the airline"></textarea>' +
|
|
3773
|
+
'<button class="btn" type="button" data-testid="result-rerun" disabled>Rerun</button></div>' +
|
|
3774
|
+
'<div id="result-door-msg" data-testid="result-door-msg"></div></div>';
|
|
3775
|
+
}
|
|
3776
|
+
function wireResultDoor(body, detail){
|
|
3777
|
+
var msg = body.querySelector("#result-door-msg");
|
|
3778
|
+
var accept = body.querySelector('[data-testid="result-accept"]');
|
|
3779
|
+
if(!accept) return;
|
|
3780
|
+
var say = function(cls, text, testid){ msg.innerHTML = '<span class="' + cls + '" data-testid="' + testid + '">' + escapeXml(text) + '</span>'; };
|
|
3781
|
+
accept.addEventListener("click", function(){ msg.innerHTML = ""; acceptDoor(detail, accept, msg); });
|
|
3782
|
+
var box = body.querySelector("#result-rerun-box");
|
|
3783
|
+
var txt = body.querySelector("#result-rerun-text");
|
|
3784
|
+
var go = body.querySelector('[data-testid="result-rerun"]');
|
|
3785
|
+
body.querySelector('[data-testid="result-rerun-open"]').addEventListener("click", function(){ box.hidden = !box.hidden; if(!box.hidden) txt.focus(); });
|
|
3786
|
+
// required: the button stays off while the words are empty, and the click re-checks (the engine refuses empty words too)
|
|
3787
|
+
txt.addEventListener("input", function(){ go.disabled = txt.value.trim() === ""; });
|
|
3788
|
+
go.addEventListener("click", function(){
|
|
3789
|
+
var words = txt.value.trim();
|
|
3790
|
+
if(words === ""){ say("resume-err", "Write what you want done differently, then press Rerun.", "result-door-err"); return; }
|
|
3791
|
+
go.disabled = true;
|
|
3792
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/rerun", {text: words}).then(function(res){
|
|
3793
|
+
if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); afterResumeRefresh(detail.runid, 6); return; }
|
|
3794
|
+
go.disabled = false;
|
|
3795
|
+
say("resume-err", (res.body && res.body.error) || ("refused (" + res.status + ")"), "result-door-err");
|
|
3796
|
+
}).catch(function(x){ go.disabled = false; say("resume-err", "could not reach the panel: " + x.message, "result-door-err"); });
|
|
3797
|
+
});
|
|
3798
|
+
var pause = body.querySelector('[data-testid="result-pause"]');
|
|
3799
|
+
pause.addEventListener("click", function(){
|
|
3800
|
+
pause.disabled = true;
|
|
3801
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/pause", {}).then(function(res){
|
|
3802
|
+
pause.disabled = false;
|
|
3803
|
+
if(res.status === 200 && res.body && res.body.ok){ say("stopping", "Paused. Nothing was spent; this stays open for you to answer later.", "result-door-paused"); return; }
|
|
3804
|
+
if(res.status === 202 && res.body && res.body.pending){ say("stopping", "still recording the pause; look again in a moment", "result-door-paused"); return; }
|
|
3805
|
+
say("resume-err", (res.body && res.body.error) || ("refused (" + res.status + ")"), "result-door-err");
|
|
3806
|
+
}).catch(function(x){ pause.disabled = false; say("resume-err", "could not reach the panel: " + x.message, "result-door-err"); });
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3809
|
+
function renderResult(){
|
|
3810
|
+
var detail = resultDetail, data = resultData;
|
|
3811
|
+
var body = document.getElementById("result-body");
|
|
3812
|
+
if(!detail || !data || data.runid !== detail.runid){ return; }
|
|
3813
|
+
document.getElementById("result-empty").hidden = true;
|
|
3814
|
+
body.hidden = false;
|
|
3815
|
+
// keep what the person is typing across a poll-driven redraw
|
|
3816
|
+
var keepText = body.querySelector("#result-rerun-text");
|
|
3817
|
+
var typed = keepText ? keepText.value : "";
|
|
3818
|
+
var rerunOpen = !!body.querySelector("#result-rerun-box:not([hidden])");
|
|
3819
|
+
var counters = document.getElementById("run-counters").textContent.split(" \u00b7 ");
|
|
3820
|
+
var h = '<div class="job-card" data-testid="result-head"><h4>Result</h4>' +
|
|
3821
|
+
resultField("Status", '<div class="ro-value">' + stateDotHTML(glyphClass(detail.glyph), detail.status && detail.status.word ? detail.status.word : "", "result-status") +
|
|
3822
|
+
' \u00b7 ' + escapeXml(detail.checkType) + ' \u00b7 ' + escapeXml(runName(detail.job, detail.runNo, detail.runid)) + '</div>') +
|
|
3823
|
+
'<div class="field-row">' + resultField("Money", '<div class="ro-value" data-testid="result-counters">' + escapeXml(counters[0] || "") + '</div>') +
|
|
3824
|
+
resultField("Time", '<div class="ro-value">' + escapeXml((counters[1] || "").replace(/^took /, "")) + '</div>') + '</div>' +
|
|
3825
|
+
((data.pages || []).length === 1 ? resultPageLink(data.pages[0], data.pageNote, "result-page-top") : "") + '</div>';
|
|
3826
|
+
var ownPages = (data.pages || []).length >= 2;
|
|
3827
|
+
(data.lines || []).forEach(function(line){ h += resultLineCard(line, ownPages, data.pageNote); });
|
|
3828
|
+
if(data.gap){
|
|
3829
|
+
h += '<div class="rs-warn" data-testid="result-gap">The check said: ' + escapeXml(data.gap.line) + (data.gap.moreText ? ' ' + escapeXml(data.gap.moreText) : "") + '</div>';
|
|
3830
|
+
}
|
|
3831
|
+
if((data.notChecked || []).length){
|
|
3832
|
+
h += '<div class="job-card"><h4>Not checked</h4>' + resultNotChecked("Not checked (the AI's own reading)", data.notChecked, "result-notchecked") + '</div>';
|
|
3833
|
+
}
|
|
3834
|
+
if(detail.accept === true && !detail.live) h += resultDoorHtml(data.door);
|
|
3835
|
+
if((data.delivered || []).length){
|
|
3836
|
+
h += '<div class="job-card" data-testid="result-saved"><h4>Saved</h4>' + data.delivered.map(function(d){
|
|
3837
|
+
return resultField(d.name, '<div class="ro-value" data-testid="result-path" style="overflow-wrap:anywhere">' + escapeXml(d.path) + '</div>' +
|
|
3838
|
+
(d.words ? '<div class="rs-warn" data-testid="result-file-gone">' + escapeXml(d.words) + '</div>'
|
|
3839
|
+
: '<div class="run-actions" style="margin:6px 0 0;"><a class="btn" style="text-decoration:none;" data-testid="result-download" download href="/api/runs/' + encodeURIComponent(data.runid) + '/download?i=' + d.i + '">Download ' + escapeXml(d.name) + '</a></div>'));
|
|
3840
|
+
}).join("") + '</div>';
|
|
3841
|
+
}
|
|
3842
|
+
body.innerHTML = h;
|
|
3843
|
+
var ta = body.querySelector("#result-rerun-text");
|
|
3844
|
+
if(ta){
|
|
3845
|
+
ta.value = typed;
|
|
3846
|
+
if(rerunOpen){ body.querySelector("#result-rerun-box").hidden = false; body.querySelector('[data-testid="result-rerun"]').disabled = typed.trim() === ""; }
|
|
3847
|
+
wireResultDoor(body, detail);
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3453
3851
|
// ---- selection + loading ----
|
|
3454
3852
|
var currentRunid = null;
|
|
3455
3853
|
// P2: guards a stale in-flight response (detail/audit/job, OR the poll
|
|
@@ -3476,6 +3874,7 @@
|
|
|
3476
3874
|
runIsLive = false;
|
|
3477
3875
|
openIsScheduled = false;
|
|
3478
3876
|
lastOpenFetchAt = Date.now();
|
|
3877
|
+
clearResult();
|
|
3479
3878
|
var myToken = ++selectToken;
|
|
3480
3879
|
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(detail){
|
|
3481
3880
|
if(myToken !== selectToken) return; // a different run was selected before this resolved
|
|
@@ -3483,6 +3882,7 @@
|
|
|
3483
3882
|
runIsLive = detail.glyph === "▶" && !detail.died;
|
|
3484
3883
|
openIsScheduled = !!(detail.schedule && detail.schedule.scheduled);
|
|
3485
3884
|
renderRun(detail);
|
|
3885
|
+
if(detail.hasResult === true) loadResult(runid);
|
|
3486
3886
|
}).catch(function(e){
|
|
3487
3887
|
if(myToken !== selectToken) return;
|
|
3488
3888
|
document.getElementById("run-empty").hidden = false;
|