bareloop 0.42.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 +88 -0
- package/bareloop.context.md +103 -41
- package/package.json +2 -2
- package/src/authorflow.js +26 -11
- package/src/authoring.js +1 -1
- package/src/authorjob.js +33 -15
- package/src/authorreadout.js +7 -6
- package/src/declaredclose.js +6 -3
- 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/interviewrun.js +0 -1
- package/src/job.js +39 -12
- 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 +138 -60
- package/src/panel/authorsession.js +88 -32
- package/src/panel/index.html +1251 -522
- package/src/panel/jobplan.js +4 -2
- package/src/panel/runroutes.js +188 -6
- package/src/panel/scheduler.js +156 -0
- package/src/panel/schedules.js +667 -0
- package/src/panel/schedulestart.js +243 -0
- package/src/panel/server.js +1234 -68
- package/src/panel/status.js +33 -0
- package/src/panelmoney.js +29 -0
- package/src/planrun.js +92 -11
- package/src/promptregisters.js +4 -0
- package/src/providerrows.js +3 -3
- package/src/replay.js +118 -7
- package/src/reviewdoor.js +37 -0
- package/src/rubricstep.js +123 -11
- package/src/runlist.js +37 -1
- package/src/schedulewords.js +299 -0
- package/src/source.js +18 -0
- package/src/thinking.js +63 -2
- package/src/trigger.js +48 -0
- package/src/triggerclock.js +127 -0
- package/src/triggerfiles.js +215 -0
- package/src/triggerledger.js +79 -0
- package/src/userrun.js +34 -21
- package/types/authorflow.d.ts +42 -6
- package/types/authorjob.d.ts +10 -2
- package/types/declaredclose.d.ts +3 -1
- 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/job.d.ts +8 -1
- 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 +50 -14
- package/types/panel/authorsession.d.ts +37 -12
- package/types/panel/runroutes.d.ts +32 -1
- package/types/panel/scheduler.d.ts +17 -0
- package/types/panel/schedules.d.ts +412 -0
- package/types/panel/schedulestart.d.ts +106 -0
- package/types/panel/server.d.ts +297 -15
- package/types/panel/status.d.ts +51 -0
- package/types/panelmoney.d.ts +5 -0
- package/types/planrun.d.ts +2 -0
- package/types/providerrows.d.ts +2 -2
- package/types/replay.d.ts +21 -1
- package/types/reviewdoor.d.ts +27 -0
- package/types/rubricstep.d.ts +57 -1
- package/types/runlist.d.ts +24 -1
- package/types/schedulewords.d.ts +137 -0
- package/types/source.d.ts +11 -0
- package/types/thinking.d.ts +39 -0
- package/types/trigger.d.ts +37 -0
- package/types/triggerclock.d.ts +52 -0
- package/types/triggerfiles.d.ts +93 -0
- package/types/triggerledger.d.ts +35 -0
- package/types/userrun.d.ts +4 -0
package/src/panel/index.html
CHANGED
|
@@ -144,8 +144,8 @@
|
|
|
144
144
|
.dot::before{content:"[·]";color:var(--grey-dot);}
|
|
145
145
|
.dot.green::before{content:"[✓]";color:var(--green);}
|
|
146
146
|
.dot.red::before{content:"[✗]";color:var(--red);}
|
|
147
|
-
/* stopped (hamr 2026-10-07): the person pressed Stop — its own sign,
|
|
148
|
-
.dot.stopped::before{content:"[■]";color:var(--
|
|
147
|
+
/* stopped (hamr 2026-10-07): the person pressed Stop — its own sign, grey (RULED 2026-10-15) */
|
|
148
|
+
.dot.stopped::before{content:"[■]";color:var(--grey-dot);}
|
|
149
149
|
/* Panel build P3 addition (2026-09-27, "yes, go"): the part cards below the
|
|
150
150
|
step map (`partResultGlyph`/`attemptGlyph`) and the chat job card render
|
|
151
151
|
their result glyph as PLAIN inline text inside a larger string (never
|
|
@@ -170,9 +170,11 @@
|
|
|
170
170
|
1.2s, matching design/panel-mockup.html's pulsing amber dot exactly —
|
|
171
171
|
one fixed rhythm, never re-derived per call site. */
|
|
172
172
|
@keyframes pulse-glyph{0%,100%{opacity:1;}50%{opacity:0.3;}}
|
|
173
|
-
.dot.amber::before{content:"[▶]";color:var(--amber);
|
|
173
|
+
.dot.amber::before{content:"[▶]";color:var(--amber);}
|
|
174
|
+
/* RULED 2026-10-15 (matches fwdloop glyphPulses): a sign blinks only while running or waiting for your Accept, keyed to `.pulse` (dotPulses decides) */
|
|
175
|
+
.dot.pulse::before{animation:pulse-glyph 1.2s ease-in-out infinite;}
|
|
174
176
|
@media (prefers-reduced-motion: reduce){
|
|
175
|
-
.dot.
|
|
177
|
+
.dot.pulse::before{animation:none;}
|
|
176
178
|
}
|
|
177
179
|
.dot.grey::before{content:"[·]";color:var(--grey-dot);}
|
|
178
180
|
/* died (hamr's ruling B, 2026-09-25): no ending was ever recorded — a
|
|
@@ -208,12 +210,12 @@
|
|
|
208
210
|
.btn:disabled::before,.btn:disabled::after{color:var(--text-faint);opacity:0.4;}
|
|
209
211
|
.btn.small{padding:0 9px;height:32px;min-height:0;font-size:12px;}
|
|
210
212
|
|
|
211
|
-
.wf-row
|
|
213
|
+
.wf-row{
|
|
212
214
|
display:flex;flex-direction:column;row-gap:4px;min-width:0;
|
|
213
215
|
padding:8px 8px 8px 18px;position:relative;border:1px solid var(--border);margin-bottom:6px;background:var(--panel2);cursor:pointer;
|
|
214
216
|
}
|
|
215
217
|
.wf-row::before{content:" ";position:absolute;left:6px;top:8px;color:var(--accent);font-weight:700;}
|
|
216
|
-
.wf-row:hover
|
|
218
|
+
.wf-row:hover{border-color:var(--border-strong);}
|
|
217
219
|
.wf-row.selected::before{content:">";}
|
|
218
220
|
/* line 1: glyph + name, kept on ONE line — truncated with an ellipsis
|
|
219
221
|
(never wrapped) when the job/runid text is longer than the row; the
|
|
@@ -226,15 +228,42 @@
|
|
|
226
228
|
.wf-meta{font-size:12px;color:var(--text-dim);}
|
|
227
229
|
.wf-ended{display:block;font-size:12px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
228
230
|
.wf-meta + .wf-meta::before{content:" · ";color:var(--text-faint);}
|
|
229
|
-
.wf-row.selected
|
|
230
|
-
/* item 2 (2026-09-26 merge): a Workflows-view job row expands inline to
|
|
231
|
-
list ALL its runs (build spec's own design) — the caret glyph on the
|
|
232
|
-
job row, and the indent wrapper the expanded runs render into. */
|
|
233
|
-
.wf-job{margin-bottom:6px;}
|
|
234
|
-
.wf-job .wf-row{margin-bottom:0;}
|
|
231
|
+
.wf-row.selected{border:2px solid var(--accent);border-radius:4px;}
|
|
235
232
|
.wf-caret{display:inline-block;width:11px;flex:none;color:var(--text-faint);font-size:10px;}
|
|
236
|
-
|
|
237
|
-
.
|
|
233
|
+
|
|
234
|
+
/* P9 v2: the one runs list. Copied from fwdloop M11 (index.html), values unchanged; only the class names carry the rl- prefix. A card's lines clip with an ellipsis (the full text is on the run page). */
|
|
235
|
+
:root{--rl-accent-bg:rgba(122,162,247,.12);--rl-accent-bg-strong:rgba(122,162,247,.2);}
|
|
236
|
+
.rl-card{display:block;width:100%;text-align:left;background:var(--panel2);border:1px solid var(--border);padding:6px 8px;margin:0 0 6px;cursor:pointer;min-height:44px;color:inherit;font:inherit;}
|
|
237
|
+
div.rl-card{cursor:default;}
|
|
238
|
+
.rl-card:hover{border-color:var(--accent);background:var(--rl-accent-bg);}
|
|
239
|
+
div.rl-card:hover{border-color:var(--border);background:var(--panel2);}
|
|
240
|
+
.rl-card.rl-sel,div.rl-card.rl-sel:hover{border:2px solid var(--accent);border-radius:4px;background:var(--rl-accent-bg-strong);}
|
|
241
|
+
.rl-card:focus-visible,.rl-fold:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
|
|
242
|
+
.rl-card.rl-live{border-color:var(--amber);}
|
|
243
|
+
.rl-card.rl-need{border-color:var(--accent);}
|
|
244
|
+
.rl-l1{display:flex;gap:6px;align-items:center;min-width:0;overflow:hidden;}
|
|
245
|
+
.rl-nm{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
246
|
+
.rl-rn{color:var(--accent);flex:none;}
|
|
247
|
+
.rl-l2{display:block;font-size:12px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
248
|
+
.rl-l2 .st-word{color:var(--text);}
|
|
249
|
+
.rl-faint{color:var(--text-faint);font-size:12px;}
|
|
250
|
+
.rl-rest{color:var(--text-faint);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
251
|
+
.rl-cnt{color:var(--text-faint);font-size:inherit;flex:none;}
|
|
252
|
+
.rl-runs{margin:6px 0 0 6px;padding-left:8px;border-left:2px solid var(--border);}
|
|
253
|
+
.rl-runs .rl-card{background:var(--bg);}
|
|
254
|
+
.rl-runs .rl-card.rl-sel{background:var(--rl-accent-bg-strong);}
|
|
255
|
+
.rl-fold{background:none;border:0;color:var(--text-dim);font:inherit;font-size:12px;padding:4px 0;cursor:pointer;min-height:32px;}
|
|
256
|
+
.rl-fold:hover{color:var(--accent);}
|
|
257
|
+
.rl-hint{font-size:12px;color:var(--text-faint);padding:2px 0 6px;}
|
|
258
|
+
.rl-pill{font-size:11px;border:1px solid var(--amber);color:var(--amber);padding:0 5px;margin-left:auto;flex:none;}
|
|
259
|
+
.rl-pill-need{border-color:var(--accent);color:var(--accent);}
|
|
260
|
+
.rl-dash{border-top:1px dashed var(--border-strong);margin:10px 0 6px;}
|
|
261
|
+
.rl-sect{font-size:12px;color:var(--text-dim);font-weight:700;margin:12px 0 6px;}
|
|
262
|
+
.rl-sect:first-child{margin-top:0;}
|
|
263
|
+
#panel-runs .rl-faint{margin-bottom:4px;}
|
|
264
|
+
@media (max-width:899px){ .rl-fold{min-height:44px;} }
|
|
265
|
+
/* the one thing P9 v2 adds to fwd: a run that matches the search or filter is marked, not selected */
|
|
266
|
+
.rl-card.rl-hit{box-shadow:inset 3px 0 0 var(--accent);}
|
|
238
267
|
|
|
239
268
|
.rp-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:2px solid var(--border-strong);background:var(--panel);flex-wrap:wrap;gap:8px;}
|
|
240
269
|
.rp-header h2{margin:0;font-size:15px;}
|
|
@@ -243,7 +272,7 @@
|
|
|
243
272
|
are already both on screen there, nothing to jump back to). */
|
|
244
273
|
.back-to-list-link{display:none;font-size:12px;color:var(--accent);text-decoration:underline;background:none;border:none;padding:0;cursor:pointer;}
|
|
245
274
|
@media (max-width: 899px){ .back-to-list-link{display:inline-block;} }
|
|
246
|
-
.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;}
|
|
247
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;}
|
|
248
277
|
.rp-tabrow button::before{content:"[" counter(rtabnum) "] ";color:var(--text-faint);font-weight:700;}
|
|
249
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);}
|
|
@@ -257,6 +286,7 @@
|
|
|
257
286
|
.ended-block.magenta{border-color:var(--magenta);}
|
|
258
287
|
.ended-block.red{border-color:var(--red);}
|
|
259
288
|
.ended-block.green{border-color:var(--green);}
|
|
289
|
+
.ended-block.grey{border-color:var(--grey-dot);}
|
|
260
290
|
.ended-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;margin-bottom:6px;}
|
|
261
291
|
.ended-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:52px;}
|
|
262
292
|
.ended-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
@@ -293,15 +323,19 @@
|
|
|
293
323
|
.rw-picked{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:6px;padding:6px 8px;border:1px solid var(--accent);}
|
|
294
324
|
.rw-picked span{overflow-wrap:anywhere;}
|
|
295
325
|
.rw-picked a{font-size:12px;cursor:pointer;flex:none;}
|
|
326
|
+
/* T Part 7: the SCHEDULED / NEXT twin of the ENDED block, and the note under both */
|
|
327
|
+
/* RULED 2026-10-15 (aligned with fwdloop M9 am7): a schedule that started nothing turns its whole SCHEDULE row red; the frame turns red with `.ended-block.red` */
|
|
328
|
+
.ended-row.is-red .ended-label,.ended-row.is-red .ended-text,.ended-text.is-red{color:var(--red);}
|
|
329
|
+
.ended-text .sched-line2{display:block;}
|
|
296
330
|
.run-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
|
|
297
331
|
.run-actions[hidden]{display:none;}
|
|
298
332
|
.run-actions .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
299
333
|
.resume-err{color:var(--red);white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
300
334
|
/* P5-R: a resumed run is ONE card; its legs are told apart by a divider line, a dotted map connector, a small tag */
|
|
301
|
-
.wf-resumed{color:var(--amber);}
|
|
302
335
|
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
303
336
|
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
304
337
|
.summary-box.red{border-color:var(--red);}
|
|
338
|
+
.summary-box.grey{border-color:var(--grey-dot);}
|
|
305
339
|
.summary-box.amber{border-color:var(--amber);}
|
|
306
340
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
307
341
|
.summary-box::before{content:"┤ SUMMARY ├";position:absolute;top:-10px;left:10px;background:var(--panel2);padding:0 6px;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;}
|
|
@@ -331,6 +365,7 @@
|
|
|
331
365
|
.map-chip.s-done{border-color:var(--green);color:var(--text);} .map-chip.s-done .chip-sign{color:#2e7d32;}
|
|
332
366
|
.map-chip.s-running{border-color:var(--amber);color:var(--text);} .map-chip.s-running .chip-sign{color:#b8860b;}
|
|
333
367
|
.map-chip.s-stopped{border-color:var(--red);color:var(--text);} .map-chip.s-stopped .chip-sign{color:var(--red);}
|
|
368
|
+
.map-chip.s-stopsign{border-color:var(--grey-dot);} .map-chip.s-stopsign .chip-sign{color:var(--grey-dot);} /* RULED 2026-10-15: a step you stopped is grey, not red */
|
|
334
369
|
.map-chip.s-died{border-color:var(--magenta);color:var(--text);} .map-chip.s-died .chip-sign{color:var(--magenta);}
|
|
335
370
|
.map-chip.retry{border-style:dashed;}
|
|
336
371
|
.map-chip.resumed-next{border-right-style:dotted;}
|
|
@@ -359,6 +394,27 @@
|
|
|
359
394
|
one) covers every table inside that wrapper, Flat's included. */
|
|
360
395
|
.audit-table-scroll table th{position:sticky;top:0;z-index:1;}
|
|
361
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;}
|
|
362
418
|
.rounds-page-info{margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
363
419
|
|
|
364
420
|
/* build item C: Audit tab grouped-by-part rows */
|
|
@@ -401,6 +457,13 @@
|
|
|
401
457
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
402
458
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
403
459
|
.field{margin-bottom:10px;}
|
|
460
|
+
/* T Part 7: the When field (sub-boxes show and hide; 44px controls) */
|
|
461
|
+
.when-sub{display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;}
|
|
462
|
+
.when-sub[hidden]{display:none;}
|
|
463
|
+
.when-sub select,.when-sub input{flex:1 1 120px;min-width:0;width:auto;min-height:44px;}
|
|
464
|
+
#jf-when{min-height:44px;}
|
|
465
|
+
.jf-err{color:var(--red);font-size:12px;overflow-wrap:anywhere;}
|
|
466
|
+
.jf-err:empty{display:none;}
|
|
404
467
|
.field input,.field select{width:100%;}
|
|
405
468
|
label{display:block;font-size:11px;font-weight:700;color:var(--text-dim);margin-bottom:2px;text-transform:uppercase;letter-spacing:0.04em;}
|
|
406
469
|
.ro-value{padding:6px 8px;min-height:32px;box-sizing:border-box;border:1px solid var(--border);background:var(--bg);color:var(--text-dim);}
|
|
@@ -628,6 +691,7 @@
|
|
|
628
691
|
.topbar{flex-direction:column;align-items:flex-start;}
|
|
629
692
|
.topbar-right{width:100%;justify-content:space-between;}
|
|
630
693
|
.btn.small{height:44px;min-height:44px;padding:0 12px;}
|
|
694
|
+
.rs-link{min-height:44px;}
|
|
631
695
|
.chip{min-height:44px;}
|
|
632
696
|
.chip.fchip{min-height:40px;min-width:40px;}
|
|
633
697
|
.rp-header{margin:8px 8px 0;}
|
|
@@ -670,6 +734,7 @@
|
|
|
670
734
|
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
671
735
|
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
672
736
|
<div class="origin-line" id="chat-origin-line" data-testid="origin-line" hidden></div>
|
|
737
|
+
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
673
738
|
|
|
674
739
|
<div class="field">
|
|
675
740
|
<label id="jf-verdict-label">Check type</label>
|
|
@@ -684,7 +749,6 @@
|
|
|
684
749
|
<ul class="rw-list" id="rw-list" data-testid="rw-list"></ul>
|
|
685
750
|
</div>
|
|
686
751
|
<div class="rw-picked" id="rw-picked" data-testid="rw-picked" hidden><span id="rw-picked-txt" data-testid="rw-picked-txt"></span><a id="rw-change" data-testid="rw-change" role="button" tabindex="0">change</a></div>
|
|
687
|
-
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
688
752
|
</div>
|
|
689
753
|
</div>
|
|
690
754
|
<div class="field">
|
|
@@ -720,6 +784,24 @@
|
|
|
720
784
|
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
721
785
|
</div>
|
|
722
786
|
|
|
787
|
+
<!-- T Part 7: the When field, only on the card opened by Schedule run / Cancel-modify schedule. Every word is filled from the server's table. -->
|
|
788
|
+
<div class="field" id="jf-when-field" data-testid="jf-when-field" hidden>
|
|
789
|
+
<label for="jf-when" id="jf-when-label">When</label>
|
|
790
|
+
<select id="jf-when" data-testid="jf-when" aria-label="When this job runs"></select>
|
|
791
|
+
<div class="when-sub" id="jf-when-clock" hidden>
|
|
792
|
+
<select id="jf-when-day" data-testid="jf-when-day" aria-label="Day of the week">
|
|
793
|
+
<option value="mon">Monday</option><option value="tue">Tuesday</option><option value="wed">Wednesday</option><option value="thu">Thursday</option>
|
|
794
|
+
<option value="fri">Friday</option><option value="sat">Saturday</option><option value="sun">Sunday</option>
|
|
795
|
+
</select>
|
|
796
|
+
<input id="jf-when-time" type="time" value="09:00" data-testid="jf-when-time" aria-label="Time, HH:MM">
|
|
797
|
+
</div>
|
|
798
|
+
<div class="when-sub" id="jf-when-filebox" hidden>
|
|
799
|
+
<input id="jf-when-path" type="text" autocomplete="off" spellcheck="false" data-testid="jf-when-path" aria-label="File or folder">
|
|
800
|
+
</div>
|
|
801
|
+
<div class="hint" id="jf-when-now" data-testid="jf-when-now"></div>
|
|
802
|
+
<div class="jf-err" id="jf-err-when" data-testid="jf-err-when"></div>
|
|
803
|
+
</div>
|
|
804
|
+
|
|
723
805
|
<div class="cap-row" data-testid="cap-row">
|
|
724
806
|
<div class="cap-field">
|
|
725
807
|
<label for="jf-cap-money">Cap $</label>
|
|
@@ -765,9 +847,7 @@
|
|
|
765
847
|
</section>
|
|
766
848
|
|
|
767
849
|
<section id="panel-runs" class="tabpanel" role="tabpanel" aria-labelledby="tab-runs" hidden>
|
|
768
|
-
<div
|
|
769
|
-
<button class="chip" type="button" aria-pressed="true" data-runs-view="workflows" data-testid="runs-view-workflows">Workflows</button>
|
|
770
|
-
<button class="chip" type="button" aria-pressed="false" data-runs-view="history" data-testid="runs-view-history">History</button>
|
|
850
|
+
<div style="margin-bottom:8px;">
|
|
771
851
|
<button class="btn small" type="button" id="wf-import" data-testid="wf-import" title="look at an exported job folder (read only)">Import</button>
|
|
772
852
|
</div>
|
|
773
853
|
<div class="import-box" id="import-box" data-testid="import-box" hidden>
|
|
@@ -784,10 +864,13 @@
|
|
|
784
864
|
<button class="btn" type="button" id="import-cancel" data-testid="import-cancel">Cancel</button>
|
|
785
865
|
</div>
|
|
786
866
|
</div>
|
|
867
|
+
<div id="rl-top" data-testid="rl-top"></div>
|
|
868
|
+
<div class="rl-dash"></div>
|
|
869
|
+
<div class="rl-faint" data-testid="rl-filter-note">search also finds Scheduled; ◷ shows only Scheduled</div>
|
|
787
870
|
<div id="runs-filterbar-anchor"></div>
|
|
871
|
+
<div id="rl-sched" data-testid="rl-sched"></div>
|
|
788
872
|
<div id="wf-list" data-testid="wf-list"></div>
|
|
789
873
|
<div id="import-list" data-testid="import-list"></div>
|
|
790
|
-
<div id="history-list" data-testid="history-list" hidden></div>
|
|
791
874
|
</section>
|
|
792
875
|
|
|
793
876
|
</div>
|
|
@@ -807,15 +890,21 @@
|
|
|
807
890
|
<button role="tab" id="tab-run" aria-controls="panel-run" aria-selected="true" data-testid="right-tab-run">Run</button>
|
|
808
891
|
<button role="tab" id="tab-audit" aria-controls="panel-audit" aria-selected="false" data-testid="right-tab-audit">Audit / logs</button>
|
|
809
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>
|
|
810
894
|
</div>
|
|
811
895
|
|
|
812
896
|
<div class="pane-body" id="right-pane-body">
|
|
813
897
|
|
|
814
898
|
<section id="panel-run" class="tabpanel active" role="tabpanel" aria-labelledby="tab-run">
|
|
815
899
|
<div class="import-view" id="import-view" data-testid="import-view" hidden></div>
|
|
816
|
-
<div id="run-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run from
|
|
900
|
+
<div id="run-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run from the Runs list</div>
|
|
817
901
|
<div id="run-content" hidden>
|
|
902
|
+
<!-- RULED 2026-10-15: ONE summary block: ENDED, NEXT, SCHEDULE (fwd #summary-blocks, aligned with M9 am7) -->
|
|
903
|
+
<div id="summary-blocks" data-testid="summary-blocks">
|
|
904
|
+
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
905
|
+
</div>
|
|
818
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>
|
|
819
908
|
<div class="counters" id="run-counters" data-testid="live-counters"></div>
|
|
820
909
|
<div class="summary-box" id="run-summary" data-testid="run-summary"></div>
|
|
821
910
|
<div class="map-box" data-testid="step-map" id="map-box"><div id="map-mount"></div></div>
|
|
@@ -826,7 +915,6 @@
|
|
|
826
915
|
<section id="panel-audit" class="tabpanel" role="tabpanel" aria-labelledby="tab-audit" hidden>
|
|
827
916
|
<div id="audit-select-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
828
917
|
<div id="audit-body" hidden>
|
|
829
|
-
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
830
918
|
<div class="audit-toolbar" style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
|
831
919
|
<div class="chips" role="group" aria-label="Filter audit rows" style="margin-bottom:0;">
|
|
832
920
|
<button class="chip" aria-pressed="true" data-filter="all" data-testid="chip-all">All</button>
|
|
@@ -842,6 +930,7 @@
|
|
|
842
930
|
<div id="audit-flat-wrap" hidden>
|
|
843
931
|
<div id="audit-empty" class="hint" data-testid="audit-empty" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
844
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>
|
|
845
934
|
<div class="audit-table-scroll">
|
|
846
935
|
<table data-testid="audit-table">
|
|
847
936
|
<thead><tr><th>Round</th><th>Step</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead>
|
|
@@ -862,7 +951,12 @@
|
|
|
862
951
|
<div class="job-card" id="job-card-readonly" hidden data-testid="job-card-readonly">
|
|
863
952
|
<h4 id="details-title">Job card — signed</h4>
|
|
864
953
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
865
|
-
<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>
|
|
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>
|
|
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>
|
|
866
960
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
867
961
|
<div class="field"><label>The job</label><div class="ro-value" id="details-plan" data-testid="details-plan">—</div></div>
|
|
868
962
|
<div class="field"><label>Inputs</label><div class="ro-value" id="details-inputs" data-testid="details-inputs">—</div></div>
|
|
@@ -877,6 +971,11 @@
|
|
|
877
971
|
</div>
|
|
878
972
|
</section>
|
|
879
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
|
+
|
|
880
979
|
</div>
|
|
881
980
|
</section>
|
|
882
981
|
|
|
@@ -1002,7 +1101,7 @@
|
|
|
1002
1101
|
});
|
|
1003
1102
|
}
|
|
1004
1103
|
wireTabs([document.getElementById("tab-chat"), document.getElementById("tab-runs")]);
|
|
1005
|
-
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")]);
|
|
1006
1105
|
|
|
1007
1106
|
// ---- fetch helpers ----
|
|
1008
1107
|
function getJSON(path){
|
|
@@ -1102,7 +1201,7 @@
|
|
|
1102
1201
|
// result when nothing was ever judged there; `step`/`fix` (real attempts)
|
|
1103
1202
|
// and `run` (its own real, non-null outcome) are the only kinds worth an
|
|
1104
1203
|
// inline attempt/result glyph.
|
|
1105
|
-
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"; }
|
|
1106
1205
|
// ONE shared result-glyph rule for both the Run tab's cards and the Audit
|
|
1107
1206
|
// tab's grouped-row headers — `null` (never a fabricated "?") for a
|
|
1108
1207
|
// no-verdict part kind; a multi-attempt part (a step with retries, or the
|
|
@@ -1167,7 +1266,7 @@
|
|
|
1167
1266
|
var sign = s.status && s.status.sign ? s.status.sign : "";
|
|
1168
1267
|
var tries = boxRetryTry(s);
|
|
1169
1268
|
var cls = "map-chip s-" + String(s.state).replace(/[^a-z-]/g, "") +
|
|
1170
|
-
(tries ? " retry" : "") + (s.resumedNext ? " resumed-next" : "");
|
|
1269
|
+
(tries ? " retry" : "") + (sign === "\u25A0" ? " s-stopsign" : "") + (s.resumedNext ? " resumed-next" : "");
|
|
1171
1270
|
var label = escapeXml(stepTitleText(numIdx[idx], s));
|
|
1172
1271
|
var blocked = s.blocked > 0 ? ' <span class="chip-blocked">' + s.blocked + ' blocked</span>' : '';
|
|
1173
1272
|
var retry = tries ? '<span class="chip-retry">\u21bb' + tries + '</span> ' : '';
|
|
@@ -1259,6 +1358,10 @@
|
|
|
1259
1358
|
function realSteps(steps){ return steps.filter(function(s){ return !s.synthetic; }); }
|
|
1260
1359
|
|
|
1261
1360
|
// ---- glyph/badge helpers (only [✓]/[✗]/[▶]/[·]/[?] — never the words green/red/soft-green/died) ----
|
|
1361
|
+
// RULED 2026-10-15 (matches fwdloop glyphPulses): ONE rule for which sign blinks. Running blinks; a run waiting for the person's Accept blinks;
|
|
1362
|
+
// everything else rests (a schedule that is on and waiting included). `needs` = the run's Needs-you tag.
|
|
1363
|
+
function dotPulses(g, needs){ return g === "▶" || needs === "Accept"; }
|
|
1364
|
+
function dotClass(g, needs){ return "dot " + glyphClass(g) + (dotPulses(g, needs) ? " pulse" : ""); }
|
|
1262
1365
|
function glyphClass(g){
|
|
1263
1366
|
if(g === "✓") return "green";
|
|
1264
1367
|
if(g === "✗") return "red";
|
|
@@ -1285,9 +1388,9 @@
|
|
|
1285
1388
|
// A run with no number (an imported bundle's run is not in the run list) falls back to its id.
|
|
1286
1389
|
function runLabel(runNo, runid){ return typeof runNo === "number" ? "run-" + runNo : String(runid || ""); }
|
|
1287
1390
|
function runName(job, runNo, runid){ return job + " (" + runLabel(runNo, runid) + ")"; }
|
|
1288
|
-
function setRunHeader(sign, nameText, bodyHtml){
|
|
1391
|
+
function setRunHeader(sign, nameText, bodyHtml, needs){
|
|
1289
1392
|
document.getElementById("active-wf-name").textContent = nameText;
|
|
1290
|
-
document.getElementById("active-wf-dot").className =
|
|
1393
|
+
document.getElementById("active-wf-dot").className = dotClass(sign, needs);
|
|
1291
1394
|
var st = document.getElementById("active-wf-verdict");
|
|
1292
1395
|
st.innerHTML = bodyHtml ? '<span class="rp-sep">\u2502</span>' + bodyHtml : "";
|
|
1293
1396
|
st.removeAttribute("title");
|
|
@@ -1306,131 +1409,89 @@
|
|
|
1306
1409
|
return statusWordHtml(detail.status, reason) + " \u00B7 " + escapeXml(when);
|
|
1307
1410
|
}
|
|
1308
1411
|
|
|
1309
|
-
//
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
//
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
var
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
'<span class="wf-meta">' + escapeXml(rowSpendText(r)) + '</span>' +
|
|
1372
|
-
'<span class="wf-meta">' + escapeXml(rowWallText(r)) + '</span>' +
|
|
1373
|
-
'<span class="wf-meta">' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1374
|
-
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) +
|
|
1375
|
-
'</span>';
|
|
1376
|
-
}
|
|
1377
|
-
row.addEventListener("click", function(){ selectRun(r.runid, row, ".hist-row"); document.getElementById("tab-run").click(); scrollRunIntoViewMobile(); });
|
|
1378
|
-
row.addEventListener("keydown", function(e){ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); row.click(); } });
|
|
1379
|
-
return row;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// ---- History view ----
|
|
1383
|
-
function renderHistory(runs){
|
|
1384
|
-
var el = document.getElementById("history-list");
|
|
1385
|
-
el.innerHTML = "";
|
|
1386
|
-
var listed = (runs || []).filter(function(r){ return !r.fileMissing; });
|
|
1387
|
-
if(!listed.length){
|
|
1388
|
-
el.innerHTML = '<div class="hint">no runs listed yet</div>';
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
(runs || []).forEach(function(r){ el.appendChild(buildRunRowEl(r)); });
|
|
1412
|
+
// ---- P9 v2: ONE runs list (PANEL-BUILD.md "P9 v2", copied from fwdloop M11). Top to bottom: Running now, Needs you (the Scheduled slot follows
|
|
1413
|
+
// them once the triggers piece lands), a dashed line and the filters (they act on Jobs and quiet jobs only), Jobs, the quiet fold. The page works
|
|
1414
|
+
// out NOTHING: `live`, `needs`, each job's head run and quiet flag, the order and the limits all come in GET /api/runs. `rlModel` is pure (payload
|
|
1415
|
+
// in, cards out); the rl*Html functions only draw it; `paintRunsList` is the ONE painter.
|
|
1416
|
+
var rlData = { runs: [], jobs: [], scheduled: [], limits: { quietDays: 0, olderRuns: 0, needsYouDays: 0 } };
|
|
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.
|
|
1418
|
+
var rlOpen = {};
|
|
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
|
|
1420
|
+
var rlLast = { top: null, sched: null, jobs: null };
|
|
1421
|
+
// The ONE writer of `rlData`: the whole /api/runs payload, as the server sent it.
|
|
1422
|
+
function setRunsData(r){
|
|
1423
|
+
rlData = { runs: r.runs || [], jobs: r.jobs || [], scheduled: r.scheduled || [], limits: r.limits || rlData.limits };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// The ONE date of every card in the list: local `MM/DD HH:MM`. An unparseable stamp is shown raw, never blank.
|
|
1427
|
+
function rlShortDate(iso){
|
|
1428
|
+
var d = new Date(iso);
|
|
1429
|
+
if(isNaN(d.getTime())) return String(iso);
|
|
1430
|
+
var t = function(n){ return ("0" + n).slice(-2); };
|
|
1431
|
+
return t(d.getMonth() + 1) + "/" + t(d.getDate()) + " " + t(d.getHours()) + ":" + t(d.getMinutes());
|
|
1432
|
+
}
|
|
1433
|
+
function runAtText(r){
|
|
1434
|
+
return r.at ? rlShortDate(r.at) : "unknown";
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Everything a run card says, in ONE place: the card is drawn from it and the search reads it, so what you can see is what you can find.
|
|
1438
|
+
function rlRunParts(r){
|
|
1439
|
+
var rn = runLabel(r.runNo, r.runid);
|
|
1440
|
+
var p = { rn: rn, name: r.job + " (" + rn + ")", date: runAtText(r), model: r.model || "", word: "", why: "", spend: "", wall: "" };
|
|
1441
|
+
if(r.starting){ p.word = "running"; p.why = r.startingWord || "starting…"; return p; }
|
|
1442
|
+
if(r.fileMissing){ p.word = ""; p.why = "file missing"; return p; }
|
|
1443
|
+
p.word = r.status && r.status.word ? r.status.word : "";
|
|
1444
|
+
p.why = r.endedLine || (p.word === "passed" ? "goal met" : "");
|
|
1445
|
+
p.spend = rowSpendText(r, true);
|
|
1446
|
+
p.wall = rowWallText(r) || "";
|
|
1447
|
+
return p;
|
|
1448
|
+
}
|
|
1449
|
+
// ONE function for "does this run match the search": the filter and the highlight both call it, so they cannot disagree. M11 am5 (PANEL-BUILD.md P9 v2 O):
|
|
1450
|
+
// the search splits at spaces and EVERY word must match, case-insensitive. A word shaped run-N is that run exactly; a word shaped MM/DD or YYYY-MM-DD (a real
|
|
1451
|
+
// day) is the run's LOCAL start day, from the same `at` the card shows (MM/DD = any year; no date = no match); a part date (10/1, 10/) matches nothing, not even
|
|
1452
|
+
// the card's date text; any other word is a substring of the whole visible row (job, run, run id, model, status word, why, spend, wall, date, check type).
|
|
1453
|
+
function runMatchesSearch(r, query){
|
|
1454
|
+
var words = String(query || "").toLowerCase().replace(/[()]/g, "").split(/\s+/).filter(function(w){ return w; });
|
|
1455
|
+
if(!words.length) return true;
|
|
1456
|
+
var p = rlRunParts(r);
|
|
1457
|
+
var d = r.at ? new Date(r.at) : null;
|
|
1458
|
+
var dated = !!d && !isNaN(d.getTime());
|
|
1459
|
+
var rn = String(p.rn).toLowerCase();
|
|
1460
|
+
var rest = [r.job, p.rn, p.name, r.runid, p.model, p.word, p.why, p.spend, p.wall, r.checkType || ""].join(" ").toLowerCase();
|
|
1461
|
+
var whole = rest + " " + String(p.date).toLowerCase();
|
|
1462
|
+
return words.every(function(w){
|
|
1463
|
+
if(/^run-\d+$/.test(w)) return rn === w;
|
|
1464
|
+
var day = /^(?:(\d{4})-)?(\d\d)[-\/](\d\d)$/.exec(w);
|
|
1465
|
+
if(day && (day[1] ? w.charAt(4) === "-" && w.charAt(7) === "-" : w.charAt(2) === "/" || w.charAt(2) === "-")){
|
|
1466
|
+
var mo = Number(day[2]), da = Number(day[3]);
|
|
1467
|
+
if(mo >= 1 && mo <= 12 && da >= 1 && da <= 31){
|
|
1468
|
+
return dated && d.getMonth() + 1 === mo && d.getDate() === da && (!day[1] || d.getFullYear() === Number(day[1]));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if(/^[\d\/-]+$/.test(w) && /[\/-]/.test(w)) return rest.indexOf(w) !== -1;
|
|
1472
|
+
return whole.indexOf(w) !== -1;
|
|
1473
|
+
});
|
|
1392
1474
|
}
|
|
1393
1475
|
|
|
1394
|
-
//
|
|
1395
|
-
|
|
1396
|
-
// (the old /api/workflows endpoint is gone; see src/panel/server.js). Runs
|
|
1397
|
-
// arrive newest-first (server contract, see listRuns), so the FIRST run
|
|
1398
|
-
// seen for a job is its latest — no re-sort needed, and job order (first-
|
|
1399
|
-
// job-seen order) is already newest-first by construction.
|
|
1400
|
-
function groupRunsByJob(runs){
|
|
1476
|
+
// A job's runs, the server's head first, the rest in the server's order (newest first). The head is the server's call (`headRunid`), never "the first row".
|
|
1477
|
+
function groupRunsByJob(runs, jobs){
|
|
1401
1478
|
var byJob = {};
|
|
1402
1479
|
var order = [];
|
|
1480
|
+
var headOf = {};
|
|
1481
|
+
(jobs || []).forEach(function(j){ headOf[j.job] = j.headRunid; if(!byJob[j.job]){ byJob[j.job] = []; order.push(j.job); } });
|
|
1403
1482
|
(runs || []).forEach(function(r){
|
|
1404
1483
|
if(!byJob[r.job]){ byJob[r.job] = []; order.push(r.job); }
|
|
1405
1484
|
byJob[r.job].push(r);
|
|
1406
1485
|
});
|
|
1407
1486
|
return order.map(function(job){
|
|
1408
|
-
var
|
|
1409
|
-
var
|
|
1410
|
-
return {
|
|
1411
|
-
job: job,
|
|
1412
|
-
runs: jobRuns,
|
|
1413
|
-
runCount: jobRuns.length,
|
|
1414
|
-
lastRunid: last.runid,
|
|
1415
|
-
lastRunNo: last.runNo,
|
|
1416
|
-
lastAt: last.at,
|
|
1417
|
-
lastGlyph: last.glyph || "▶",
|
|
1418
|
-
lastStatus: last.status || null,
|
|
1419
|
-
lastEndedLine: last.endedLine || null,
|
|
1420
|
-
lastResumedCount: last.resumedCount || 0,
|
|
1421
|
-
lastCheckType: last.checkType || "unknown",
|
|
1422
|
-
lastModel: last.model || null,
|
|
1423
|
-
lastSpend: rowSpendText(last, true),
|
|
1424
|
-
lastWall: rowWallText(last) || "unknown",
|
|
1425
|
-
lastDate: last.date,
|
|
1426
|
-
};
|
|
1487
|
+
var all = byJob[job];
|
|
1488
|
+
var head = all.filter(function(r){ return r.runid === headOf[job]; })[0] || all[0] || null;
|
|
1489
|
+
return { job: job, runs: head ? [head].concat(all.filter(function(r){ return r !== head; })) : [], runCount: all.length, headRunid: head ? head.runid : null };
|
|
1427
1490
|
});
|
|
1428
1491
|
}
|
|
1429
1492
|
|
|
1430
|
-
// Matches a job if ANY of its runs match the filters (not only the
|
|
1431
|
-
//
|
|
1432
|
-
// of the AND/OR/time/search semantics), same posture as the old
|
|
1433
|
-
// filterWorkflows but now correct against the job's FULL run list.
|
|
1493
|
+
// Matches a job if ANY of its runs match the filters (not only the head): a one-line adapter over filterRuns, never a reimplementation of the
|
|
1494
|
+
// AND/OR/time/search semantics.
|
|
1434
1495
|
function filterWorkflows(jobs, filters, nowMs){
|
|
1435
1496
|
return (jobs || []).filter(function(g){
|
|
1436
1497
|
return filterRuns(g.runs, filters, nowMs).length > 0;
|
|
@@ -1446,166 +1507,256 @@
|
|
|
1446
1507
|
));
|
|
1447
1508
|
}
|
|
1448
1509
|
|
|
1449
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
var
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
if(
|
|
1513
|
-
|
|
1514
|
-
return;
|
|
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
|
+
}
|
|
1520
|
+
function rlModel(data, groups, filters, limits, sel, open){
|
|
1521
|
+
var active = filtersActive(filters);
|
|
1522
|
+
var schedOnly = !!(filters && filters.scheduledOnly);
|
|
1523
|
+
var place = {};
|
|
1524
|
+
(data.jobs || []).forEach(function(j, i){ place[j.job] = { quiet: j.quiet === true, at: i }; });
|
|
1525
|
+
var cards = groups.slice().sort(function(a, b){
|
|
1526
|
+
return (place[a.job] ? place[a.job].at : Infinity) - (place[b.job] ? place[b.job].at : Infinity);
|
|
1527
|
+
}).map(function(g){
|
|
1528
|
+
var quiet = !!(place[g.job] && place[g.job].quiet);
|
|
1529
|
+
if(g.headRunid === null) return { job: g.job, group: g, head: null, empty: true, quiet: quiet, selected: false, count: 0, sel: sel, hits: [], older: { open: false, shown: [], hidden: 0, left: [], active: active, total: 0 } };
|
|
1530
|
+
var head = g.runs[0];
|
|
1531
|
+
var matching = active ? filterRuns(g.runs, filters, Date.now()) : [];
|
|
1532
|
+
var older = g.runs.slice(1);
|
|
1533
|
+
var isSel = function(r){ return r.runid === sel.runid; };
|
|
1534
|
+
// M11 am5 item 4: the fold is capped at limits.olderRuns, newest first. With a search or chip on, only the older runs that match count; the selected run stays shown either way.
|
|
1535
|
+
var pool = active ? older.filter(function(r){ return matching.indexOf(r) !== -1; }) : older;
|
|
1536
|
+
var top = pool.slice(0, limits.olderRuns);
|
|
1537
|
+
var shown = top.concat(older.filter(function(r){ return isSel(r) && top.indexOf(r) === -1; }));
|
|
1538
|
+
var left = pool.filter(function(r){ return shown.indexOf(r) === -1; });
|
|
1539
|
+
// the fold opens by itself when any older run matches (even if the head matches too), or when the selected run is an older one
|
|
1540
|
+
var auto = (active && older.some(function(r){ return matching.indexOf(r) !== -1; })) || older.some(isSel);
|
|
1541
|
+
return {
|
|
1542
|
+
job: g.job, group: g, head: head, empty: false, quiet: quiet, count: g.runCount, sel: sel,
|
|
1543
|
+
selected: g.runs.some(isSel),
|
|
1544
|
+
hits: matching.map(function(r){ return r.runid; }),
|
|
1545
|
+
older: { open: Object.prototype.hasOwnProperty.call(open, g.job) ? open[g.job] : auto, shown: shown, hidden: left.length, left: left, active: active, total: older.length },
|
|
1546
|
+
};
|
|
1547
|
+
});
|
|
1548
|
+
var quietCards = cards.filter(function(c){ return c.quiet; });
|
|
1549
|
+
return {
|
|
1550
|
+
running: data.runs.filter(function(r){ return r.live === true; }),
|
|
1551
|
+
needs: data.runs.filter(function(r){ return typeof r.needs === "string" && r.needs !== ""; }),
|
|
1552
|
+
scheduled: (data.scheduled || []).filter(function(c){ return rlScheduledMatches(c, filters && filters.search); }),
|
|
1553
|
+
schedOnly: schedOnly,
|
|
1554
|
+
gaveUp: data.gaveUp || [],
|
|
1555
|
+
jobs: schedOnly ? [] : cards.filter(function(c){ return !c.quiet; }),
|
|
1556
|
+
quiet: schedOnly ? [] : quietCards,
|
|
1557
|
+
quietOpen: Object.prototype.hasOwnProperty.call(open, "__quiet") ? open.__quiet : quietCards.some(function(c){ return c.selected; }),
|
|
1558
|
+
limits: limits,
|
|
1559
|
+
noRows: data.runs.length === 0,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
function rlSectHtml(label, n, note){
|
|
1563
|
+
return '<div class="rl-sect">' + label + ' (' + n + ')' + (note ? ' ' + note : '') + '</div>';
|
|
1564
|
+
}
|
|
1565
|
+
// line 1 and 2 of a Running now / Needs you card; both jump to the run (rlJump). The tag is a label (a span), never a button.
|
|
1566
|
+
function rlJumpCardHtml(r, need){
|
|
1567
|
+
var rn = runLabel(r.runNo, r.runid);
|
|
1568
|
+
var faint;
|
|
1569
|
+
if(r.liveSince){
|
|
1570
|
+
faint = '<span data-rl-since="' + escapeXml(r.liveSince) + '" data-rl-prior="' + (typeof r.priorWallMs === "number" ? r.priorWallMs : "leg") + '"></span>';
|
|
1571
|
+
} else faint = 'time unknown';
|
|
1572
|
+
var l2;
|
|
1573
|
+
if(need){
|
|
1574
|
+
var p = rlRunParts(r);
|
|
1575
|
+
var why = r.needs === "Accept" ? "passed · waiting for your Accept" : [p.word, r.endedLine].filter(function(x){ return x; }).join(" · ");
|
|
1576
|
+
l2 = escapeXml(why) + ' · ' + escapeXml(r.stoppedAt ? rlShortDate(r.stoppedAt) : runAtText(r));
|
|
1577
|
+
} else l2 = escapeXml(r.liveWhy || "running");
|
|
1578
|
+
return '<button class="rl-card ' + (need ? 'rl-need' : 'rl-live') + '" type="button" data-rl-jump="' + escapeXml(r.runid) + '" data-testid="rl-' + (need ? 'need' : 'live') + '-' + escapeXml(r.job) + '-' + escapeXml(r.runid) + '">' +
|
|
1579
|
+
'<span class="rl-l1"><span class="' + dotClass(r.glyph, r.needs) + '"></span><span class="rl-nm" title="' + escapeXml(r.job) + '">' + escapeXml(r.job) + '</span> <span class="rl-rn">· ' + escapeXml(rn) + '</span>' +
|
|
1580
|
+
(need ? '<span class="rl-pill rl-pill-need">' + escapeXml(r.needs) + '</span>' : ' <span class="rl-rest">· ' + faint + ' · ' + escapeXml(rowSpendText(r, true)) + '</span>') + '</span>' +
|
|
1581
|
+
'<span class="rl-l2">' + l2 + '</span></button>';
|
|
1582
|
+
}
|
|
1583
|
+
// RULED 2026-10-15: `job · run-N` in the small lists; the server names the run a Scheduled / gave-up card opens (`runNo`), nothing is made up when it has none
|
|
1584
|
+
function rlRunTagHtml(c){ return typeof c.runNo === "number" ? ' <span class="rl-rn">· ' + escapeXml(runLabel(c.runNo, c.runId)) + '</span>' : ''; }
|
|
1585
|
+
// A schedule that gave up (RULED 2026-10-15): all words from the server (`pill`, `words`, `at`); a click opens the job's newest run, like the other cards.
|
|
1586
|
+
function rlGaveUpCardHtml(g){
|
|
1587
|
+
var body = '<span class="rl-l1"><span class="dot grey"></span><span class="rl-nm" title="' + escapeXml(g.job) + '">' + escapeXml(g.job) + '</span>' + rlRunTagHtml(g) + ' <span class="rl-pill rl-pill-need">' + escapeXml(g.pill) + '</span></span>' +
|
|
1588
|
+
'<span class="rl-l2">' + escapeXml(g.words) + ' · ' + escapeXml(g.at) + '</span>';
|
|
1589
|
+
if(typeof g.runId !== "string" || g.runId === "") return '<div class="rl-card rl-need" data-testid="rl-gaveup-' + escapeXml(g.job) + '">' + body + '</div>';
|
|
1590
|
+
return '<button class="rl-card rl-need" type="button" data-rl-jump="' + escapeXml(g.runId) + '" data-testid="rl-gaveup-' + escapeXml(g.job) + '">' + body + '</button>';
|
|
1591
|
+
}
|
|
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:
|
|
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
|
|
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.
|
|
1595
|
+
function rlScheduledCardHtml(s){
|
|
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) +
|
|
1597
|
+
(s.at ? ' <span class="rl-rest">· ' + escapeXml(s.at) + '</span>' : '') + '</span>' +
|
|
1598
|
+
'<span class="rl-l2">' + escapeXml([s.in, s.words].filter(function(x){ return x; }).join(" · ")) + '</span>';
|
|
1599
|
+
if(typeof s.runId !== "string" || s.runId === "") return '<div class="rl-card" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</div>';
|
|
1600
|
+
return '<button class="rl-card" type="button" data-rl-jump="' + escapeXml(s.runId) + '" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</button>';
|
|
1601
|
+
}
|
|
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).
|
|
1603
|
+
function rlTopHtml(m){
|
|
1604
|
+
var sections = [
|
|
1605
|
+
{ label: "Running now", items: m.running, draw: function(r){ return rlJumpCardHtml(r, false); } },
|
|
1606
|
+
{ label: "Needs you", items: m.needs.concat(m.gaveUp || []), draw: function(r){ return r.pill ? rlGaveUpCardHtml(r) : rlJumpCardHtml(r, true); } },
|
|
1607
|
+
];
|
|
1608
|
+
return sections.map(function(s){
|
|
1609
|
+
return s.items.length ? rlSectHtml(s.label, s.items.length) + s.items.map(s.draw).join("") : "";
|
|
1610
|
+
}).join("");
|
|
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
|
+
}
|
|
1621
|
+
// a run card inside a job: `[sign] run-N MM/DD HH:MM · model` / `word — why · $cost · wall`
|
|
1622
|
+
function rlRunCardHtml(r, selected, hit){
|
|
1623
|
+
var p = rlRunParts(r);
|
|
1624
|
+
var bits = [p.spend, p.wall].filter(function(b){ return b; });
|
|
1625
|
+
var l2;
|
|
1626
|
+
if(r.fileMissing) l2 = escapeXml(p.why);
|
|
1627
|
+
else if(r.starting) l2 = '<b class="st-word">' + escapeXml(p.word) + '</b> — ' + escapeXml(p.why);
|
|
1628
|
+
else l2 = (p.word ? '<b class="st-word">' + escapeXml(p.word) + '</b>' : '') + (p.why ? (p.word ? ' — ' : '') + escapeXml(p.why) : '') + (bits.length ? ' · ' + escapeXml(bits.join(" · ")) : '');
|
|
1629
|
+
return '<button class="rl-card' + (selected ? ' rl-sel' : '') + (hit ? ' rl-hit' : '') + (r.live ? ' rl-live' : '') + '" type="button" data-rl-run="' + escapeXml(r.runid) + '" data-testid="rl-run-' + escapeXml(r.job) + '-' + escapeXml(r.runid) + '" aria-pressed="' + (selected ? 'true' : 'false') + '" title="' + escapeXml(p.name) + '">' +
|
|
1630
|
+
'<span class="rl-l1"><span class="' + dotClass(r.glyph, r.needs) + '"></span><span class="rl-rn">' + escapeXml(p.rn) + '</span> <span class="rl-rest">· ' + escapeXml([p.date, p.model].filter(function(b){ return b; }).join(" · ")) + '</span></span>' +
|
|
1631
|
+
'<span class="rl-l2">' + l2 + '</span></button>';
|
|
1632
|
+
}
|
|
1633
|
+
// a job card: `[sign] job · k runs · type` / `word · why` of its newest run, highlighted when the selected run is one of its runs; not clickable. Inside: the newest run's card, then the older-runs fold.
|
|
1634
|
+
function rlJobCardHtml(c){
|
|
1635
|
+
var h = c.head;
|
|
1636
|
+
var top = '<div class="rl-card' + (c.selected ? ' rl-sel' : '') + '" id="rl-job-' + escapeXml(c.job) + '" data-testid="rl-job-' + escapeXml(c.job) + '">';
|
|
1637
|
+
if(c.empty){
|
|
1638
|
+
return top + '<span class="rl-l1"><span class="dot grey"></span><span class="rl-nm" title="' + escapeXml(c.job) + '">' + escapeXml(c.job) + '</span></span><span class="rl-l2">no runs yet</span></div>';
|
|
1515
1639
|
}
|
|
1516
|
-
var
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
// above) is normally the latest run, so this is unchanged in the
|
|
1550
|
-
// common case; a filter/search that matched an older run instead
|
|
1551
|
-
// makes the parent follow THAT run once it's the selected one.
|
|
1552
|
-
var isSelected = currentRunid === targetRunid;
|
|
1553
|
-
row.className = "wf-row" + (isSelected ? " selected" : "");
|
|
1554
|
-
row.setAttribute("data-wf", g.job);
|
|
1555
|
-
row.setAttribute("data-testid", "wf-row-" + g.job);
|
|
1556
|
-
row.setAttribute("tabindex", "0");
|
|
1557
|
-
row.setAttribute("role", "button");
|
|
1558
|
-
row.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1559
|
-
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
|
1560
|
-
var wfFullName = runName(g.job, g.lastRunNo, g.lastRunid);
|
|
1561
|
-
row.innerHTML =
|
|
1562
|
-
'<span class="wf-line1 wf-compact"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1563
|
-
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span>' +
|
|
1564
|
-
(g.lastStatus ? '<span class="wf-ended wf-ended-inline" data-testid="wf-ended-' + escapeXml(g.job) + '">' + statusWordHtml(g.lastStatus, g.lastEndedLine) + '</span>' : '') + '</span>' +
|
|
1565
|
-
'<span class="wf-meta-line">' +
|
|
1566
|
-
'<span class="wf-meta">' + escapeXml(g.lastCheckType) + '</span>' +
|
|
1567
|
-
'<span class="wf-meta">' + escapeXml(g.lastSpend) + '</span>' +
|
|
1568
|
-
'<span class="wf-meta">' + escapeXml(g.lastWall) + '</span>' +
|
|
1569
|
-
'<span class="wf-meta">' + escapeXml(g.lastDate || "unknown") + '</span>' +
|
|
1570
|
-
'</span>';
|
|
1571
|
-
row.addEventListener("click", function(){
|
|
1572
|
-
// a single-run job has nothing to expand — the click still opens
|
|
1573
|
-
// the run, it just never toggles `wfExpanded` for it.
|
|
1574
|
-
if(hasOtherRuns) wfExpanded[g.job] = !expanded;
|
|
1575
|
-
// build item (2026-09-26): clicking the parent also opens its
|
|
1576
|
-
// latest run (as it did before the Workflows/History merge) — pass
|
|
1577
|
-
// rowEl null since renderWorkflows(jobs) below rebuilds every row
|
|
1578
|
-
// right after, so a class set directly on this element would be
|
|
1579
|
-
// discarded; the fresh render marks selection off `currentRunid`
|
|
1580
|
-
// instead (see `isSelected` above). build item (2026-09-26 fix):
|
|
1581
|
-
// opens `targetRunid`, not always `g.lastRunid` — with a filter/
|
|
1582
|
-
// search active that excludes the latest but matches an older run,
|
|
1583
|
-
// that's the run the search actually found (see `representedRun`).
|
|
1584
|
-
selectRun(targetRunid, null, ".hist-row");
|
|
1585
|
-
document.getElementById("tab-run").click();
|
|
1586
|
-
scrollRunIntoViewMobile();
|
|
1587
|
-
renderWorkflows(jobs);
|
|
1588
|
-
});
|
|
1589
|
-
row.addEventListener("keydown", function(e){ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); row.click(); } });
|
|
1590
|
-
wrap.appendChild(row);
|
|
1591
|
-
|
|
1592
|
-
if(expanded){
|
|
1593
|
-
var runsWrap = document.createElement("div");
|
|
1594
|
-
runsWrap.className = "wf-runs";
|
|
1595
|
-
var capped = capSubRuns(childRuns, active, olderSelected);
|
|
1596
|
-
capped.shown.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1597
|
-
if(capped.more){
|
|
1598
|
-
var olderHint = document.createElement("div");
|
|
1599
|
-
olderHint.className = "hint";
|
|
1600
|
-
olderHint.setAttribute("data-testid", "wf-older-" + g.job);
|
|
1601
|
-
olderHint.textContent = "older: use search.";
|
|
1602
|
-
runsWrap.appendChild(olderHint);
|
|
1640
|
+
var p = rlRunParts(h);
|
|
1641
|
+
var isSel = function(r){ return r.runid === c.sel.runid; };
|
|
1642
|
+
var isHit = function(r){ return c.hits.indexOf(r.runid) !== -1; };
|
|
1643
|
+
var type = h.checkType && h.checkType !== "unknown" ? ' · ' + escapeXml(h.checkType) : '';
|
|
1644
|
+
var l2 = h.fileMissing ? escapeXml(p.why) : '<b class="st-word">' + escapeXml(p.word) + '</b>' + (p.why ? ' · ' + escapeXml(p.why) : '');
|
|
1645
|
+
var out = top + '<span class="rl-l1"><span class="' + dotClass(h.glyph, h.needs) + '"></span><span class="rl-nm" title="' + escapeXml(c.job) + '">' + escapeXml(c.job) + '</span> <span class="rl-cnt">· ' + c.count + ' run' + (c.count === 1 ? '' : 's') + type + '</span></span>' +
|
|
1646
|
+
'<span class="rl-l2">' + l2 + '</span>' +
|
|
1647
|
+
'<div class="rl-runs">' + rlRunCardHtml(h, isSel(h), isHit(h));
|
|
1648
|
+
if(c.older.total > 0){
|
|
1649
|
+
out += '<button class="rl-fold" type="button" data-rl-fold="' + escapeXml(c.job) + '" data-testid="rl-fold-' + escapeXml(c.job) + '" aria-expanded="' + (c.older.open ? 'true' : 'false') + '">' + (c.older.open ? '▾' : '▸') + ' older runs (' + c.older.total + ')</button>';
|
|
1650
|
+
if(c.older.open){
|
|
1651
|
+
out += c.older.shown.map(function(r){ return rlRunCardHtml(r, isSel(r), isHit(r)); }).join("");
|
|
1652
|
+
if(c.older.hidden > 0){
|
|
1653
|
+
// M11 am7 item 4 + am9, the line after the 7: the hidden runs' (searching: matches') real start days (the time the card
|
|
1654
|
+
// shows, `at`), the MIN to the MAX over ALL of `left` whatever its order: `k more older runs, MM/DD to MM/DD. Search to find one.`, or one day
|
|
1655
|
+
// `k more runs on MM/DD. Search to find one.`. A hidden run with no date drops the date part; nothing is made up. "run" is singular for k = 1.
|
|
1656
|
+
var days = c.older.left.map(function(x){
|
|
1657
|
+
var dd = x.at ? new Date(x.at) : null;
|
|
1658
|
+
return dd && !isNaN(dd.getTime()) ? new Date(dd.getFullYear(), dd.getMonth(), dd.getDate()) : null;
|
|
1659
|
+
});
|
|
1660
|
+
var md = function(dd){ return ("0" + (dd.getMonth() + 1)).slice(-2) + "/" + ("0" + dd.getDate()).slice(-2); };
|
|
1661
|
+
var dated = days.length > 0 && days.indexOf(null) === -1;
|
|
1662
|
+
var lo = dated ? new Date(Math.min.apply(null, days)) : null, hi = dated ? new Date(Math.max.apply(null, days)) : null;
|
|
1663
|
+
var k = c.older.hidden, runWord = k === 1 ? "run" : "runs";
|
|
1664
|
+
var one = dated && md(lo) === md(hi);
|
|
1665
|
+
// am9 (fwd verbatim, `matches` for every k): searching, the same days as the no-search line
|
|
1666
|
+
out += '<div class="rl-hint">' + (c.older.active ?
|
|
1667
|
+
(!dated ? k + ' more matches. Add a date or run number to narrow it.' :
|
|
1668
|
+
one ? k + ' more matches on ' + md(lo) + '. Add a run number to narrow it.' :
|
|
1669
|
+
k + ' more matches, ' + md(lo) + ' to ' + md(hi) + '. Add a date or run number to narrow it.') :
|
|
1670
|
+
!dated ? k + ' more older ' + runWord + '. Search to find one.' :
|
|
1671
|
+
one ? k + ' more ' + runWord + ' on ' + md(lo) + '. Search to find one.' :
|
|
1672
|
+
k + ' more older runs, ' + md(lo) + ' to ' + md(hi) + '. Search to find one.') + '</div>';
|
|
1603
1673
|
}
|
|
1604
|
-
wrap.appendChild(runsWrap);
|
|
1605
1674
|
}
|
|
1606
|
-
|
|
1675
|
+
}
|
|
1676
|
+
return out + '</div></div>';
|
|
1677
|
+
}
|
|
1678
|
+
function rlJobsHtml(m){
|
|
1679
|
+
if(m.schedOnly) return "";
|
|
1680
|
+
var h = "";
|
|
1681
|
+
if(m.jobs.length) h += rlSectHtml("Jobs", m.jobs.length, "· run in the last " + m.limits.quietDays + " days") + m.jobs.map(rlJobCardHtml).join("");
|
|
1682
|
+
if(m.quiet.length){
|
|
1683
|
+
h += '<button class="rl-fold" type="button" data-rl-fold="__quiet" data-testid="rl-fold-quiet" aria-expanded="' + (m.quietOpen ? 'true' : 'false') + '">' + (m.quietOpen ? '▾' : '▸') + ' quiet jobs (' + m.quiet.length + ') · no run in ' + m.limits.quietDays + ' days</button>';
|
|
1684
|
+
if(m.quietOpen) h += m.quiet.map(rlJobCardHtml).join("");
|
|
1685
|
+
}
|
|
1686
|
+
if(h === "" && m.noRows) h = '<div class="hint">no runs listed yet</div>';
|
|
1687
|
+
return h;
|
|
1688
|
+
}
|
|
1689
|
+
// Running now: the wall counts up IN PLACE from the server's start time (no rebuild): the page's 2 s tick calls this. Earlier legs add the server's
|
|
1690
|
+
// `priorWallMs`; when that is not exact the card says it counts this leg only.
|
|
1691
|
+
function tickSince(){
|
|
1692
|
+
document.querySelectorAll("[data-rl-since]").forEach(function(el){
|
|
1693
|
+
var t = Date.parse(el.getAttribute("data-rl-since"));
|
|
1694
|
+
if(isNaN(t)){ el.textContent = "time unknown"; return; }
|
|
1695
|
+
var prior = el.getAttribute("data-rl-prior");
|
|
1696
|
+
var leg = prior === "leg";
|
|
1697
|
+
el.textContent = duration(Math.max(0, Date.now() - t) + (leg ? 0 : Number(prior) || 0)) + (leg ? " this leg" : "");
|
|
1607
1698
|
});
|
|
1608
1699
|
}
|
|
1700
|
+
// keep focus on the same card (by testid) across a real repaint of a box
|
|
1701
|
+
function withFocusPreserved(box, fn){
|
|
1702
|
+
var a = document.activeElement;
|
|
1703
|
+
var tid = a && box.contains && box.contains(a) && a.getAttribute ? a.getAttribute("data-testid") : null;
|
|
1704
|
+
fn();
|
|
1705
|
+
if(!tid) return;
|
|
1706
|
+
var nodes = box.querySelectorAll("[data-testid]");
|
|
1707
|
+
for(var i = 0; i < nodes.length; i += 1){
|
|
1708
|
+
if(nodes[i].getAttribute("data-testid") === tid){ if(nodes[i].focus) nodes[i].focus(); return; }
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// The ONE painter of the list. Called by the filter bar (a poll, a chip, the search box) and by every select/click here; it writes a box only when its markup changed.
|
|
1712
|
+
function paintRunsList(){
|
|
1713
|
+
var filters = currentRunsFilters();
|
|
1714
|
+
var groups = filterWorkflows(groupRunsByJob(rlData.runs, rlData.jobs), filters, Date.now());
|
|
1715
|
+
var m = rlModel(rlData, groups, filters, rlData.limits, { runid: currentRunid }, rlOpen);
|
|
1716
|
+
var topEl = document.getElementById("rl-top");
|
|
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);
|
|
1721
|
+
var top = rlTopHtml(m);
|
|
1722
|
+
var jobs = rlJobsHtml(m);
|
|
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);
|
|
1726
|
+
if(jobs !== rlLast.jobs){ rlLast.jobs = jobs; withFocusPreserved(listEl, function(){ listEl.innerHTML = jobs; }); }
|
|
1727
|
+
tickSince();
|
|
1728
|
+
}
|
|
1729
|
+
function rlRowOf(runid){
|
|
1730
|
+
return rlData.runs.filter(function(r){ return r.runid === runid; })[0] || null;
|
|
1731
|
+
}
|
|
1732
|
+
// Running now / Needs you click: select that run, show it on the Run tab and scroll its job card into view.
|
|
1733
|
+
function rlJump(runid){
|
|
1734
|
+
selectRun(runid);
|
|
1735
|
+
document.getElementById("tab-run").click();
|
|
1736
|
+
var row = rlRowOf(runid);
|
|
1737
|
+
var card = row ? document.getElementById("rl-job-" + row.job) : null;
|
|
1738
|
+
if(card && card.scrollIntoView) card.scrollIntoView({ block: "nearest" });
|
|
1739
|
+
}
|
|
1740
|
+
// one delegated click handler for the whole list: a fold, a jump card or a run card
|
|
1741
|
+
function rlClick(ev){
|
|
1742
|
+
var t = ev.target && ev.target.closest ? ev.target : null;
|
|
1743
|
+
if(!t) return;
|
|
1744
|
+
var fold = t.closest("[data-rl-fold]");
|
|
1745
|
+
if(fold){
|
|
1746
|
+
rlOpen[fold.getAttribute("data-rl-fold")] = fold.getAttribute("aria-expanded") !== "true";
|
|
1747
|
+
paintRunsList();
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
var jump = t.closest("[data-rl-jump]");
|
|
1751
|
+
if(jump){ rlJump(jump.getAttribute("data-rl-jump")); return; }
|
|
1752
|
+
var card = t.closest("[data-rl-run]");
|
|
1753
|
+
if(card){
|
|
1754
|
+
selectRun(card.getAttribute("data-rl-run"));
|
|
1755
|
+
document.getElementById("tab-run").click();
|
|
1756
|
+
scrollRunIntoViewMobile();
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
document.getElementById("panel-runs").addEventListener("click", rlClick);
|
|
1609
1760
|
|
|
1610
1761
|
// ---- Filter bar (item 5 + hamr's 2026-09-25 live-panel feedback) ----
|
|
1611
1762
|
// ONE component, ONE state shape ({checkTypes, results, time}), used by
|
|
@@ -1616,51 +1767,39 @@
|
|
|
1616
1767
|
// "[ ]"; ".fchip" turns that off), time is a short label, then "clear".
|
|
1617
1768
|
// No "unknown" check-type chip — runs/workflows with unknown type still
|
|
1618
1769
|
// show whenever no type chip is selected (checkTypes stays empty).
|
|
1770
|
+
// The chips, in the order they are drawn: ONE declared array, never object key order.
|
|
1771
|
+
var RL_CHIPS = [
|
|
1772
|
+
{ group: "checkType", value: "deterministic", title: "deterministic", id: "ct-deterministic", label: "D" },
|
|
1773
|
+
{ group: "checkType", value: "rubric", title: "rubric", id: "ct-rubric", label: "R" },
|
|
1774
|
+
{ group: "result", value: "\u2713", title: "pass", id: "res-pass", label: "✓" },
|
|
1775
|
+
{ group: "result", value: "\u2717", title: "fail", id: "res-fail", label: "✗" },
|
|
1776
|
+
{ group: "result", value: "?", title: "died", id: "res-died", label: "?" },
|
|
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" },
|
|
1779
|
+
{ group: "time", value: "7d", title: "last 7 days", id: "time-7d", label: "7d" },
|
|
1780
|
+
{ group: "time", value: "30d", title: "last 30 days", id: "time-30d", label: "30d" },
|
|
1781
|
+
{ group: "time", value: "all", title: "all time", id: "time-all", label: "all", pressed: true },
|
|
1782
|
+
{ clear: true },
|
|
1783
|
+
];
|
|
1619
1784
|
function filterBarHTML(scope){
|
|
1620
1785
|
return (
|
|
1621
1786
|
'<div class="filterbar" data-scope="' + scope + '" data-testid="' + scope + '-filters">' +
|
|
1622
|
-
'<input type="text" class="search-input" placeholder="
|
|
1787
|
+
'<input type="text" class="search-input" placeholder="job, run-23, or date 10/13" ' +
|
|
1623
1788
|
'id="' + scope + '-filter-search" data-testid="' + scope + '-filter-search" autocomplete="off">' +
|
|
1624
1789
|
'<div class="chips" role="group" aria-label="Filter">' +
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
'<button class="chip fchip" type="button" aria-pressed="false" data-filter-group="result" data-filter-value="?" title="died" data-testid="chip-' + scope + '-res-died">?</button>' +
|
|
1630
|
-
'<button class="chip fchip" type="button" aria-pressed="false" data-filter-group="result" data-filter-value="▶" title="running" data-testid="chip-' + scope + '-res-running">▶</button>' +
|
|
1631
|
-
'<button class="chip fchip" type="button" aria-pressed="false" data-filter-group="time" data-filter-value="7d" title="last 7 days" data-testid="chip-' + scope + '-time-7d">7d</button>' +
|
|
1632
|
-
'<button class="chip fchip" type="button" aria-pressed="false" data-filter-group="time" data-filter-value="30d" title="last 30 days" data-testid="chip-' + scope + '-time-30d">30d</button>' +
|
|
1633
|
-
'<button class="chip fchip" type="button" aria-pressed="true" data-filter-group="time" data-filter-value="all" title="all time" data-testid="chip-' + scope + '-time-all">all</button>' +
|
|
1634
|
-
'<button class="chip fchip" type="button" id="' + scope + '-filter-clear" data-testid="' + scope + '-filter-clear">clear</button>' +
|
|
1790
|
+
RL_CHIPS.map(function(c){
|
|
1791
|
+
if(c.clear) return '<button class="chip fchip" type="button" id="' + scope + '-filter-clear" data-testid="' + scope + '-filter-clear">clear</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>';
|
|
1793
|
+
}).join("") +
|
|
1635
1794
|
'</div>' +
|
|
1636
1795
|
'<div class="hint" id="' + scope + '-filter-count" data-testid="' + scope + '-filter-count"></div>' +
|
|
1637
1796
|
'</div>'
|
|
1638
1797
|
);
|
|
1639
1798
|
}
|
|
1640
1799
|
|
|
1641
|
-
// Pure function: chips toggle on/off independently and combine AND across
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
// `time` is single-select, default "all". `nowMs` is injectable for tests.
|
|
1645
|
-
// Field names (checkType/glyph/at) match a History run row's own shape.
|
|
1646
|
-
// item 1 (2026-09-25): case-insensitive substring match on job name AND
|
|
1647
|
-
// runid, combined AND with the chip groups (an empty/whitespace-only query
|
|
1648
|
-
// means "no search filter" — everything passes it, same posture as an
|
|
1649
|
-
// empty chip group). item C (2026-09-26): also matches the run's model
|
|
1650
|
-
// name (e.g. "deepseek", "sonnet") — `model` is `null`/absent on an older
|
|
1651
|
-
// archived run, same posture as a missing job/runid. Also the run's shown
|
|
1652
|
-
// name: `run-N` and `job (run-N)` (N is the server's `runNo`, never counted here).
|
|
1653
|
-
function matchesSearch(job, runid, model, query, runNo){
|
|
1654
|
-
if(!query) return true;
|
|
1655
|
-
var q = String(query).trim().toLowerCase();
|
|
1656
|
-
if(!q) return true;
|
|
1657
|
-
var j = String(job || "").toLowerCase();
|
|
1658
|
-
var r = String(runid || "").toLowerCase();
|
|
1659
|
-
var m = String(model || "").toLowerCase();
|
|
1660
|
-
var label = typeof runNo === "number" ? "run-" + runNo : "";
|
|
1661
|
-
var shown = label ? j + " (" + label + ")" : "";
|
|
1662
|
-
return j.indexOf(q) !== -1 || r.indexOf(q) !== -1 || m.indexOf(q) !== -1 || (label !== "" && (label.indexOf(q) !== -1 || shown.indexOf(q) !== -1));
|
|
1663
|
-
}
|
|
1800
|
+
// Pure function: chips toggle on/off independently and combine AND across the three groups (checkType/result/time), OR within a group (an empty
|
|
1801
|
+
// group array means "no filter" for that group — everything passes it). `time` is single-select, default "all". `nowMs` is injectable for tests.
|
|
1802
|
+
// The search is `runMatchesSearch`: the whole visible row, case-insensitive; an empty query passes everything.
|
|
1664
1803
|
function filterRuns(runs, filters, nowMs){
|
|
1665
1804
|
nowMs = (typeof nowMs === "number") ? nowMs : Date.now();
|
|
1666
1805
|
var ct = (filters && filters.checkTypes) || [];
|
|
@@ -1671,7 +1810,7 @@
|
|
|
1671
1810
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1672
1811
|
// a stopped run ([■]) was a [✗] before hamr's 2026-10-07 sign ruling — the fail chip still finds it
|
|
1673
1812
|
if(rs.length && rs.indexOf(r.glyph) === -1 && !(r.glyph === "\u25A0" && rs.indexOf("\u2717") !== -1)) return false;
|
|
1674
|
-
if(!
|
|
1813
|
+
if(!runMatchesSearch(r, q)) return false;
|
|
1675
1814
|
if(time !== "all"){
|
|
1676
1815
|
var days = time === "7d" ? 7 : 30;
|
|
1677
1816
|
var t = typeof r.at === "string" ? Date.parse(r.at) : NaN;
|
|
@@ -1686,13 +1825,12 @@
|
|
|
1686
1825
|
* One filter-bar instance: its own localStorage key, its own DOM scope
|
|
1687
1826
|
* (`.filterbar[data-scope="..."]`), its own item list — but the same
|
|
1688
1827
|
* wiring as every other instance.
|
|
1689
|
-
* @param {{scope:string, storageKey:string,
|
|
1828
|
+
* @param {{scope:string, storageKey:string, onChange:Function}} opts
|
|
1690
1829
|
*/
|
|
1691
1830
|
function createFilterBar(opts){
|
|
1692
1831
|
var scope = opts.scope;
|
|
1693
1832
|
var storageKey = opts.storageKey;
|
|
1694
|
-
var state = { checkTypes: [], results: [], time: "all", search: "" };
|
|
1695
|
-
var allItems = [];
|
|
1833
|
+
var state = { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
|
|
1696
1834
|
|
|
1697
1835
|
function load(){
|
|
1698
1836
|
try{
|
|
@@ -1705,6 +1843,7 @@
|
|
|
1705
1843
|
results: Array.isArray(parsed.results) ? parsed.results : [],
|
|
1706
1844
|
time: typeof parsed.time === "string" ? parsed.time : "all",
|
|
1707
1845
|
search: typeof parsed.search === "string" ? parsed.search : "",
|
|
1846
|
+
scheduledOnly: parsed.scheduledOnly === true,
|
|
1708
1847
|
};
|
|
1709
1848
|
}
|
|
1710
1849
|
}
|
|
@@ -1719,6 +1858,7 @@
|
|
|
1719
1858
|
var value = chip.getAttribute("data-filter-value");
|
|
1720
1859
|
var active;
|
|
1721
1860
|
if(group === "time"){ active = state.time === value; }
|
|
1861
|
+
else if(group === "view"){ active = state.scheduledOnly === true; }
|
|
1722
1862
|
else if(group === "checkType"){ active = state.checkTypes.indexOf(value) !== -1; }
|
|
1723
1863
|
else { active = state.results.indexOf(value) !== -1; }
|
|
1724
1864
|
chip.setAttribute("aria-pressed", active ? "true" : "false");
|
|
@@ -1727,10 +1867,7 @@
|
|
|
1727
1867
|
if(searchEl && searchEl.value !== state.search) searchEl.value = state.search;
|
|
1728
1868
|
}
|
|
1729
1869
|
function apply(){
|
|
1730
|
-
|
|
1731
|
-
opts.applyFn(filtered);
|
|
1732
|
-
var countEl = document.getElementById(scope + "-filter-count");
|
|
1733
|
-
if(countEl) countEl.textContent = "showing " + filtered.length + " of " + allItems.length + " runs";
|
|
1870
|
+
opts.onChange(state);
|
|
1734
1871
|
}
|
|
1735
1872
|
document.querySelectorAll('.filterbar[data-scope="' + scope + '"] .chip[data-filter-group]').forEach(function(chip){
|
|
1736
1873
|
chip.addEventListener("click", function(){
|
|
@@ -1738,6 +1875,8 @@
|
|
|
1738
1875
|
var value = chip.getAttribute("data-filter-value");
|
|
1739
1876
|
if(group === "time"){
|
|
1740
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
|
|
1741
1880
|
} else if(group === "checkType"){
|
|
1742
1881
|
var idx = state.checkTypes.indexOf(value);
|
|
1743
1882
|
if(idx === -1) state.checkTypes.push(value); else state.checkTypes.splice(idx, 1);
|
|
@@ -1753,7 +1892,7 @@
|
|
|
1753
1892
|
var clearBtn = document.getElementById(scope + "-filter-clear");
|
|
1754
1893
|
if(clearBtn){
|
|
1755
1894
|
clearBtn.addEventListener("click", function(){
|
|
1756
|
-
state = { checkTypes: [], results: [], time: "all", search: "" };
|
|
1895
|
+
state = { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
|
|
1757
1896
|
paint();
|
|
1758
1897
|
save();
|
|
1759
1898
|
apply();
|
|
@@ -1771,72 +1910,22 @@
|
|
|
1771
1910
|
paint();
|
|
1772
1911
|
|
|
1773
1912
|
return {
|
|
1774
|
-
setItems: function(items){ allItems = items || []; apply(); },
|
|
1775
1913
|
getState: function(){ return state; },
|
|
1776
1914
|
refresh: function(){ apply(); },
|
|
1777
1915
|
};
|
|
1778
1916
|
}
|
|
1779
1917
|
|
|
1780
|
-
//
|
|
1781
|
-
// per-viewer-convenience posture as every other localStorage read/write on
|
|
1782
|
-
// this page (try/catch, never assumed available).
|
|
1783
|
-
var RUNS_VIEW_KEY = "bareloop-panel-runs-view";
|
|
1784
|
-
function loadRunsViewMode(){
|
|
1785
|
-
try{
|
|
1786
|
-
var v = localStorage.getItem(RUNS_VIEW_KEY);
|
|
1787
|
-
if(v === "workflows" || v === "history") return v;
|
|
1788
|
-
}catch(e){ /* per-viewer convenience only */ }
|
|
1789
|
-
return "workflows";
|
|
1790
|
-
}
|
|
1791
|
-
function saveRunsViewMode(v){
|
|
1792
|
-
try{ localStorage.setItem(RUNS_VIEW_KEY, v); }catch(e){ /* best-effort only */ }
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
// item 2 (2026-09-26 merge): ONE shared filter bar drives whichever view
|
|
1796
|
-
// (Workflows/History) is currently on — one scope, one localStorage key,
|
|
1797
|
-
// so switching views never loses the search/chip state.
|
|
1918
|
+
// ONE filter bar drives the runs list (the Workflows / History switch is gone, P9 v2): one scope, one localStorage key.
|
|
1798
1919
|
document.getElementById("runs-filterbar-anchor").innerHTML = filterBarHTML("runs");
|
|
1799
1920
|
|
|
1800
|
-
var runsViewMode = loadRunsViewMode();
|
|
1801
|
-
function runsFilterFn(items, filters, nowMs){
|
|
1802
|
-
if(runsViewMode === "history") return filterRuns(items, filters, nowMs);
|
|
1803
|
-
return filterWorkflows(groupRunsByJob(items), filters, nowMs);
|
|
1804
|
-
}
|
|
1805
|
-
function runsApplyFn(filtered){
|
|
1806
|
-
if(runsViewMode === "history") renderHistory(filtered);
|
|
1807
|
-
else renderWorkflows(filtered);
|
|
1808
|
-
}
|
|
1809
1921
|
var runsFilterBar = createFilterBar({
|
|
1810
1922
|
scope: "runs",
|
|
1811
1923
|
storageKey: "bareloop-panel-runs-filters",
|
|
1812
|
-
|
|
1813
|
-
applyFn: runsApplyFn,
|
|
1924
|
+
onChange: function(){ paintRunsList(); },
|
|
1814
1925
|
});
|
|
1815
1926
|
function currentRunsFilters(){
|
|
1816
|
-
return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "" };
|
|
1927
|
+
return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
|
|
1817
1928
|
}
|
|
1818
|
-
function paintRunsViewToggle(){
|
|
1819
|
-
document.querySelectorAll('.chip[data-runs-view]').forEach(function(c){
|
|
1820
|
-
c.setAttribute("aria-pressed", c.getAttribute("data-runs-view") === runsViewMode ? "true" : "false");
|
|
1821
|
-
});
|
|
1822
|
-
document.getElementById("wf-list").hidden = runsViewMode !== "workflows";
|
|
1823
|
-
document.getElementById("history-list").hidden = runsViewMode !== "history";
|
|
1824
|
-
// P5 item 4: Import lives on the Workflows view only
|
|
1825
|
-
document.getElementById("wf-import").hidden = runsViewMode !== "workflows";
|
|
1826
|
-
document.getElementById("import-list").hidden = runsViewMode !== "workflows";
|
|
1827
|
-
if(runsViewMode !== "workflows") document.getElementById("import-box").hidden = true;
|
|
1828
|
-
}
|
|
1829
|
-
document.querySelectorAll('.chip[data-runs-view]').forEach(function(chip){
|
|
1830
|
-
chip.addEventListener("click", function(){
|
|
1831
|
-
var next = chip.getAttribute("data-runs-view");
|
|
1832
|
-
if(next === runsViewMode) return;
|
|
1833
|
-
runsViewMode = next;
|
|
1834
|
-
saveRunsViewMode(runsViewMode);
|
|
1835
|
-
paintRunsViewToggle();
|
|
1836
|
-
runsFilterBar.refresh();
|
|
1837
|
-
});
|
|
1838
|
-
});
|
|
1839
|
-
paintRunsViewToggle();
|
|
1840
1929
|
|
|
1841
1930
|
// item 4: phone-only "scroll the run view into view" on row select, and
|
|
1842
1931
|
// the matching "↑ list" jump back. Desktop never runs either — both panes
|
|
@@ -1858,8 +1947,8 @@
|
|
|
1858
1947
|
});
|
|
1859
1948
|
}
|
|
1860
1949
|
|
|
1861
|
-
function clearSelection(
|
|
1862
|
-
document.querySelectorAll(".wf-row
|
|
1950
|
+
function clearSelection(){
|
|
1951
|
+
document.querySelectorAll(".wf-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
1863
1952
|
}
|
|
1864
1953
|
|
|
1865
1954
|
// ---- P5 item 4: IMPORT (read only) — the folder browser, the imported rows, the view ----
|
|
@@ -1974,13 +2063,14 @@
|
|
|
1974
2063
|
currentImportId = null;
|
|
1975
2064
|
document.getElementById("import-view").hidden = true;
|
|
1976
2065
|
document.getElementById("run-counters").hidden = false;
|
|
1977
|
-
document.getElementById("run-empty").textContent = "select a run from
|
|
2066
|
+
document.getElementById("run-empty").textContent = "select a run from the Runs list";
|
|
1978
2067
|
document.getElementById("audit-select-empty").textContent = "select a run first";
|
|
1979
2068
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed", "false"); });
|
|
1980
2069
|
}
|
|
1981
2070
|
function selectImport(id){
|
|
1982
2071
|
clearSelection();
|
|
1983
2072
|
currentRunid = null;
|
|
2073
|
+
paintRunsList();
|
|
1984
2074
|
runIsLive = false;
|
|
1985
2075
|
currentImportId = id;
|
|
1986
2076
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
@@ -2120,38 +2210,30 @@
|
|
|
2120
2210
|
document.getElementById("tab-runs").addEventListener("click", function(){ loadImports(); });
|
|
2121
2211
|
|
|
2122
2212
|
// ---- Run tab ----
|
|
2123
|
-
// panelMoney —
|
|
2124
|
-
//
|
|
2125
|
-
//
|
|
2126
|
-
//
|
|
2127
|
-
// (
|
|
2128
|
-
// browser-script sibling of `src/panel/authorroutes.js`'s own
|
|
2129
|
-
// `panelMoney2` (same rule, can't share code — one runs in Node, one
|
|
2130
|
-
// 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:
|
|
2131
2218
|
// - null/undefined/non-finite -> "unknown" (never a fabricated $0).
|
|
2132
|
-
// - a REAL positive amount
|
|
2133
|
-
//
|
|
2134
|
-
// -
|
|
2135
|
-
//
|
|
2136
|
-
//
|
|
2137
|
-
//
|
|
2138
|
-
//
|
|
2139
|
-
// sides: dollar figures here are derived from at most 6-decimal-precision
|
|
2140
|
-
// money (the same precision `src/text.js`'s `tallyCalls`/`priceOf` use),
|
|
2141
|
-
// so rounding to 6 decimals first erases that noise (e.g. 0.815 really
|
|
2142
|
-
// stored as 0.81499999999999995), and ONLY THEN is the value rounded/
|
|
2143
|
-
// floored to cents — a value already exactly on a cent boundary (e.g.
|
|
2144
|
-
// 0.82) can be stored as 81.99999999999999 cents post-multiply, and a
|
|
2145
|
-
// floor there would wrongly drop a whole cent; the tiny `+1e-6` nudge (at
|
|
2146
|
-
// dollar-cents scale, far below anything a real half-cent tie could be)
|
|
2147
|
-
// corrects that without ever crossing a true half-cent boundary the wrong
|
|
2148
|
-
// 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.
|
|
2149
2226
|
function panelMoney(n, floor){
|
|
2150
2227
|
if(n === null || n === undefined) return "unknown";
|
|
2151
2228
|
var v = Number(n);
|
|
2152
2229
|
if(!isFinite(v)) return "unknown";
|
|
2153
|
-
if(v > 0 && v < 0.01) return "<$0.01";
|
|
2154
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
|
+
}
|
|
2155
2237
|
var cents = floor ? Math.floor(clean * 100 + 1e-6) : Math.round(clean * 100 + 1e-6);
|
|
2156
2238
|
if(cents < 0) cents = 0;
|
|
2157
2239
|
return "$" + (cents / 100).toFixed(2);
|
|
@@ -2164,8 +2246,9 @@
|
|
|
2164
2246
|
* complete run whose drafting share is itself incomplete — hamr's ruling:
|
|
2165
2247
|
* a reader summing the two must never read the leading number as exact
|
|
2166
2248
|
* when the bracket next to it isn't). `draftSpendComplete === false` marks
|
|
2167
|
-
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX". `shortForm`
|
|
2168
|
-
* "drafting" (the Runs list cards only — "$5.00 ($0.08)"
|
|
2249
|
+
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX". `shortForm` moves the word
|
|
2250
|
+
* "drafting" to lead inside the bracket (the Runs list cards only — "$5.00 (drafting $0.08)",
|
|
2251
|
+
* floor "(drafting at least $0.08)").
|
|
2169
2252
|
*/
|
|
2170
2253
|
function panelMoneyWithDraft(spendCoreVal, spendCoreFloor, draftSpentUsd, draftSpendComplete, budgetUsd, shortForm){
|
|
2171
2254
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
@@ -2173,8 +2256,16 @@
|
|
|
2173
2256
|
var core = (spendCoreVal === null || spendCoreVal === undefined)
|
|
2174
2257
|
? "unknown"
|
|
2175
2258
|
: ((spendCoreFloor || draftFloor) ? "at least " + panelMoney(spendCoreVal) : panelMoney(spendCoreVal));
|
|
2176
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2177
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) + ")" : "";
|
|
2178
2269
|
return core + draftPart + capPart;
|
|
2179
2270
|
}
|
|
2180
2271
|
// P5 item 6: a LIVE row (no job-end yet, runner alive) reads the same running floors the
|
|
@@ -2231,7 +2322,7 @@
|
|
|
2231
2322
|
if(spendFloorUsd === null || spendFloorUsd === undefined) return "unknown";
|
|
2232
2323
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
2233
2324
|
var draftFloor = hasDraft && draftSpendComplete === false;
|
|
2234
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2325
|
+
var draftPart = hasDraft ? (shortForm ? " (drafting " : " (") + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2235
2326
|
return panelMoney(spendFloorUsd) + " so far" + draftPart;
|
|
2236
2327
|
}
|
|
2237
2328
|
/** build item 6 — the matching wall-clock phrase: "running <duration>",
|
|
@@ -2374,55 +2465,70 @@
|
|
|
2374
2465
|
else { row.hidden = true; val.textContent = ""; }
|
|
2375
2466
|
}
|
|
2376
2467
|
|
|
2377
|
-
// ---- P5: the ENDED block (
|
|
2378
|
-
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2379
|
-
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2468
|
+
// ---- P5: the ENDED block (Run tab, first block above the action row, every finished run) and Resume ----
|
|
2380
2469
|
var lastEndedSig = null;
|
|
2470
|
+
function summaryRow(label, text, testid, red, extraHtml){
|
|
2471
|
+
return '<div class="ended-row' + (red ? " is-red" : "") + '"><span class="ended-label">' + escapeXml(label) + '</span><span class="ended-text" data-testid="' + testid + '">' + escapeXml(text) + (extraHtml || "") + '</span></div>';
|
|
2472
|
+
}
|
|
2381
2473
|
// Resume (P5-R, hamr 2026-10-02: opened the run's own Job tab) MOVED to the left Chat card (hamr 2026-10-07): the card
|
|
2382
2474
|
// in RESUME mode, only the money cap and the time cap open (applyResume / signResume). The Job tab is read-only on
|
|
2383
2475
|
// every run — values only, no inputs, no buttons. A resume is the SAME run: no new card, no new job.
|
|
2476
|
+
// RULED 2026-10-15 (aligned with fwdloop M9 am7): ONE block, the same shape every time: ENDED (a finished run), NEXT (a finished run), SCHEDULE
|
|
2477
|
+
// (always: the schedule's words and a second line, or "By hand"). A schedule that started nothing turns the SCHEDULE row AND the frame red.
|
|
2478
|
+
// The sentences are CODE-OWNED on the server (`endedFor`/`endedText` in src/panel/server.js, `detail.schedule` from src/schedulewords.js) — this only paints them.
|
|
2384
2479
|
function renderEnded(detail){
|
|
2385
2480
|
var box = document.getElementById("ended-block");
|
|
2386
2481
|
var e = detail.ended;
|
|
2387
|
-
|
|
2482
|
+
var sc = detail.schedule;
|
|
2483
|
+
if(!e && !sc){ box.hidden = true; box.innerHTML = ""; lastEndedSig = null; return; }
|
|
2388
2484
|
// rebuilt only when what it shows changed
|
|
2389
|
-
var sig2 = detail.runid + "|" + JSON.stringify(e) + "|" + JSON.stringify(detail.resume);
|
|
2485
|
+
var sig2 = detail.runid + "|" + JSON.stringify(e) + "|" + JSON.stringify(detail.resume) + "|" + (detail.endedText || "") + "|" + JSON.stringify(sc && [sc.scheduled, sc.text, sc.line2, sc.problem]);
|
|
2390
2486
|
if(sig2 === lastEndedSig) return;
|
|
2391
2487
|
lastEndedSig = sig2;
|
|
2392
2488
|
box.hidden = false;
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2489
|
+
var problem = !!(sc && sc.scheduled && sc.problem === true);
|
|
2490
|
+
box.className = "ended-block " + (problem ? "red" : (!e ? "" : (detail.died ? "magenta" : (detail.glyph === "✓" ? "green" : (detail.glyph === "■" ? "grey" : "red")))));
|
|
2491
|
+
var h = "";
|
|
2492
|
+
if(e){
|
|
2493
|
+
// text only: the ONE place for [Stop] / [Resume] / [Reuse workflow] is the Run tab's top action row (renderRunActions)
|
|
2494
|
+
h += summaryRow("ENDED", detail.endedText || e.reason, "ended-reason", false) + summaryRow("NEXT", e.next, "ended-next", false);
|
|
2495
|
+
}
|
|
2496
|
+
if(sc){
|
|
2497
|
+
var w = sc.words || {};
|
|
2498
|
+
var label = w.scheduledLabel || "SCHEDULE";
|
|
2499
|
+
if(sc.scheduled){
|
|
2500
|
+
h += summaryRow(label, sc.text, "sched-text", problem, sc.line2 ? '<span class="sched-line2" data-testid="sched-line2">' + escapeXml(sc.line2) + '</span>' : "");
|
|
2501
|
+
} else {
|
|
2502
|
+
h += summaryRow(label, w.byHand || "By hand", "sched-text", false);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
box.innerHTML = h;
|
|
2398
2506
|
}
|
|
2399
2507
|
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2400
|
-
// it ended in a way the engine would accept (opens the Chat card's resume mode),
|
|
2401
|
-
// The Ended block (
|
|
2508
|
+
// it ended in a way the engine would accept (opens the Chat card's resume mode), Reuse workflow on every ending.
|
|
2509
|
+
// The Ended block (Run tab, above this row) is text only and carries no buttons.
|
|
2402
2510
|
var stopAsked = {};
|
|
2403
2511
|
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
2404
2512
|
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2405
2513
|
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2406
|
-
function reuseWorkflow(runid, model){
|
|
2514
|
+
function reuseWorkflow(runid, model, label){
|
|
2407
2515
|
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&" + model : "")).then(function(r){
|
|
2408
2516
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2409
2517
|
document.getElementById("tab-chat").click();
|
|
2410
|
-
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2518
|
+
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r, label: label || ""}}));
|
|
2411
2519
|
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2412
2520
|
}
|
|
2413
|
-
//
|
|
2414
|
-
//
|
|
2415
|
-
//
|
|
2416
|
-
function
|
|
2417
|
-
var
|
|
2418
|
-
|
|
2419
|
-
". Every box is open. Change anything and it drafts as a new job; change nothing and it is another run of the same job.";
|
|
2420
|
-
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2521
|
+
// Schedule run / Cancel / modify schedule (T Part 7): starts nothing and signs nothing. It opens the SAME Chat card as the neighbouring Reuse
|
|
2522
|
+
// workflow button, in schedule mode (the When field showing the current schedule). The run it copies from is the server's answer
|
|
2523
|
+
// (`detail.schedule.fromRunid`: the green the schedule was made from), so a red run of a scheduled job still opens the card.
|
|
2524
|
+
function scheduleInChat(detail){
|
|
2525
|
+
var from = (detail.schedule && detail.schedule.fromRunid) || detail.runid;
|
|
2526
|
+
authorGet("/api/author/start-from?mode=schedule&runid=" + encodeURIComponent(from)).then(function(r){
|
|
2421
2527
|
document.getElementById("tab-chat").click();
|
|
2422
|
-
document.dispatchEvent(new CustomEvent("bareloop-
|
|
2528
|
+
document.dispatchEvent(new CustomEvent("bareloop-schedule", {detail: (r && r.ok) ? {runid: from, prefill: r, job: detail.job} : {error: (r && r.error) || "nothing to schedule"}}));
|
|
2423
2529
|
}).catch(function(x){
|
|
2424
2530
|
document.getElementById("tab-chat").click();
|
|
2425
|
-
document.dispatchEvent(new CustomEvent("bareloop-
|
|
2531
|
+
document.dispatchEvent(new CustomEvent("bareloop-schedule", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2426
2532
|
});
|
|
2427
2533
|
}
|
|
2428
2534
|
// Resume (hamr 2026-10-07, option B — moved from the run's Job tab): the Chat card opens in RESUME mode, filled from the
|
|
@@ -2437,6 +2543,19 @@
|
|
|
2437
2543
|
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2438
2544
|
});
|
|
2439
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
|
+
}
|
|
2440
2559
|
function renderRunActions(detail){
|
|
2441
2560
|
var bar = document.getElementById("run-actions");
|
|
2442
2561
|
var html = "";
|
|
@@ -2452,20 +2571,46 @@
|
|
|
2452
2571
|
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2453
2572
|
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2454
2573
|
}
|
|
2455
|
-
//
|
|
2456
|
-
|
|
2457
|
-
|
|
2574
|
+
// Accept (T Part 8): right after Resume, before Reuse workflow. Shown only when the SERVER says this run waits at its review door
|
|
2575
|
+
// (`detail.accept`: a door record, not expired, no honoured decision after it). It records the person's accept through the engine's own door.
|
|
2576
|
+
if(!live && detail.accept === true){
|
|
2577
|
+
html += '<button class="btn primary" type="button" data-testid="btn-accept-run">Accept</button>';
|
|
2458
2578
|
}
|
|
2459
|
-
// Reuse workflow:
|
|
2460
|
-
if(!live &&
|
|
2579
|
+
// Reuse workflow: ONE door on every ending (v1.101), a died run included - the Chat card opens with every box open
|
|
2580
|
+
if(!live && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2461
2581
|
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
2462
2582
|
}
|
|
2583
|
+
// T Part 7: the schedule button is LAST, right after Reuse workflow. The SERVER decides (detail.schedule): `scheduled` shows
|
|
2584
|
+
// Cancel / modify schedule (every run of the job, also while one is live: cancel is never refused); else `canSchedule` shows
|
|
2585
|
+
// Schedule run; else no button. The label never changes in place — a different state is a different button.
|
|
2586
|
+
var sc = detail.schedule || null;
|
|
2587
|
+
var scw = (sc && sc.words) || {};
|
|
2588
|
+
if(sc && sc.scheduled){
|
|
2589
|
+
html += '<button class="btn" type="button" data-testid="run-cancel-schedule">' + escapeXml(scw.cancelModify || "") + '</button>';
|
|
2590
|
+
} else if(sc && sc.canSchedule){
|
|
2591
|
+
html += '<button class="btn" type="button" data-testid="run-schedule">' + escapeXml(scw.scheduleRun || "") + '</button>';
|
|
2592
|
+
}
|
|
2593
|
+
// RULED 2026-10-15: Remove from Needs you is LAST. The SERVER says when it shows (a stopped run in Needs you, never a live Accept, never a running run).
|
|
2594
|
+
if(!live && detail.canRemoveFromNeeds === true){
|
|
2595
|
+
html += '<button class="btn" type="button" data-testid="btn-needs-remove">Remove from Needs you</button>';
|
|
2596
|
+
}
|
|
2463
2597
|
bar.innerHTML = html;
|
|
2464
2598
|
bar.hidden = html === "";
|
|
2599
|
+
var needsBtn = bar.querySelector('[data-testid="btn-needs-remove"]');
|
|
2600
|
+
if(needsBtn){
|
|
2601
|
+
needsBtn.addEventListener("click", function(){
|
|
2602
|
+
needsBtn.disabled = true;
|
|
2603
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/needs-remove", {}).then(function(res){
|
|
2604
|
+
if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
|
|
2605
|
+
needsBtn.disabled = false;
|
|
2606
|
+
bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="needs-remove-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
|
|
2607
|
+
}).catch(function(x){ needsBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="needs-remove-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
var schedBtn = bar.querySelector('[data-testid="run-schedule"], [data-testid="run-cancel-schedule"]');
|
|
2611
|
+
if(schedBtn) schedBtn.addEventListener("click", function(){ scheduleInChat(detail); });
|
|
2465
2612
|
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2466
|
-
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2467
|
-
var editBtn = bar.querySelector('[data-testid="btn-edit-chat"]');
|
|
2468
|
-
if(editBtn) editBtn.addEventListener("click", function(){ editInChat(detail); });
|
|
2613
|
+
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid, "", runName(detail.job, detail.runNo, detail.runid)); });
|
|
2469
2614
|
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2470
2615
|
if(stopBtn){
|
|
2471
2616
|
stopBtn.addEventListener("click", function(){
|
|
@@ -2476,6 +2621,8 @@
|
|
|
2476
2621
|
}).catch(function(x){ stopBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="stop-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
|
|
2477
2622
|
});
|
|
2478
2623
|
}
|
|
2624
|
+
var accBtn = bar.querySelector('[data-testid="btn-accept-run"]');
|
|
2625
|
+
if(accBtn) accBtn.addEventListener("click", function(){ acceptDoor(detail, accBtn, bar); });
|
|
2479
2626
|
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2480
2627
|
if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
|
|
2481
2628
|
}
|
|
@@ -2487,7 +2634,7 @@
|
|
|
2487
2634
|
if(currentRunid !== runid) return;
|
|
2488
2635
|
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(d){
|
|
2489
2636
|
if(currentRunid !== runid) return;
|
|
2490
|
-
if(d.glyph === "▶" && !d.died){ selectRun(runid
|
|
2637
|
+
if(d.glyph === "▶" && !d.died){ selectRun(runid); return; }
|
|
2491
2638
|
if(tries > 0) afterResumeRefresh(runid, tries - 1);
|
|
2492
2639
|
}).catch(function(){ if(tries > 0) afterResumeRefresh(runid, tries - 1); });
|
|
2493
2640
|
}, 1500);
|
|
@@ -2508,6 +2655,7 @@
|
|
|
2508
2655
|
|
|
2509
2656
|
function renderRun(detail){
|
|
2510
2657
|
if(detail.fileMissing){
|
|
2658
|
+
syncResultTab({});
|
|
2511
2659
|
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
2512
2660
|
document.getElementById("run-empty").hidden = false;
|
|
2513
2661
|
document.getElementById("run-empty").textContent = "spine file no longer on disk";
|
|
@@ -2518,13 +2666,29 @@
|
|
|
2518
2666
|
return;
|
|
2519
2667
|
}
|
|
2520
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
|
+
|
|
2521
2682
|
if(detail.starting){
|
|
2522
|
-
|
|
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)));
|
|
2523
2687
|
document.getElementById("run-empty").hidden = false;
|
|
2524
|
-
document.getElementById("run-empty").textContent =
|
|
2688
|
+
document.getElementById("run-empty").textContent = startWords;
|
|
2525
2689
|
document.getElementById("run-content").hidden = true;
|
|
2526
2690
|
document.getElementById("audit-select-empty").hidden = false;
|
|
2527
|
-
document.getElementById("audit-select-empty").textContent =
|
|
2691
|
+
document.getElementById("audit-select-empty").textContent = startWords;
|
|
2528
2692
|
document.getElementById("audit-body").hidden = true;
|
|
2529
2693
|
return;
|
|
2530
2694
|
}
|
|
@@ -2532,7 +2696,7 @@
|
|
|
2532
2696
|
document.getElementById("audit-body").hidden = false;
|
|
2533
2697
|
renderEnded(detail);
|
|
2534
2698
|
|
|
2535
|
-
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), runHeaderBody(detail));
|
|
2699
|
+
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), runHeaderBody(detail), detail.accept ? "Accept" : null);
|
|
2536
2700
|
|
|
2537
2701
|
document.getElementById("run-empty").hidden = true;
|
|
2538
2702
|
document.getElementById("run-content").hidden = false;
|
|
@@ -2584,7 +2748,7 @@
|
|
|
2584
2748
|
" · " + wallPhrase;
|
|
2585
2749
|
|
|
2586
2750
|
var box = document.getElementById("run-summary");
|
|
2587
|
-
box.className = "summary-box " + (detail.died ? "magenta" : (
|
|
2751
|
+
box.className = "summary-box " + (detail.died ? "magenta" : (detail.glyph === "✗" ? "red" : (detail.glyph === "■" ? "grey" : (detail.glyph === "▶" ? "amber" : ""))));
|
|
2588
2752
|
// F196: a synthetic placeholder box (e.g. "died during planning") is a
|
|
2589
2753
|
// LABEL, not a real step — excluded from this count so a run that died
|
|
2590
2754
|
// before any step started reads "0 of 0 done" / "none started", never
|
|
@@ -2602,10 +2766,13 @@
|
|
|
2602
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>' +
|
|
2603
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>' +
|
|
2604
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>' : '') +
|
|
2605
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>' : '') +
|
|
2606
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>';
|
|
2607
2773
|
|
|
2608
2774
|
paintOfferedRow();
|
|
2775
|
+
syncResultTab(detail);
|
|
2609
2776
|
|
|
2610
2777
|
// build item B (2026-09-26 rewrite): the map AND the card list are both
|
|
2611
2778
|
// built from ONE list — `detail.parts` (src/replay.js's own ordered part
|
|
@@ -2640,9 +2807,11 @@
|
|
|
2640
2807
|
'<div class="step-head"><h4>' + titleText + '</h4>' +
|
|
2641
2808
|
stepStateHTML(box, 'part-state-' + idx) +
|
|
2642
2809
|
(box.blocked > 0 ? '<span class="badge red" data-testid="part-blocked-badge">' + box.blocked + ' blocked</span>' : '') +
|
|
2810
|
+
actorMarkHtml(part) +
|
|
2643
2811
|
'</div>' +
|
|
2644
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>' +
|
|
2645
|
-
'<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>' : '');
|
|
2646
2815
|
listEl.appendChild(card);
|
|
2647
2816
|
var jumpToAudit = function(){
|
|
2648
2817
|
document.getElementById("tab-audit").click();
|
|
@@ -2664,7 +2833,11 @@
|
|
|
2664
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
|
|
2665
2834
|
function stepStateHTML(box, testid){
|
|
2666
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")));
|
|
2667
|
-
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>';
|
|
2668
2841
|
}
|
|
2669
2842
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2670
2843
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
@@ -2684,6 +2857,7 @@
|
|
|
2684
2857
|
row.innerHTML =
|
|
2685
2858
|
'<button class="btn small attempt-toggle" type="button" aria-expanded="false">' +
|
|
2686
2859
|
'attempt ' + a.n + ' ' + attemptGlyph(a.outcome, boxIsLive) +
|
|
2860
|
+
(a.thinking ? ' · ' + escapeXml(a.thinking) : '') +
|
|
2687
2861
|
(checksText ? ' · ' + checksText : '') +
|
|
2688
2862
|
'</button>' +
|
|
2689
2863
|
(checksList ? '<div class="checks-list" data-testid="audit-attempt-checks-' + partIndex + '-' + a.n + '" hidden>' + checksList + '</div>' : '') +
|
|
@@ -2706,6 +2880,90 @@
|
|
|
2706
2880
|
});
|
|
2707
2881
|
}
|
|
2708
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
|
+
|
|
2709
2967
|
function roundsUrl(partIndex, attempt, offset){
|
|
2710
2968
|
var params = "part=" + encodeURIComponent(partIndex) + "&attempt=" + encodeURIComponent(attempt.n) + "&offset=" + offset;
|
|
2711
2969
|
return "/api/runs/" + encodeURIComponent(currentRunid) + "/rounds?" + params;
|
|
@@ -2815,6 +3073,7 @@
|
|
|
2815
3073
|
}
|
|
2816
3074
|
// line 2 of a part's card / Audit body: the drafting step names, or a worker part's tool breakdown
|
|
2817
3075
|
function partLine2Text(part){
|
|
3076
|
+
if(part && part.kind === "door") return part.doorView ? "last answer: " + part.doorView.last : "";
|
|
2818
3077
|
if(part && part.kind === "drafting") return (part.draftSteps || []).map(function(x){ return x.label; }).join(" · ");
|
|
2819
3078
|
return toolBreakdownLine(part.byTool);
|
|
2820
3079
|
}
|
|
@@ -2837,6 +3096,10 @@
|
|
|
2837
3096
|
rows += "<tr><td>" + escapeXml(st.label) + " (" + escapeXml(c.label) + ")</td><td>–</td><td>1</td><td>" +
|
|
2838
3097
|
(c.model ? escapeXml(c.model) : "–") + "</td><td>" + draftingCallCost(c) + "</td></tr>";
|
|
2839
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
|
+
});
|
|
2840
3103
|
});
|
|
2841
3104
|
rows += '<tr class="drafting-total"><td><b>total</b></td><td>' + (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) +
|
|
2842
3105
|
"</td><td>" + (typeof part.rounds === "number" ? part.rounds : 0) + "</td><td>–</td><td>" + draftingMoneyText(part) + "</td></tr>";
|
|
@@ -2873,6 +3136,7 @@
|
|
|
2873
3136
|
*/
|
|
2874
3137
|
function partLine1Text(part, detail){
|
|
2875
3138
|
if(part && part.kind === "drafting") return draftingLine1Text(part);
|
|
3139
|
+
if(part && part.kind === "door") return part.doorView ? escapeXml(part.doorView.head) : "";
|
|
2876
3140
|
var isLiveRunPart = part && part.kind === "run" && detail && !detail.died && detail.spentUsd === null;
|
|
2877
3141
|
var wallText = (isLiveRunPart && part.wallMs === null) ? liveWallPhrase(detail.wallFloorMs) : duration(part.wallMs);
|
|
2878
3142
|
var spendText = (isLiveRunPart && part.spentUsd === null)
|
|
@@ -2888,6 +3152,8 @@
|
|
|
2888
3152
|
// MOST RECENT attempt's own declared-close grading (the opening
|
|
2889
3153
|
// outer-close grade counts as attempt 1, same list `buildAttemptsList`
|
|
2890
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));
|
|
2891
3157
|
var latestAttempt = Array.isArray(part.attempts) && part.attempts.length ? part.attempts[part.attempts.length - 1] : null;
|
|
2892
3158
|
var headline = latestAttempt ? attemptChecksLine(latestAttempt) : "";
|
|
2893
3159
|
if(headline) bits.push(headline);
|
|
@@ -3059,6 +3325,57 @@
|
|
|
3059
3325
|
var isLiveRun = detail.glyph === "▶" && !detail.died;
|
|
3060
3326
|
var boxes = buildOrderedBoxes(parts, isLiveRun);
|
|
3061
3327
|
var numberMap = stepNumberIndices(boxes);
|
|
3328
|
+
// RULED 2026-10-15 (v1.104): ONE `schedule` section at the top of the Audit of every run of a job that has a schedule now, always folded when the page
|
|
3329
|
+
// opens, worded on the server (`detail.scheduleSection`: title, sign, head, rows [{label,text}]). A run a CANCELLED schedule started keeps its one old line.
|
|
3330
|
+
if(typeof detail.startedAudit === "string" && detail.startedAudit !== ""){
|
|
3331
|
+
var startedEl = document.createElement("div");
|
|
3332
|
+
startedEl.className = "hint";
|
|
3333
|
+
startedEl.setAttribute("data-testid", "audit-started");
|
|
3334
|
+
startedEl.style.overflowWrap = "anywhere";
|
|
3335
|
+
startedEl.textContent = detail.startedAudit;
|
|
3336
|
+
container.appendChild(startedEl);
|
|
3337
|
+
}
|
|
3338
|
+
var sec = detail.scheduleSection;
|
|
3339
|
+
if(sec && Array.isArray(sec.rows) && sec.rows.length){
|
|
3340
|
+
var gWrap = document.createElement("div");
|
|
3341
|
+
gWrap.className = "audit-part";
|
|
3342
|
+
gWrap.setAttribute("data-testid", "audit-schedule-group");
|
|
3343
|
+
var gBtn = document.createElement("button");
|
|
3344
|
+
gBtn.type = "button";
|
|
3345
|
+
gBtn.className = "btn audit-part-toggle";
|
|
3346
|
+
gBtn.setAttribute("aria-expanded", "false");
|
|
3347
|
+
gBtn.setAttribute("data-testid", "audit-schedule-toggle");
|
|
3348
|
+
var secHead = function(open){ return (open ? "\u25BE " : "\u25B8 ") + stateDotHTML(glyphClass(sec.sign), sec.word, "audit-schedule-state") + " " + escapeXml(sec.title) + " · " + escapeXml(sec.head); };
|
|
3349
|
+
gBtn.innerHTML = secHead(false);
|
|
3350
|
+
var gBody = document.createElement("div");
|
|
3351
|
+
gBody.className = "audit-part-body";
|
|
3352
|
+
gBody.hidden = true;
|
|
3353
|
+
gBody.setAttribute("data-testid", "audit-schedule-body");
|
|
3354
|
+
// same row look as the drafting/step tables: the `rounds-table` in the `audit-table-scroll` wrapper (label column, dotted row lines)
|
|
3355
|
+
var schedRows = "";
|
|
3356
|
+
sec.rows.forEach(function(l){
|
|
3357
|
+
schedRows += '<tr><td class="hint" style="white-space:nowrap">' + escapeXml(l.label) + '</td><td style="overflow-wrap:anywhere;white-space:pre-wrap">' + escapeXml(l.text) + '</td></tr>';
|
|
3358
|
+
});
|
|
3359
|
+
gBody.innerHTML = '<div class="audit-table-scroll"><table class="rounds-table" data-testid="audit-schedule-table"><tbody>' + schedRows + '</tbody></table></div>';
|
|
3360
|
+
gBtn.addEventListener("click", function(){
|
|
3361
|
+
var open = gBtn.getAttribute("aria-expanded") === "true";
|
|
3362
|
+
gBtn.setAttribute("aria-expanded", open ? "false" : "true");
|
|
3363
|
+
gBody.hidden = open;
|
|
3364
|
+
gBtn.innerHTML = secHead(!open);
|
|
3365
|
+
});
|
|
3366
|
+
gWrap.appendChild(gBtn);
|
|
3367
|
+
gWrap.appendChild(gBody);
|
|
3368
|
+
container.appendChild(gWrap);
|
|
3369
|
+
}
|
|
3370
|
+
// RULED 2026-10-15: how the run left Needs you (by you, or after 7 days), worded on the server
|
|
3371
|
+
if(typeof detail.needsAudit === "string" && detail.needsAudit !== ""){
|
|
3372
|
+
var needsEl = document.createElement("div");
|
|
3373
|
+
needsEl.className = "hint";
|
|
3374
|
+
needsEl.setAttribute("data-testid", "audit-needs-removed");
|
|
3375
|
+
needsEl.style.overflowWrap = "anywhere";
|
|
3376
|
+
needsEl.textContent = detail.needsAudit;
|
|
3377
|
+
container.appendChild(needsEl);
|
|
3378
|
+
}
|
|
3062
3379
|
// P5-R: a resumed run is ONE run — its legs are told apart by a divider line (code-owned text from the server)
|
|
3063
3380
|
var dividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
3064
3381
|
boxes.forEach(function(box, idx){
|
|
@@ -3080,7 +3397,8 @@
|
|
|
3080
3397
|
headBtn.innerHTML =
|
|
3081
3398
|
stepStateHTML(box, 'audit-part-state-' + idx) + ' ' + titleText + ' · ' + partLine1Text(part, detail) +
|
|
3082
3399
|
(groupResultGlyph !== null ? ' · ' + groupResultGlyph : '') +
|
|
3083
|
-
(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);
|
|
3084
3402
|
var body = document.createElement("div");
|
|
3085
3403
|
body.className = "audit-part-body";
|
|
3086
3404
|
body.hidden = !startExpanded;
|
|
@@ -3099,6 +3417,12 @@
|
|
|
3099
3417
|
var draftingEl = document.createElement("div");
|
|
3100
3418
|
draftingEl.innerHTML = draftingTableHtml(part);
|
|
3101
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");
|
|
3102
3426
|
} else if(attempts.length > 1){
|
|
3103
3427
|
// parts WITH an attempt sub-loop (a multi-attempt step, or the fix
|
|
3104
3428
|
// loop) expand to attempt rows first — each attempt expands to its
|
|
@@ -3291,6 +3615,14 @@
|
|
|
3291
3615
|
var verdictEl = document.getElementById("details-verdict");
|
|
3292
3616
|
if(job.checkTypeTitle){ verdictEl.title = job.checkTypeTitle; } else { verdictEl.removeAttribute("title"); }
|
|
3293
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 : "";
|
|
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)
|
|
3621
|
+
[["started", job.started], ["schedule", job.schedule]].forEach(function(p){
|
|
3622
|
+
var f = document.getElementById("details-" + p[0] + "-field");
|
|
3623
|
+
if(typeof p[1] === "string" && p[1] !== ""){ f.hidden = false; document.getElementById("details-" + p[0]).textContent = p[1]; }
|
|
3624
|
+
else { f.hidden = true; }
|
|
3625
|
+
});
|
|
3294
3626
|
var descField = document.getElementById("details-description-field");
|
|
3295
3627
|
if(job.description){ descField.hidden = false; document.getElementById("details-description").textContent = job.description; }
|
|
3296
3628
|
else { descField.hidden = true; }
|
|
@@ -3311,6 +3643,211 @@
|
|
|
3311
3643
|
paintOfferedRow();
|
|
3312
3644
|
}
|
|
3313
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
|
+
|
|
3314
3851
|
// ---- selection + loading ----
|
|
3315
3852
|
var currentRunid = null;
|
|
3316
3853
|
// P2: guards a stale in-flight response (detail/audit/job, OR the poll
|
|
@@ -3322,22 +3859,30 @@
|
|
|
3322
3859
|
// from the run's own `glyph`/`died` fields, same rule `renderRun` already
|
|
3323
3860
|
// uses for `isLive` — never re-derived a second way).
|
|
3324
3861
|
var runIsLive = false;
|
|
3325
|
-
|
|
3862
|
+
// a FINISHED run of a scheduled job is still refetched on the list cadence: its SCHEDULED / NEXT block changes without the run
|
|
3863
|
+
// changing (a refused fire turns NEXT red, a slot passes), and nothing else would repaint it
|
|
3864
|
+
var openIsScheduled = false;
|
|
3865
|
+
var lastOpenFetchAt = 0;
|
|
3866
|
+
function selectRun(runid){
|
|
3326
3867
|
hideImportView();
|
|
3327
3868
|
clearSelection();
|
|
3328
|
-
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
3329
|
-
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
3330
3869
|
currentRunid = runid;
|
|
3870
|
+
paintRunsList(); // the list marks the selected run itself (rl-sel); it writes only when the markup changed
|
|
3331
3871
|
lastLegDividers = [];
|
|
3332
3872
|
lastJobToolsList = null;
|
|
3333
3873
|
lastRunDetailSig = null;
|
|
3334
3874
|
runIsLive = false;
|
|
3875
|
+
openIsScheduled = false;
|
|
3876
|
+
lastOpenFetchAt = Date.now();
|
|
3877
|
+
clearResult();
|
|
3335
3878
|
var myToken = ++selectToken;
|
|
3336
3879
|
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(detail){
|
|
3337
3880
|
if(myToken !== selectToken) return; // a different run was selected before this resolved
|
|
3338
3881
|
lastRunDetailSig = sig(detail);
|
|
3339
3882
|
runIsLive = detail.glyph === "▶" && !detail.died;
|
|
3883
|
+
openIsScheduled = !!(detail.schedule && detail.schedule.scheduled);
|
|
3340
3884
|
renderRun(detail);
|
|
3885
|
+
if(detail.hasResult === true) loadResult(runid);
|
|
3341
3886
|
}).catch(function(e){
|
|
3342
3887
|
if(myToken !== selectToken) return;
|
|
3343
3888
|
document.getElementById("run-empty").hidden = false;
|
|
@@ -3389,6 +3934,7 @@
|
|
|
3389
3934
|
var RUNS_LIST_POLL_MS = 10000;
|
|
3390
3935
|
var pollTimer = null;
|
|
3391
3936
|
var lastRunsSig = null;
|
|
3937
|
+
var lastRunsPayloadSig = null;
|
|
3392
3938
|
var lastRunDetailSig = null;
|
|
3393
3939
|
var lastRunsListRefreshAt = 0;
|
|
3394
3940
|
var pollFailStreak = 0;
|
|
@@ -3420,14 +3966,14 @@
|
|
|
3420
3966
|
return getJSON("/api/runs").then(function(result){
|
|
3421
3967
|
pollFailStreak = 0;
|
|
3422
3968
|
var runs = result.runs || [];
|
|
3423
|
-
var next = sig(
|
|
3424
|
-
if(next ===
|
|
3425
|
-
|
|
3969
|
+
var next = sig(result);
|
|
3970
|
+
if(next === lastRunsPayloadSig) return runs; // unchanged payload — never rebuild the DOM for nothing
|
|
3971
|
+
lastRunsPayloadSig = next;
|
|
3972
|
+
lastRunsSig = sig(runs);
|
|
3426
3973
|
withScrollPreserved(["left-pane-body"], function(){
|
|
3427
|
-
//
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
runsFilterBar.setItems(runs);
|
|
3974
|
+
// the painter re-runs the SAME filter/search state — a poll tick never resets it, and it writes a box only when its markup changed.
|
|
3975
|
+
setRunsData(result);
|
|
3976
|
+
runsFilterBar.refresh();
|
|
3431
3977
|
});
|
|
3432
3978
|
return runs;
|
|
3433
3979
|
}).catch(function(){
|
|
@@ -3443,7 +3989,10 @@
|
|
|
3443
3989
|
}
|
|
3444
3990
|
|
|
3445
3991
|
function refreshOpenRun(){
|
|
3446
|
-
if(!currentRunid
|
|
3992
|
+
if(!currentRunid) return Promise.resolve();
|
|
3993
|
+
var scheduleDue = openIsScheduled && (Date.now() - lastOpenFetchAt) >= RUNS_LIST_POLL_MS;
|
|
3994
|
+
if(!runIsLive && !scheduleDue) return Promise.resolve();
|
|
3995
|
+
lastOpenFetchAt = Date.now();
|
|
3447
3996
|
var runid = currentRunid;
|
|
3448
3997
|
var myToken = selectToken;
|
|
3449
3998
|
return getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(detail){
|
|
@@ -3454,6 +4003,7 @@
|
|
|
3454
4003
|
var changed = next !== lastRunDetailSig;
|
|
3455
4004
|
lastRunDetailSig = next;
|
|
3456
4005
|
runIsLive = detail.glyph === "▶" && !detail.died;
|
|
4006
|
+
openIsScheduled = !!(detail.schedule && detail.schedule.scheduled);
|
|
3457
4007
|
if(changed){
|
|
3458
4008
|
withScrollPreserved(["right-pane-body"], function(){ renderRun(detail); });
|
|
3459
4009
|
}
|
|
@@ -3462,6 +4012,7 @@
|
|
|
3462
4012
|
|
|
3463
4013
|
function pollTick(forceList){
|
|
3464
4014
|
if(document.hidden) return;
|
|
4015
|
+
tickSince(); // Running now: the wall counts up in place
|
|
3465
4016
|
refreshRunsList(forceList);
|
|
3466
4017
|
var wasLive = runIsLive;
|
|
3467
4018
|
return refreshOpenRun().then(function(){
|
|
@@ -3487,25 +4038,17 @@
|
|
|
3487
4038
|
getJSON("/api/runs").then(function(result){
|
|
3488
4039
|
var runs = result.runs || [];
|
|
3489
4040
|
lastRunsSig = sig(runs);
|
|
3490
|
-
|
|
4041
|
+
lastRunsPayloadSig = sig(result);
|
|
4042
|
+
setRunsData(result);
|
|
4043
|
+
runsFilterBar.refresh();
|
|
3491
4044
|
if(runs.length && !runs[0].fileMissing){
|
|
3492
|
-
//
|
|
3493
|
-
|
|
3494
|
-
// away from Chat on every load that had a run). The RIGHT pane still
|
|
3495
|
-
// loads the newest run's details so they're ready the moment a
|
|
3496
|
-
// person switches to the Runs tab themselves — the Workflows view
|
|
3497
|
-
// starts collapsed, but its job row still renders "selected" (build
|
|
3498
|
-
// item, 2026-09-26: a job row shows selected state like a child row)
|
|
3499
|
-
// since `runsFilterBar.refresh()` re-renders AFTER `currentRunid` is
|
|
3500
|
-
// set.
|
|
3501
|
-
selectRun(runs[0].runid, null, ".hist-row");
|
|
4045
|
+
// the LEFT pane opens on Chat by default; the RIGHT pane loads the newest run so it is ready when a person opens the Runs tab.
|
|
4046
|
+
selectRun(runs[0].runid);
|
|
3502
4047
|
document.getElementById("tab-run").click();
|
|
3503
|
-
runsFilterBar.refresh();
|
|
3504
4048
|
}
|
|
3505
4049
|
startPolling();
|
|
3506
4050
|
}).catch(function(e){
|
|
3507
4051
|
document.getElementById("wf-list").innerHTML = '<div class="hint">failed to load: ' + escapeXml(e.message) + '</div>';
|
|
3508
|
-
document.getElementById("history-list").innerHTML = '<div class="hint">failed to load: ' + escapeXml(e.message) + '</div>';
|
|
3509
4052
|
startPolling(); // still start — a transient initial failure shouldn't block all future polling
|
|
3510
4053
|
});
|
|
3511
4054
|
|
|
@@ -3957,6 +4500,18 @@
|
|
|
3957
4500
|
// Reuse workflow — set when the card was opened by [Reuse workflow]: the run (or imported job) it came from.
|
|
3958
4501
|
// Only Source, Destination and the two caps are open on that card; the SERVER refuses any other change.
|
|
3959
4502
|
var startFrom = null;
|
|
4503
|
+
// ONE door (v1.101): what the card was prefilled from, so the page can tell whether a JOB box changed. {card, green, name, nextName}
|
|
4504
|
+
// `name` = the origin's job name (shown while no job box changed), `nextName` = the server's next free <base>-N (shown once one does).
|
|
4505
|
+
var reuseOrigin = null;
|
|
4506
|
+
var nameTouched = false; // the person typed their own name for a changed job
|
|
4507
|
+
// T Part 7: the card in SCHEDULE mode (opened by Schedule run / Cancel-modify schedule): the Reuse card plus the When field.
|
|
4508
|
+
// `sched` = {job, runid, words, cur, scheduled, refusal, now} while it is on, else null. `cur` = the signed trigger (the When field's
|
|
4509
|
+
// starting value); every word comes from `words` (the server's one table, src/schedulewords.js SCHEDULE_WORDS).
|
|
4510
|
+
var sched = null;
|
|
4511
|
+
var whenHash = null; // the hash click 1 returned; null = click 1 not done (or a When / card box was edited since)
|
|
4512
|
+
var whenConfirm = null; // the lower-cased words the label shows after click 1: "Confirm: every day at 13:15"
|
|
4513
|
+
var whenBusy = false; // a request is out: the main button dims and cannot be pressed
|
|
4514
|
+
var whenLocked = false; // ~1.5 s after click 2 the main button is dead (hamr clicked Run twice thinking it was the trigger)
|
|
3960
4515
|
// the picked job's OWN check type: with the Check type radio on "Reuse workflow" the card's checkType comes from here, never from the radio
|
|
3961
4516
|
var reuseCheckType = "deterministic";
|
|
3962
4517
|
var reusePickedName = "";
|
|
@@ -3983,7 +4538,8 @@
|
|
|
3983
4538
|
// the one exception on a resume card: an older spec that names no model leaves the Model box open (picked at resume)
|
|
3984
4539
|
var modelOpen = resuming && resumeRun.pickModel === true;
|
|
3985
4540
|
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1 && !(modelOpen && id === "jf-model"))); });
|
|
3986
|
-
|
|
4541
|
+
// a schedule card keeps the Model the green run had (T9): only Inputs, Destination, When and the two caps are open
|
|
4542
|
+
document.getElementById("jf-model").disabled = live || (resuming && !modelOpen) || !!sched;
|
|
3987
4543
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3988
4544
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3989
4545
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
@@ -3998,7 +4554,8 @@
|
|
|
3998
4554
|
NUMBERED_IDS.forEach(gutterSync);
|
|
3999
4555
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
4000
4556
|
}
|
|
4001
|
-
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
4557
|
+
startFrom = null; reuseOrigin = null; nameTouched = false; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
4558
|
+
scheduleOff();
|
|
4002
4559
|
sfLine.hidden = true; sfNote.hidden = true; originLine.hidden = true;
|
|
4003
4560
|
setReuseLocked(false);
|
|
4004
4561
|
renderPicker();
|
|
@@ -4040,7 +4597,6 @@
|
|
|
4040
4597
|
rwSearch.hidden = picked;
|
|
4041
4598
|
rwPicked.hidden = !picked;
|
|
4042
4599
|
rwPickedTxt.innerHTML = picked ? "<b>" + escapeXml(reusePickedName) + "</b>" : "";
|
|
4043
|
-
if(!picked) sfLine.hidden = true;
|
|
4044
4600
|
}
|
|
4045
4601
|
function rwMark(name, q){
|
|
4046
4602
|
var i = q ? name.toLowerCase().indexOf(q.toLowerCase()) : -1;
|
|
@@ -4226,13 +4782,18 @@
|
|
|
4226
4782
|
// Check type: Deterministic / Rubric / Reuse workflow. Reuse opens the picker on an empty card; Deterministic or
|
|
4227
4783
|
// Rubric while the picker is showing (picked or not) LEAVES reuse — boxes untouched (clearStartFrom(true)).
|
|
4228
4784
|
function onVerdictChange(v){
|
|
4229
|
-
if(v === "reuse"){
|
|
4785
|
+
if(v === "reuse"){
|
|
4786
|
+
if(startFrom !== null){ clearStartFrom(true); setVerdict("reuse"); openPicker(); return; }
|
|
4787
|
+
if(rwBox.hidden) openPicker();
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4230
4790
|
if(!rwBox.hidden){
|
|
4231
4791
|
clearStartFrom(true);
|
|
4232
4792
|
setVerdict(v);
|
|
4233
4793
|
renderPicker();
|
|
4234
4794
|
}
|
|
4235
4795
|
setJobPlaceholders(v);
|
|
4796
|
+
syncReuseName();
|
|
4236
4797
|
refreshStartEnabled();
|
|
4237
4798
|
}
|
|
4238
4799
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
@@ -4479,6 +5040,22 @@
|
|
|
4479
5040
|
var ask = st.pendingAsk;
|
|
4480
5041
|
var atMenu = !!ask && ask.kind === "menu";
|
|
4481
5042
|
var hint = "";
|
|
5043
|
+
// T Part 7 (hamr found these by clicking fwdloop): after click 2 of a schedule the card is the empty New job card and the button is DEAD for ~1.5 s,
|
|
5044
|
+
// reading "Start drafting" — never a Run / Sign & run invitation
|
|
5045
|
+
if(!st.session && st.whenLocked){
|
|
5046
|
+
return {text: "Start drafting", action: "none", disabled: true, hint: ""};
|
|
5047
|
+
}
|
|
5048
|
+
// When changed from the signed value = the ONE button turns into Sign schedule (Cancel schedule when the choice is By hand): two clicks, $0, starts
|
|
5049
|
+
// nothing. After click 1 it reads "Confirm: <words>". It never reaches the run routes. Unchanged = disabled, with the plain words that nothing changed.
|
|
5050
|
+
if(!st.session && st.when){
|
|
5051
|
+
var ww = st.when.words || {};
|
|
5052
|
+
var wtext = st.when.confirm ? (ww.confirmPrefix || "") + st.when.confirm : (st.when.cancel ? ww.cancelSchedule : ww.signSchedule);
|
|
5053
|
+
if(st.when.changed){
|
|
5054
|
+
return {text: wtext, action: st.when.confirm ? "when-sign" : "when-sign-prepare", disabled: !!st.when.busy, hint: ""};
|
|
5055
|
+
}
|
|
5056
|
+
// nothing changed: a schedule card has nothing to sign and NEVER falls through to the run button; the plain words say why (a refused card says its own reason)
|
|
5057
|
+
return {text: ww.signSchedule, action: "none", disabled: true, hint: st.when.refused ? "" : (ww.nothingChanged || "")};
|
|
5058
|
+
}
|
|
4482
5059
|
if(!st.session){
|
|
4483
5060
|
return {text: reuse ? "Sign & run" : "Start drafting", action: "start", disabled: !st.startOk, hint: ""};
|
|
4484
5061
|
}
|
|
@@ -4507,7 +5084,8 @@
|
|
|
4507
5084
|
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind === "menu";
|
|
4508
5085
|
}
|
|
4509
5086
|
function renderMain(){
|
|
4510
|
-
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce};
|
|
5087
|
+
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce, whenLocked: whenLocked};
|
|
5088
|
+
if(sched){ st.when = {changed: whenChanged(), refused: !!sched.refusal, cancel: byId("jf-when").value === "hand", confirm: whenConfirm, busy: whenBusy, words: sched.words}; }
|
|
4511
5089
|
if(sessionLive && lastState){
|
|
4512
5090
|
st.phase = lastState.phase; st.pendingAsk = lastState.pendingAsk;
|
|
4513
5091
|
st.specHash = lastState.specHash;
|
|
@@ -4516,7 +5094,7 @@
|
|
|
4516
5094
|
var open = askBoxOpenFor(st);
|
|
4517
5095
|
msgInput.disabled = !open;
|
|
4518
5096
|
if(!open) msgInput.value = "";
|
|
4519
|
-
var b = mainButtonFor(st, !msgInput.value.trim(),
|
|
5097
|
+
var b = mainButtonFor(st, !msgInput.value.trim(), reusePathNow() || verdictValue() === "reuse");
|
|
4520
5098
|
mainBtn.textContent = b.text;
|
|
4521
5099
|
mainBtn.disabled = b.disabled;
|
|
4522
5100
|
mainAction = b.action;
|
|
@@ -4709,7 +5287,7 @@
|
|
|
4709
5287
|
|
|
4710
5288
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4711
5289
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4712
|
-
// ONE owner of filling the card from a prefill (a re-attached session, Reuse,
|
|
5290
|
+
// ONE owner of filling the card from a prefill (a re-attached session, Reuse, Resume): the six boxes P7 left
|
|
4713
5291
|
// (the old Goal/Source/Success/Guardrails/Judge boxes are gone), then every box re-fits and its number gutter re-measures.
|
|
4714
5292
|
function fillCard(c){
|
|
4715
5293
|
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
@@ -4719,20 +5297,27 @@
|
|
|
4719
5297
|
fitCardBoxes();
|
|
4720
5298
|
}
|
|
4721
5299
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4722
|
-
// `bareloop-reuse` event)
|
|
5300
|
+
// `bareloop-reuse` event). ONE DOOR (v1.101): every box is open and prefilled; nothing is locked. Only the schedule card
|
|
5301
|
+
// (`d.lock`) keeps the job boxes locked - it signs the green run's plan as it is.
|
|
4723
5302
|
function applyReuse(d){
|
|
4724
5303
|
var pre = d.prefill || {};
|
|
4725
5304
|
var c = pre.card || {};
|
|
5305
|
+
var lock = d.lock === true;
|
|
4726
5306
|
clearStartFrom();
|
|
4727
5307
|
openNewCard();
|
|
4728
5308
|
startFrom = d.importId ? {importId: d.importId} : {runid: d.runid};
|
|
4729
5309
|
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4730
5310
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4731
|
-
setVerdict("reuse");
|
|
5311
|
+
setVerdict(lock ? "reuse" : reuseCheckType);
|
|
4732
5312
|
if(c.model){ setPick(c); }
|
|
4733
5313
|
fillCard(c);
|
|
4734
|
-
|
|
4735
|
-
|
|
5314
|
+
reuseOrigin = {card: c, green: pre.green === true, name: reusePickedName, nextName: pre.nextName || ""};
|
|
5315
|
+
nameTouched = false;
|
|
5316
|
+
setReuseLocked(lock);
|
|
5317
|
+
if(!lock){
|
|
5318
|
+
originLine.textContent = "Reuse: " + (d.label || reusePickedName);
|
|
5319
|
+
originLine.hidden = false;
|
|
5320
|
+
}
|
|
4736
5321
|
renderPicker();
|
|
4737
5322
|
sfLine.hidden = false;
|
|
4738
5323
|
sfLine.textContent = pre.line ? rwMeta({checkType: reuseCheckType, line: pre.line}) : "";
|
|
@@ -4745,27 +5330,33 @@
|
|
|
4745
5330
|
refreshStartEnabled();
|
|
4746
5331
|
}
|
|
4747
5332
|
document.addEventListener("bareloop-reuse", function(e){ applyReuse(e.detail || {}); });
|
|
4748
|
-
|
|
4749
|
-
//
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
fillCard(c);
|
|
4760
|
-
|
|
4761
|
-
originLine.textContent = d.origin || "";
|
|
4762
|
-
originLine.hidden = !d.origin;
|
|
4763
|
-
renderPicker();
|
|
4764
|
-
refreshModelStatus();
|
|
4765
|
-
refreshCapNote();
|
|
4766
|
-
refreshStartEnabled();
|
|
5333
|
+
// Did a JOB box (the job text, the check type, the thinking tick) change from the run the card came from? The same comparison the
|
|
5334
|
+
// server makes on the start (whitespace ignored): the Run boxes (Inputs, Destination, Model, caps) never count.
|
|
5335
|
+
function jobBoxesChangedNow(){
|
|
5336
|
+
if(reuseOrigin === null) return false;
|
|
5337
|
+
var cur = currentCard(), o = reuseOrigin.card;
|
|
5338
|
+
var t = function(v){ return typeof v === "string" ? v.replace(/\s+/g, "") : ""; };
|
|
5339
|
+
return t(cur.jobText) !== t(o.jobText) || cur.checkType !== (o.checkType === "rubric" ? "rubric" : "deterministic") || (cur.thinking === true) !== (o.thinking === true);
|
|
5340
|
+
}
|
|
5341
|
+
// the reuse path (signed plan verbatim, no drafting, $0): a card made by Reuse workflow from a GREEN run with no job box changed
|
|
5342
|
+
function reusePathNow(){
|
|
5343
|
+
return startFrom !== null && (reuseOn || (reuseOrigin !== null && reuseOrigin.green === true && !jobBoxesChangedNow()));
|
|
4767
5344
|
}
|
|
4768
|
-
|
|
5345
|
+
// the Job name box: the origin's name while no job box changed; the next free <base>-N the moment one does (renamable); the origin's
|
|
5346
|
+
// again when the boxes go back. A name typed while nothing changed is put back (the origin's name is forced).
|
|
5347
|
+
function syncReuseName(){
|
|
5348
|
+
if(reuseOrigin === null || reuseOn) return;
|
|
5349
|
+
var nameEl = document.getElementById("jf-name");
|
|
5350
|
+
if(!jobBoxesChangedNow()){ nameEl.value = reuseOrigin.name; nameTouched = false; }
|
|
5351
|
+
else if(!nameTouched){ nameEl.value = reuseOrigin.nextName; }
|
|
5352
|
+
}
|
|
5353
|
+
function reuseBoxesEdited(){ syncReuseName(); renderMain(); }
|
|
5354
|
+
document.getElementById("jf-job").addEventListener("input", reuseBoxesEdited);
|
|
5355
|
+
thinkingBox.addEventListener("change", reuseBoxesEdited);
|
|
5356
|
+
document.getElementById("jf-name").addEventListener("input", function(){
|
|
5357
|
+
if(reuseOrigin === null || reuseOn) return;
|
|
5358
|
+
if(jobBoxesChangedNow()) nameTouched = true; else syncReuseName();
|
|
5359
|
+
});
|
|
4769
5360
|
|
|
4770
5361
|
// Resume — the card in RESUME mode: the signed job filled and locked, only Cap $ and Time cap open (prefilled from the
|
|
4771
5362
|
// engine's own resume plan, the values the Job tab used to show). A live drafting session is handled exactly as
|
|
@@ -4940,12 +5531,150 @@
|
|
|
4940
5531
|
});
|
|
4941
5532
|
}
|
|
4942
5533
|
|
|
5534
|
+
// ---- T Part 7: the schedule card (Schedule run / Cancel-modify schedule, PANEL-BUILD.md T addendum C.7, design/schedule-mock.html) ----
|
|
5535
|
+
// The Reuse card plus the When field, copied from fwdloop's Run card (index.html:998-1019, 4690-4772). The page works out nothing: the
|
|
5536
|
+
// server's words, refusals and hashes. Click 1 (schedule-prepare) is $0 and signs nothing; click 2 (schedule) signs; NOTHING runs.
|
|
5537
|
+
function byId(id){ return document.getElementById(id); }
|
|
5538
|
+
var WHEN_KINDS = ["hand", "daily", "weekdays", "weekly", "file-updated", "new-file"];
|
|
5539
|
+
function whenTriggerFromBoxes(){
|
|
5540
|
+
var kind = byId("jf-when").value;
|
|
5541
|
+
if(kind === "hand") return null;
|
|
5542
|
+
if(kind === "daily" || kind === "weekdays" || kind === "weekly"){
|
|
5543
|
+
var m = /^(\d{2}):(\d{2})$/.exec(byId("jf-when-time").value);
|
|
5544
|
+
var o = m ? {kind: kind, hour: Number(m[1]), minute: Number(m[2])} : {kind: kind}; // an empty time goes to the server, which says so
|
|
5545
|
+
if(kind === "weekly") o.day = byId("jf-when-day").value;
|
|
5546
|
+
return o;
|
|
5547
|
+
}
|
|
5548
|
+
var q = {kind: kind};
|
|
5549
|
+
q[kind === "new-file" ? "folder" : "path"] = byId("jf-when-path").value.trim();
|
|
5550
|
+
return q;
|
|
5551
|
+
}
|
|
5552
|
+
// one comparable spelling of a trigger (or By hand), so "changed" is a plain string compare
|
|
5553
|
+
function whenCanon(t){
|
|
5554
|
+
if(!t) return "hand";
|
|
5555
|
+
return [t.kind, t.hour === undefined ? "" : t.hour, t.minute === undefined ? "" : t.minute, t.day || "", t.path || t.folder || ""].join("|");
|
|
5556
|
+
}
|
|
5557
|
+
// Is the card changed from what was signed? The ONE reader. A schedule card (Schedule run, Cancel / modify schedule) never starts a run: the main
|
|
5558
|
+
// button is Sign schedule while this is true and disabled while it is not. On a scheduled job any When, Inputs, Destination or cap edit counts
|
|
5559
|
+
// (a re-sign); on an unscheduled job only When does (nothing to re-sign until a schedule is picked).
|
|
5560
|
+
function whenChanged(){
|
|
5561
|
+
if(!sched || sched.refusal) return false;
|
|
5562
|
+
if(whenCanon(whenTriggerFromBoxes()) !== whenCanon(sched.cur)) return true;
|
|
5563
|
+
if(sched.scheduled !== true || !sched.base) return false;
|
|
5564
|
+
var b = sched.base;
|
|
5565
|
+
return byId("jf-inputs").value.trim() !== b.inputs || byId("jf-dest").value.trim() !== b.dest ||
|
|
5566
|
+
byId("jf-cap-money").value.trim() !== b.capUsd || byId("jf-cap-time").value.trim() !== b.capTime;
|
|
5567
|
+
}
|
|
5568
|
+
function whenShowSubs(){
|
|
5569
|
+
var kind = byId("jf-when").value;
|
|
5570
|
+
byId("jf-when-clock").hidden = !(kind === "daily" || kind === "weekdays" || kind === "weekly");
|
|
5571
|
+
byId("jf-when-day").hidden = kind !== "weekly";
|
|
5572
|
+
byId("jf-when-filebox").hidden = !(kind === "file-updated" || kind === "new-file");
|
|
5573
|
+
byId("jf-when-path").placeholder = kind === "new-file" ? "/full/path/to/folder" : "/full/path/to/file";
|
|
5574
|
+
}
|
|
5575
|
+
function paintWhen(){
|
|
5576
|
+
var field = byId("jf-when-field");
|
|
5577
|
+
field.hidden = !sched;
|
|
5578
|
+
if(!sched) return;
|
|
5579
|
+
var w = sched.words || {};
|
|
5580
|
+
var opts = w.options || {};
|
|
5581
|
+
byId("jf-when-label").textContent = w.whenLabel || "";
|
|
5582
|
+
var sel = byId("jf-when");
|
|
5583
|
+
sel.innerHTML = "";
|
|
5584
|
+
WHEN_KINDS.forEach(function(k){
|
|
5585
|
+
var op = document.createElement("option");
|
|
5586
|
+
op.value = k;
|
|
5587
|
+
// on a scheduled job the first option reads Cancel schedule instead of By hand
|
|
5588
|
+
op.textContent = k === "hand" ? (sched.scheduled ? w.cancelSchedule : w.byHand) : (opts[k] || k);
|
|
5589
|
+
sel.appendChild(op);
|
|
5590
|
+
});
|
|
5591
|
+
var t = sched.cur;
|
|
5592
|
+
sel.value = t ? t.kind : "hand";
|
|
5593
|
+
if(t && t.hour !== undefined){ byId("jf-when-time").value = ("0" + t.hour).slice(-2) + ":" + ("0" + t.minute).slice(-2); }
|
|
5594
|
+
if(t && t.day) byId("jf-when-day").value = t.day;
|
|
5595
|
+
byId("jf-when-path").value = t ? (t.path || t.folder || "") : "";
|
|
5596
|
+
// refused = the select and every sub-box are DISABLED and the reason stands where "Now:" would be
|
|
5597
|
+
var refused = !!sched.refusal;
|
|
5598
|
+
sel.disabled = byId("jf-when-day").disabled = byId("jf-when-time").disabled = byId("jf-when-path").disabled = refused;
|
|
5599
|
+
byId("jf-when-now").textContent = refused ? sched.refusal : (sched.now ? (w.nowPrefix || "") + sched.now : "");
|
|
5600
|
+
byId("jf-err-when").textContent = "";
|
|
5601
|
+
whenShowSubs();
|
|
5602
|
+
}
|
|
5603
|
+
function scheduleOff(){
|
|
5604
|
+
sched = null; whenHash = null; whenConfirm = null; whenBusy = false;
|
|
5605
|
+
paintWhen();
|
|
5606
|
+
}
|
|
5607
|
+
// editing any When box between the two clicks DROPS the hash: the label returns to Sign schedule
|
|
5608
|
+
function whenEdited(){
|
|
5609
|
+
whenHash = null; whenConfirm = null;
|
|
5610
|
+
byId("jf-err-when").textContent = "";
|
|
5611
|
+
whenShowSubs();
|
|
5612
|
+
renderMain();
|
|
5613
|
+
}
|
|
5614
|
+
["jf-when", "jf-when-day", "jf-when-time"].forEach(function(id){ byId(id).addEventListener("change", whenEdited); });
|
|
5615
|
+
byId("jf-when-path").addEventListener("input", whenEdited);
|
|
5616
|
+
// the hash binds the WHOLE card (inputs, destination, caps), so a change to any of them drops it too (and repaints the button: on a scheduled job a box edit is Sign schedule)
|
|
5617
|
+
["jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time"].forEach(function(id){ byId(id).addEventListener("input", function(){ if(!sched) return; if(whenHash !== null) whenEdited(); else renderMain(); }); });
|
|
5618
|
+
function doWhenSign(){
|
|
5619
|
+
if(!sched || whenBusy || whenLocked) return;
|
|
5620
|
+
var timeCapMin = byId("jf-cap-time").value.trim();
|
|
5621
|
+
var body = {
|
|
5622
|
+
runid: sched.runid, when: whenTriggerFromBoxes(), inputs: byId("jf-inputs").value.trim(), destination: byId("jf-dest").value.trim(),
|
|
5623
|
+
capUsd: Number(byId("jf-cap-money").value), maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
5624
|
+
};
|
|
5625
|
+
var second = whenHash !== null;
|
|
5626
|
+
if(second) body.hash = whenHash;
|
|
5627
|
+
whenBusy = true; renderMain();
|
|
5628
|
+
authorPost(second ? "/api/author/schedule" : "/api/author/schedule-prepare", body).then(function(r){
|
|
5629
|
+
whenBusy = false;
|
|
5630
|
+
if(!(r.status >= 200 && r.status < 300 && r.body && r.body.ok === true)){
|
|
5631
|
+
whenHash = null; whenConfirm = null;
|
|
5632
|
+
byId("jf-err-when").textContent = (r.body && (r.body.say || r.body.error)) || ("refused (" + r.status + ")");
|
|
5633
|
+
renderMain(); return;
|
|
5634
|
+
}
|
|
5635
|
+
var words = typeof r.body.words === "string" ? r.body.words : "";
|
|
5636
|
+
if(!second){
|
|
5637
|
+
whenHash = r.body.hash;
|
|
5638
|
+
whenConfirm = words ? words.charAt(0).toLowerCase() + words.slice(1) : "";
|
|
5639
|
+
byId("jf-err-when").textContent = "";
|
|
5640
|
+
renderMain(); return;
|
|
5641
|
+
}
|
|
5642
|
+
// after click 2 the card clears to the empty New job card and NOTHING runs; the button is dead for 1.5 s
|
|
5643
|
+
var say = typeof r.body.say === "string" ? r.body.say : "";
|
|
5644
|
+
whenHash = null; whenConfirm = null;
|
|
5645
|
+
whenLocked = true;
|
|
5646
|
+
setTimeout(function(){ whenLocked = false; renderMain(); }, 1500);
|
|
5647
|
+
resetCard();
|
|
5648
|
+
if(say) chatActionFailed(say);
|
|
5649
|
+
refreshRunsList(true);
|
|
5650
|
+
if(currentRunid) selectRun(currentRunid);
|
|
5651
|
+
}).catch(function(){
|
|
5652
|
+
whenBusy = false; whenHash = null; whenConfirm = null;
|
|
5653
|
+
byId("jf-err-when").textContent = "network error — could not reach the panel server, nothing was signed";
|
|
5654
|
+
renderMain();
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
// the card opened by Schedule run / Cancel-modify schedule: the SAME Reuse fill (applyReuse), then the When field
|
|
5658
|
+
function applyScheduleCard(d){
|
|
5659
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
5660
|
+
if(sessionLive || resumeRun !== null){ errEl.textContent = "Finish or clear what is on the card first, then try again."; return; }
|
|
5661
|
+
applyReuse({runid: d.runid, prefill: d.prefill, lock: true});
|
|
5662
|
+
var sc = (d.prefill || {}).scheduleCard || {};
|
|
5663
|
+
sched = {job: d.job, runid: d.runid, words: sc.words || {}, cur: sc.trigger || null, scheduled: sc.scheduled === true, refusal: sc.refusal || null, now: sc.now || null,
|
|
5664
|
+
base: {inputs: byId("jf-inputs").value.trim(), dest: byId("jf-dest").value.trim(), capUsd: byId("jf-cap-money").value.trim(), capTime: byId("jf-cap-time").value.trim()}};
|
|
5665
|
+
whenHash = null; whenConfirm = null; whenBusy = false;
|
|
5666
|
+
paintWhen();
|
|
5667
|
+
refreshStartEnabled();
|
|
5668
|
+
}
|
|
5669
|
+
document.addEventListener("bareloop-schedule", function(e){ applyScheduleCard(e.detail || {}); });
|
|
5670
|
+
|
|
4943
5671
|
mainBtn.addEventListener("click", function(){
|
|
4944
5672
|
if(mainBtn.disabled) return;
|
|
4945
5673
|
if(mainAction === "start") doStart();
|
|
4946
5674
|
else if(mainAction === "check-deps") doCheckDeps();
|
|
4947
5675
|
else if(mainAction === "send") doSend();
|
|
4948
5676
|
else if(mainAction === "sign-prepare" || mainAction === "sign") doSign();
|
|
5677
|
+
else if(mainAction === "when-sign-prepare" || mainAction === "when-sign") doWhenSign();
|
|
4949
5678
|
});
|
|
4950
5679
|
|
|
4951
5680
|
refreshStartEnabled();
|