bareloop 0.41.0 → 0.43.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 +109 -1
- package/README.md +13 -21
- package/bareloop.context.md +97 -34
- package/package.json +4 -3
- package/src/authorflow.js +337 -29
- package/src/authoring.js +92 -7
- package/src/authorjob.js +84 -16
- package/src/authorreadout.js +89 -5
- package/src/authorrun.js +14 -5
- package/src/authorscout.js +108 -0
- package/src/declaredclose.js +25 -14
- package/src/facts.js +317 -0
- package/src/factwords.js +129 -0
- package/src/inputlook.js +301 -0
- package/src/interviewrun.js +0 -1
- package/src/job.js +40 -12
- package/src/jobblock.js +7 -2
- package/src/kinds.js +84 -3
- package/src/labelscan.js +170 -0
- package/src/pagefetch.js +125 -0
- package/src/panel/authorroutes.js +121 -34
- package/src/panel/authorsession.js +154 -45
- package/src/panel/index.html +845 -490
- package/src/panel/jobplan.js +34 -11
- package/src/panel/runroutes.js +163 -5
- 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 +467 -48
- package/src/planrun.js +42 -9
- package/src/providerrows.js +3 -3
- package/src/reviewdoor.js +37 -0
- package/src/rubricstep.js +804 -0
- package/src/runlist.js +35 -0
- package/src/schedulewords.js +296 -0
- package/src/source.js +166 -1
- 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/u-readout.js +1 -1
- package/src/userrun.js +20 -9
- package/types/authorflow.d.ts +267 -13
- package/types/authoring.d.ts +29 -1
- package/types/authorjob.d.ts +7 -5
- package/types/authorreadout.d.ts +27 -2
- package/types/authorscout.d.ts +70 -0
- package/types/declaredclose.d.ts +5 -3
- package/types/facts.d.ts +71 -0
- package/types/factwords.d.ts +42 -0
- package/types/inputlook.d.ts +238 -0
- package/types/job.d.ts +8 -1
- package/types/jobblock.d.ts +8 -1
- package/types/labelscan.d.ts +38 -0
- package/types/pagefetch.d.ts +71 -0
- package/types/panel/authorroutes.d.ts +34 -1
- package/types/panel/authorsession.d.ts +55 -14
- package/types/panel/jobplan.d.ts +12 -4
- 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 +145 -14
- package/types/planrun.d.ts +2 -0
- package/types/providerrows.d.ts +2 -2
- package/types/reviewdoor.d.ts +27 -0
- package/types/rubricstep.d.ts +286 -0
- package/types/runlist.d.ts +20 -0
- package/types/schedulewords.d.ts +135 -0
- package/types/source.d.ts +94 -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 +11 -0
package/src/panel/index.html
CHANGED
|
@@ -113,8 +113,9 @@
|
|
|
113
113
|
.topbar > div:first-child{display:flex;align-items:baseline;gap:0;white-space:nowrap;overflow:hidden;}
|
|
114
114
|
.topbar .brand{font-weight:700;font-size:17px;letter-spacing:0.06em;flex:none;}
|
|
115
115
|
.topbar .brand::before{content:"■ ";}
|
|
116
|
-
.topbar .tagline{font-size:12px;color:var(--text-dim);margin-left:8px;overflow:hidden;text-overflow:ellipsis;}
|
|
117
|
-
.topbar .tagline::before{content:"· ";color:var(--text-faint);}
|
|
116
|
+
.topbar .tagline,.topbar .version{font-size:12px;color:var(--text-dim);margin-left:8px;overflow:hidden;text-overflow:ellipsis;font-weight:400;}
|
|
117
|
+
.topbar .tagline::before,.topbar .version::before{content:"· ";color:var(--text-faint);}
|
|
118
|
+
.topbar .version{flex:none;}
|
|
118
119
|
@media (max-width: 600px){ .topbar .tagline{display:none;} }
|
|
119
120
|
.topbar-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
|
|
120
121
|
|
|
@@ -143,8 +144,8 @@
|
|
|
143
144
|
.dot::before{content:"[·]";color:var(--grey-dot);}
|
|
144
145
|
.dot.green::before{content:"[✓]";color:var(--green);}
|
|
145
146
|
.dot.red::before{content:"[✗]";color:var(--red);}
|
|
146
|
-
/* stopped (hamr 2026-10-07): the person pressed Stop — its own sign,
|
|
147
|
-
.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);}
|
|
148
149
|
/* Panel build P3 addition (2026-09-27, "yes, go"): the part cards below the
|
|
149
150
|
step map (`partResultGlyph`/`attemptGlyph`) and the chat job card render
|
|
150
151
|
their result glyph as PLAIN inline text inside a larger string (never
|
|
@@ -169,9 +170,11 @@
|
|
|
169
170
|
1.2s, matching design/panel-mockup.html's pulsing amber dot exactly —
|
|
170
171
|
one fixed rhythm, never re-derived per call site. */
|
|
171
172
|
@keyframes pulse-glyph{0%,100%{opacity:1;}50%{opacity:0.3;}}
|
|
172
|
-
.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;}
|
|
173
176
|
@media (prefers-reduced-motion: reduce){
|
|
174
|
-
.dot.
|
|
177
|
+
.dot.pulse::before{animation:none;}
|
|
175
178
|
}
|
|
176
179
|
.dot.grey::before{content:"[·]";color:var(--grey-dot);}
|
|
177
180
|
/* died (hamr's ruling B, 2026-09-25): no ending was ever recorded — a
|
|
@@ -207,12 +210,12 @@
|
|
|
207
210
|
.btn:disabled::before,.btn:disabled::after{color:var(--text-faint);opacity:0.4;}
|
|
208
211
|
.btn.small{padding:0 9px;height:32px;min-height:0;font-size:12px;}
|
|
209
212
|
|
|
210
|
-
.wf-row
|
|
213
|
+
.wf-row{
|
|
211
214
|
display:flex;flex-direction:column;row-gap:4px;min-width:0;
|
|
212
215
|
padding:8px 8px 8px 18px;position:relative;border:1px solid var(--border);margin-bottom:6px;background:var(--panel2);cursor:pointer;
|
|
213
216
|
}
|
|
214
217
|
.wf-row::before{content:" ";position:absolute;left:6px;top:8px;color:var(--accent);font-weight:700;}
|
|
215
|
-
.wf-row:hover
|
|
218
|
+
.wf-row:hover{border-color:var(--border-strong);}
|
|
216
219
|
.wf-row.selected::before{content:">";}
|
|
217
220
|
/* line 1: glyph + name, kept on ONE line — truncated with an ellipsis
|
|
218
221
|
(never wrapped) when the job/runid text is longer than the row; the
|
|
@@ -225,15 +228,42 @@
|
|
|
225
228
|
.wf-meta{font-size:12px;color:var(--text-dim);}
|
|
226
229
|
.wf-ended{display:block;font-size:12px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
227
230
|
.wf-meta + .wf-meta::before{content:" · ";color:var(--text-faint);}
|
|
228
|
-
.wf-row.selected
|
|
229
|
-
/* item 2 (2026-09-26 merge): a Workflows-view job row expands inline to
|
|
230
|
-
list ALL its runs (build spec's own design) — the caret glyph on the
|
|
231
|
-
job row, and the indent wrapper the expanded runs render into. */
|
|
232
|
-
.wf-job{margin-bottom:6px;}
|
|
233
|
-
.wf-job .wf-row{margin-bottom:0;}
|
|
231
|
+
.wf-row.selected{border:2px solid var(--accent);border-radius:4px;}
|
|
234
232
|
.wf-caret{display:inline-block;width:11px;flex:none;color:var(--text-faint);font-size:10px;}
|
|
235
|
-
|
|
236
|
-
.
|
|
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);}
|
|
237
267
|
|
|
238
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;}
|
|
239
269
|
.rp-header h2{margin:0;font-size:15px;}
|
|
@@ -256,6 +286,7 @@
|
|
|
256
286
|
.ended-block.magenta{border-color:var(--magenta);}
|
|
257
287
|
.ended-block.red{border-color:var(--red);}
|
|
258
288
|
.ended-block.green{border-color:var(--green);}
|
|
289
|
+
.ended-block.grey{border-color:var(--grey-dot);}
|
|
259
290
|
.ended-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;margin-bottom:6px;}
|
|
260
291
|
.ended-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:52px;}
|
|
261
292
|
.ended-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
@@ -292,15 +323,19 @@
|
|
|
292
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);}
|
|
293
324
|
.rw-picked span{overflow-wrap:anywhere;}
|
|
294
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;}
|
|
295
330
|
.run-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
|
|
296
331
|
.run-actions[hidden]{display:none;}
|
|
297
332
|
.run-actions .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
298
333
|
.resume-err{color:var(--red);white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
299
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 */
|
|
300
|
-
.wf-resumed{color:var(--amber);}
|
|
301
335
|
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
302
336
|
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
303
337
|
.summary-box.red{border-color:var(--red);}
|
|
338
|
+
.summary-box.grey{border-color:var(--grey-dot);}
|
|
304
339
|
.summary-box.amber{border-color:var(--amber);}
|
|
305
340
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
306
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;}
|
|
@@ -330,6 +365,7 @@
|
|
|
330
365
|
.map-chip.s-done{border-color:var(--green);color:var(--text);} .map-chip.s-done .chip-sign{color:#2e7d32;}
|
|
331
366
|
.map-chip.s-running{border-color:var(--amber);color:var(--text);} .map-chip.s-running .chip-sign{color:#b8860b;}
|
|
332
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 */
|
|
333
369
|
.map-chip.s-died{border-color:var(--magenta);color:var(--text);} .map-chip.s-died .chip-sign{color:var(--magenta);}
|
|
334
370
|
.map-chip.retry{border-style:dashed;}
|
|
335
371
|
.map-chip.resumed-next{border-right-style:dotted;}
|
|
@@ -400,6 +436,13 @@
|
|
|
400
436
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
401
437
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
402
438
|
.field{margin-bottom:10px;}
|
|
439
|
+
/* T Part 7: the When field (sub-boxes show and hide; 44px controls) */
|
|
440
|
+
.when-sub{display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;}
|
|
441
|
+
.when-sub[hidden]{display:none;}
|
|
442
|
+
.when-sub select,.when-sub input{flex:1 1 120px;min-width:0;width:auto;min-height:44px;}
|
|
443
|
+
#jf-when{min-height:44px;}
|
|
444
|
+
.jf-err{color:var(--red);font-size:12px;overflow-wrap:anywhere;}
|
|
445
|
+
.jf-err:empty{display:none;}
|
|
403
446
|
.field input,.field select{width:100%;}
|
|
404
447
|
label{display:block;font-size:11px;font-weight:700;color:var(--text-dim);margin-bottom:2px;text-transform:uppercase;letter-spacing:0.04em;}
|
|
405
448
|
.ro-value{padding:6px 8px;min-height:32px;box-sizing:border-box;border:1px solid var(--border);background:var(--bg);color:var(--text-dim);}
|
|
@@ -590,6 +633,9 @@
|
|
|
590
633
|
.pv-typed input{flex:1 1 160px;min-width:0;max-width:260px;min-height:28px;box-sizing:border-box;}
|
|
591
634
|
.pv-typed .hint{flex:1 1 100%;margin:0;}
|
|
592
635
|
.hint.bad{color:var(--red);}
|
|
636
|
+
/* a refused Start (a $0 refusal, e.g. an Inputs line) is read like the red failure line of the progress list, not like a faint hint */
|
|
637
|
+
.card-refusal:not(:empty){font-size:13px;margin-top:8px;line-height:1.4;}
|
|
638
|
+
.card-refusal:empty{display:none;}
|
|
593
639
|
.field label.jf-thinking{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:6px;margin-top:6px;font-size:12px;text-transform:none;letter-spacing:0;}
|
|
594
640
|
.field label.jf-thinking input{width:auto;min-height:0;margin:0;}
|
|
595
641
|
.table-wrap{overflow-x:auto;border:1px solid var(--border-strong);background:var(--panel2);max-width:100%;}
|
|
@@ -638,6 +684,7 @@
|
|
|
638
684
|
<header class="topbar">
|
|
639
685
|
<div>
|
|
640
686
|
<span class="brand">bareloop</span>
|
|
687
|
+
<span class="version" id="panel-version">v__BARELOOP_PANEL_VERSION__</span>
|
|
641
688
|
<span class="tagline">automate a job, verified</span>
|
|
642
689
|
</div>
|
|
643
690
|
<div class="topbar-right">
|
|
@@ -665,6 +712,7 @@
|
|
|
665
712
|
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
666
713
|
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
667
714
|
<div class="origin-line" id="chat-origin-line" data-testid="origin-line" hidden></div>
|
|
715
|
+
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
668
716
|
|
|
669
717
|
<div class="field">
|
|
670
718
|
<label id="jf-verdict-label">Check type</label>
|
|
@@ -679,7 +727,6 @@
|
|
|
679
727
|
<ul class="rw-list" id="rw-list" data-testid="rw-list"></ul>
|
|
680
728
|
</div>
|
|
681
729
|
<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>
|
|
682
|
-
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
683
730
|
</div>
|
|
684
731
|
</div>
|
|
685
732
|
<div class="field">
|
|
@@ -715,6 +762,24 @@
|
|
|
715
762
|
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
716
763
|
</div>
|
|
717
764
|
|
|
765
|
+
<!-- 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. -->
|
|
766
|
+
<div class="field" id="jf-when-field" data-testid="jf-when-field" hidden>
|
|
767
|
+
<label for="jf-when" id="jf-when-label">When</label>
|
|
768
|
+
<select id="jf-when" data-testid="jf-when" aria-label="When this job runs"></select>
|
|
769
|
+
<div class="when-sub" id="jf-when-clock" hidden>
|
|
770
|
+
<select id="jf-when-day" data-testid="jf-when-day" aria-label="Day of the week">
|
|
771
|
+
<option value="mon">Monday</option><option value="tue">Tuesday</option><option value="wed">Wednesday</option><option value="thu">Thursday</option>
|
|
772
|
+
<option value="fri">Friday</option><option value="sat">Saturday</option><option value="sun">Sunday</option>
|
|
773
|
+
</select>
|
|
774
|
+
<input id="jf-when-time" type="time" value="09:00" data-testid="jf-when-time" aria-label="Time, HH:MM">
|
|
775
|
+
</div>
|
|
776
|
+
<div class="when-sub" id="jf-when-filebox" hidden>
|
|
777
|
+
<input id="jf-when-path" type="text" autocomplete="off" spellcheck="false" data-testid="jf-when-path" aria-label="File or folder">
|
|
778
|
+
</div>
|
|
779
|
+
<div class="hint" id="jf-when-now" data-testid="jf-when-now"></div>
|
|
780
|
+
<div class="jf-err" id="jf-err-when" data-testid="jf-err-when"></div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
718
783
|
<div class="cap-row" data-testid="cap-row">
|
|
719
784
|
<div class="cap-field">
|
|
720
785
|
<label for="jf-cap-money">Cap $</label>
|
|
@@ -726,7 +791,7 @@
|
|
|
726
791
|
<input id="jf-cap-time" type="text" placeholder="or blank = no wall">
|
|
727
792
|
</div>
|
|
728
793
|
</div>
|
|
729
|
-
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
794
|
+
<div class="hint bad card-refusal" id="chat-card-error" data-testid="chat-card-error" role="alert"></div>
|
|
730
795
|
<div id="chat-resume-box" class="chat-resume-box" data-testid="resume-box" hidden>
|
|
731
796
|
<div class="hint" id="chat-resume-spent" data-testid="resume-spent"></div>
|
|
732
797
|
<div class="ended-actions"><button class="btn primary" type="button" id="chat-resume-sign" data-testid="btn-sign-resume">Sign & resume</button> <button class="btn" type="button" id="chat-resume-cancel" data-testid="btn-resume-cancel">Cancel</button></div>
|
|
@@ -760,9 +825,7 @@
|
|
|
760
825
|
</section>
|
|
761
826
|
|
|
762
827
|
<section id="panel-runs" class="tabpanel" role="tabpanel" aria-labelledby="tab-runs" hidden>
|
|
763
|
-
<div
|
|
764
|
-
<button class="chip" type="button" aria-pressed="true" data-runs-view="workflows" data-testid="runs-view-workflows">Workflows</button>
|
|
765
|
-
<button class="chip" type="button" aria-pressed="false" data-runs-view="history" data-testid="runs-view-history">History</button>
|
|
828
|
+
<div style="margin-bottom:8px;">
|
|
766
829
|
<button class="btn small" type="button" id="wf-import" data-testid="wf-import" title="look at an exported job folder (read only)">Import</button>
|
|
767
830
|
</div>
|
|
768
831
|
<div class="import-box" id="import-box" data-testid="import-box" hidden>
|
|
@@ -779,10 +842,12 @@
|
|
|
779
842
|
<button class="btn" type="button" id="import-cancel" data-testid="import-cancel">Cancel</button>
|
|
780
843
|
</div>
|
|
781
844
|
</div>
|
|
845
|
+
<div id="rl-top" data-testid="rl-top"></div>
|
|
846
|
+
<div class="rl-dash"></div>
|
|
847
|
+
<div class="rl-faint" data-testid="rl-filter-note">filters below act on Jobs and quiet jobs only</div>
|
|
782
848
|
<div id="runs-filterbar-anchor"></div>
|
|
783
849
|
<div id="wf-list" data-testid="wf-list"></div>
|
|
784
850
|
<div id="import-list" data-testid="import-list"></div>
|
|
785
|
-
<div id="history-list" data-testid="history-list" hidden></div>
|
|
786
851
|
</section>
|
|
787
852
|
|
|
788
853
|
</div>
|
|
@@ -808,8 +873,12 @@
|
|
|
808
873
|
|
|
809
874
|
<section id="panel-run" class="tabpanel active" role="tabpanel" aria-labelledby="tab-run">
|
|
810
875
|
<div class="import-view" id="import-view" data-testid="import-view" hidden></div>
|
|
811
|
-
<div id="run-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run from
|
|
876
|
+
<div id="run-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run from the Runs list</div>
|
|
812
877
|
<div id="run-content" hidden>
|
|
878
|
+
<!-- RULED 2026-10-15: ONE summary block: ENDED, NEXT, SCHEDULE (fwd #summary-blocks, aligned with M9 am7) -->
|
|
879
|
+
<div id="summary-blocks" data-testid="summary-blocks">
|
|
880
|
+
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
881
|
+
</div>
|
|
813
882
|
<div class="run-actions" id="run-actions" data-testid="run-actions" hidden></div>
|
|
814
883
|
<div class="counters" id="run-counters" data-testid="live-counters"></div>
|
|
815
884
|
<div class="summary-box" id="run-summary" data-testid="run-summary"></div>
|
|
@@ -821,7 +890,6 @@
|
|
|
821
890
|
<section id="panel-audit" class="tabpanel" role="tabpanel" aria-labelledby="tab-audit" hidden>
|
|
822
891
|
<div id="audit-select-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
823
892
|
<div id="audit-body" hidden>
|
|
824
|
-
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
825
893
|
<div class="audit-toolbar" style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
|
826
894
|
<div class="chips" role="group" aria-label="Filter audit rows" style="margin-bottom:0;">
|
|
827
895
|
<button class="chip" aria-pressed="true" data-filter="all" data-testid="chip-all">All</button>
|
|
@@ -858,6 +926,8 @@
|
|
|
858
926
|
<h4 id="details-title">Job card — signed</h4>
|
|
859
927
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
860
928
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
929
|
+
<div class="field" id="details-started-field" hidden><label>Started</label><div class="ro-value" id="details-started" data-testid="details-started" style="overflow-wrap:anywhere;">—</div></div>
|
|
930
|
+
<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>
|
|
861
931
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
862
932
|
<div class="field"><label>The job</label><div class="ro-value" id="details-plan" data-testid="details-plan">—</div></div>
|
|
863
933
|
<div class="field"><label>Inputs</label><div class="ro-value" id="details-inputs" data-testid="details-inputs">—</div></div>
|
|
@@ -1162,7 +1232,7 @@
|
|
|
1162
1232
|
var sign = s.status && s.status.sign ? s.status.sign : "";
|
|
1163
1233
|
var tries = boxRetryTry(s);
|
|
1164
1234
|
var cls = "map-chip s-" + String(s.state).replace(/[^a-z-]/g, "") +
|
|
1165
|
-
(tries ? " retry" : "") + (s.resumedNext ? " resumed-next" : "");
|
|
1235
|
+
(tries ? " retry" : "") + (sign === "\u25A0" ? " s-stopsign" : "") + (s.resumedNext ? " resumed-next" : "");
|
|
1166
1236
|
var label = escapeXml(stepTitleText(numIdx[idx], s));
|
|
1167
1237
|
var blocked = s.blocked > 0 ? ' <span class="chip-blocked">' + s.blocked + ' blocked</span>' : '';
|
|
1168
1238
|
var retry = tries ? '<span class="chip-retry">\u21bb' + tries + '</span> ' : '';
|
|
@@ -1254,6 +1324,10 @@
|
|
|
1254
1324
|
function realSteps(steps){ return steps.filter(function(s){ return !s.synthetic; }); }
|
|
1255
1325
|
|
|
1256
1326
|
// ---- glyph/badge helpers (only [✓]/[✗]/[▶]/[·]/[?] — never the words green/red/soft-green/died) ----
|
|
1327
|
+
// RULED 2026-10-15 (matches fwdloop glyphPulses): ONE rule for which sign blinks. Running blinks; a run waiting for the person's Accept blinks;
|
|
1328
|
+
// everything else rests (a schedule that is on and waiting included). `needs` = the run's Needs-you tag.
|
|
1329
|
+
function dotPulses(g, needs){ return g === "▶" || needs === "Accept"; }
|
|
1330
|
+
function dotClass(g, needs){ return "dot " + glyphClass(g) + (dotPulses(g, needs) ? " pulse" : ""); }
|
|
1257
1331
|
function glyphClass(g){
|
|
1258
1332
|
if(g === "✓") return "green";
|
|
1259
1333
|
if(g === "✗") return "red";
|
|
@@ -1280,9 +1354,9 @@
|
|
|
1280
1354
|
// A run with no number (an imported bundle's run is not in the run list) falls back to its id.
|
|
1281
1355
|
function runLabel(runNo, runid){ return typeof runNo === "number" ? "run-" + runNo : String(runid || ""); }
|
|
1282
1356
|
function runName(job, runNo, runid){ return job + " (" + runLabel(runNo, runid) + ")"; }
|
|
1283
|
-
function setRunHeader(sign, nameText, bodyHtml){
|
|
1357
|
+
function setRunHeader(sign, nameText, bodyHtml, needs){
|
|
1284
1358
|
document.getElementById("active-wf-name").textContent = nameText;
|
|
1285
|
-
document.getElementById("active-wf-dot").className =
|
|
1359
|
+
document.getElementById("active-wf-dot").className = dotClass(sign, needs);
|
|
1286
1360
|
var st = document.getElementById("active-wf-verdict");
|
|
1287
1361
|
st.innerHTML = bodyHtml ? '<span class="rp-sep">\u2502</span>' + bodyHtml : "";
|
|
1288
1362
|
st.removeAttribute("title");
|
|
@@ -1301,131 +1375,89 @@
|
|
|
1301
1375
|
return statusWordHtml(detail.status, reason) + " \u00B7 " + escapeXml(when);
|
|
1302
1376
|
}
|
|
1303
1377
|
|
|
1304
|
-
//
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
//
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
var
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
'<span class="wf-meta">' + escapeXml(rowSpendText(r)) + '</span>' +
|
|
1367
|
-
'<span class="wf-meta">' + escapeXml(rowWallText(r)) + '</span>' +
|
|
1368
|
-
'<span class="wf-meta">' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1369
|
-
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) +
|
|
1370
|
-
'</span>';
|
|
1371
|
-
}
|
|
1372
|
-
row.addEventListener("click", function(){ selectRun(r.runid, row, ".hist-row"); document.getElementById("tab-run").click(); scrollRunIntoViewMobile(); });
|
|
1373
|
-
row.addEventListener("keydown", function(e){ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); row.click(); } });
|
|
1374
|
-
return row;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// ---- History view ----
|
|
1378
|
-
function renderHistory(runs){
|
|
1379
|
-
var el = document.getElementById("history-list");
|
|
1380
|
-
el.innerHTML = "";
|
|
1381
|
-
var listed = (runs || []).filter(function(r){ return !r.fileMissing; });
|
|
1382
|
-
if(!listed.length){
|
|
1383
|
-
el.innerHTML = '<div class="hint">no runs listed yet</div>';
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
(runs || []).forEach(function(r){ el.appendChild(buildRunRowEl(r)); });
|
|
1378
|
+
// ---- 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
|
|
1379
|
+
// 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
|
|
1380
|
+
// 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
|
|
1381
|
+
// in, cards out); the rl*Html functions only draw it; `paintRunsList` is the ONE painter.
|
|
1382
|
+
var rlData = { runs: [], jobs: [], scheduled: [], limits: { quietDays: 0, olderRuns: 0, needsYouDays: 0 } };
|
|
1383
|
+
// manual fold memory, keyed by job ("__quiet" is the quiet fold): a per-viewer convenience, reset on reload. A manual click wins over an automatic open.
|
|
1384
|
+
var rlOpen = {};
|
|
1385
|
+
// the last markup written into each box: an identical repaint (every poll) writes nothing, so focus, scroll and a half-read card survive it
|
|
1386
|
+
var rlLast = { top: null, jobs: null };
|
|
1387
|
+
// The ONE writer of `rlData`: the whole /api/runs payload, as the server sent it.
|
|
1388
|
+
function setRunsData(r){
|
|
1389
|
+
rlData = { runs: r.runs || [], jobs: r.jobs || [], scheduled: r.scheduled || [], limits: r.limits || rlData.limits };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// The ONE date of every card in the list: local `MM/DD HH:MM`. An unparseable stamp is shown raw, never blank.
|
|
1393
|
+
function rlShortDate(iso){
|
|
1394
|
+
var d = new Date(iso);
|
|
1395
|
+
if(isNaN(d.getTime())) return String(iso);
|
|
1396
|
+
var t = function(n){ return ("0" + n).slice(-2); };
|
|
1397
|
+
return t(d.getMonth() + 1) + "/" + t(d.getDate()) + " " + t(d.getHours()) + ":" + t(d.getMinutes());
|
|
1398
|
+
}
|
|
1399
|
+
function runAtText(r){
|
|
1400
|
+
return r.at ? rlShortDate(r.at) : "unknown";
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// 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.
|
|
1404
|
+
function rlRunParts(r){
|
|
1405
|
+
var rn = runLabel(r.runNo, r.runid);
|
|
1406
|
+
var p = { rn: rn, name: r.job + " (" + rn + ")", date: runAtText(r), model: r.model || "", word: "", why: "", spend: "", wall: "" };
|
|
1407
|
+
if(r.starting){ p.word = "running"; p.why = "starting…"; return p; }
|
|
1408
|
+
if(r.fileMissing){ p.word = ""; p.why = "file missing"; return p; }
|
|
1409
|
+
p.word = r.status && r.status.word ? r.status.word : "";
|
|
1410
|
+
p.why = r.endedLine || (p.word === "passed" ? "goal met" : "");
|
|
1411
|
+
p.spend = rowSpendText(r, true);
|
|
1412
|
+
p.wall = rowWallText(r) || "";
|
|
1413
|
+
return p;
|
|
1414
|
+
}
|
|
1415
|
+
// 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):
|
|
1416
|
+
// 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
|
|
1417
|
+
// 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
|
|
1418
|
+
// 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).
|
|
1419
|
+
function runMatchesSearch(r, query){
|
|
1420
|
+
var words = String(query || "").toLowerCase().replace(/[()]/g, "").split(/\s+/).filter(function(w){ return w; });
|
|
1421
|
+
if(!words.length) return true;
|
|
1422
|
+
var p = rlRunParts(r);
|
|
1423
|
+
var d = r.at ? new Date(r.at) : null;
|
|
1424
|
+
var dated = !!d && !isNaN(d.getTime());
|
|
1425
|
+
var rn = String(p.rn).toLowerCase();
|
|
1426
|
+
var rest = [r.job, p.rn, p.name, r.runid, p.model, p.word, p.why, p.spend, p.wall, r.checkType || ""].join(" ").toLowerCase();
|
|
1427
|
+
var whole = rest + " " + String(p.date).toLowerCase();
|
|
1428
|
+
return words.every(function(w){
|
|
1429
|
+
if(/^run-\d+$/.test(w)) return rn === w;
|
|
1430
|
+
var day = /^(?:(\d{4})-)?(\d\d)[-\/](\d\d)$/.exec(w);
|
|
1431
|
+
if(day && (day[1] ? w.charAt(4) === "-" && w.charAt(7) === "-" : w.charAt(2) === "/" || w.charAt(2) === "-")){
|
|
1432
|
+
var mo = Number(day[2]), da = Number(day[3]);
|
|
1433
|
+
if(mo >= 1 && mo <= 12 && da >= 1 && da <= 31){
|
|
1434
|
+
return dated && d.getMonth() + 1 === mo && d.getDate() === da && (!day[1] || d.getFullYear() === Number(day[1]));
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if(/^[\d\/-]+$/.test(w) && /[\/-]/.test(w)) return rest.indexOf(w) !== -1;
|
|
1438
|
+
return whole.indexOf(w) !== -1;
|
|
1439
|
+
});
|
|
1387
1440
|
}
|
|
1388
1441
|
|
|
1389
|
-
//
|
|
1390
|
-
|
|
1391
|
-
// (the old /api/workflows endpoint is gone; see src/panel/server.js). Runs
|
|
1392
|
-
// arrive newest-first (server contract, see listRuns), so the FIRST run
|
|
1393
|
-
// seen for a job is its latest — no re-sort needed, and job order (first-
|
|
1394
|
-
// job-seen order) is already newest-first by construction.
|
|
1395
|
-
function groupRunsByJob(runs){
|
|
1442
|
+
// 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".
|
|
1443
|
+
function groupRunsByJob(runs, jobs){
|
|
1396
1444
|
var byJob = {};
|
|
1397
1445
|
var order = [];
|
|
1446
|
+
var headOf = {};
|
|
1447
|
+
(jobs || []).forEach(function(j){ headOf[j.job] = j.headRunid; if(!byJob[j.job]){ byJob[j.job] = []; order.push(j.job); } });
|
|
1398
1448
|
(runs || []).forEach(function(r){
|
|
1399
1449
|
if(!byJob[r.job]){ byJob[r.job] = []; order.push(r.job); }
|
|
1400
1450
|
byJob[r.job].push(r);
|
|
1401
1451
|
});
|
|
1402
1452
|
return order.map(function(job){
|
|
1403
|
-
var
|
|
1404
|
-
var
|
|
1405
|
-
return {
|
|
1406
|
-
job: job,
|
|
1407
|
-
runs: jobRuns,
|
|
1408
|
-
runCount: jobRuns.length,
|
|
1409
|
-
lastRunid: last.runid,
|
|
1410
|
-
lastRunNo: last.runNo,
|
|
1411
|
-
lastAt: last.at,
|
|
1412
|
-
lastGlyph: last.glyph || "▶",
|
|
1413
|
-
lastStatus: last.status || null,
|
|
1414
|
-
lastEndedLine: last.endedLine || null,
|
|
1415
|
-
lastResumedCount: last.resumedCount || 0,
|
|
1416
|
-
lastCheckType: last.checkType || "unknown",
|
|
1417
|
-
lastModel: last.model || null,
|
|
1418
|
-
lastSpend: rowSpendText(last, true),
|
|
1419
|
-
lastWall: rowWallText(last) || "unknown",
|
|
1420
|
-
lastDate: last.date,
|
|
1421
|
-
};
|
|
1453
|
+
var all = byJob[job];
|
|
1454
|
+
var head = all.filter(function(r){ return r.runid === headOf[job]; })[0] || all[0] || null;
|
|
1455
|
+
return { job: job, runs: head ? [head].concat(all.filter(function(r){ return r !== head; })) : [], runCount: all.length, headRunid: head ? head.runid : null };
|
|
1422
1456
|
});
|
|
1423
1457
|
}
|
|
1424
1458
|
|
|
1425
|
-
// Matches a job if ANY of its runs match the filters (not only the
|
|
1426
|
-
//
|
|
1427
|
-
// of the AND/OR/time/search semantics), same posture as the old
|
|
1428
|
-
// filterWorkflows but now correct against the job's FULL run list.
|
|
1459
|
+
// 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
|
|
1460
|
+
// AND/OR/time/search semantics.
|
|
1429
1461
|
function filterWorkflows(jobs, filters, nowMs){
|
|
1430
1462
|
return (jobs || []).filter(function(g){
|
|
1431
1463
|
return filterRuns(g.runs, filters, nowMs).length > 0;
|
|
@@ -1441,166 +1473,230 @@
|
|
|
1441
1473
|
));
|
|
1442
1474
|
}
|
|
1443
1475
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
-
|
|
1476
|
+
function rlModel(data, groups, filters, limits, sel, open){
|
|
1477
|
+
var active = filtersActive(filters);
|
|
1478
|
+
var place = {};
|
|
1479
|
+
(data.jobs || []).forEach(function(j, i){ place[j.job] = { quiet: j.quiet === true, at: i }; });
|
|
1480
|
+
var cards = groups.slice().sort(function(a, b){
|
|
1481
|
+
return (place[a.job] ? place[a.job].at : Infinity) - (place[b.job] ? place[b.job].at : Infinity);
|
|
1482
|
+
}).map(function(g){
|
|
1483
|
+
var quiet = !!(place[g.job] && place[g.job].quiet);
|
|
1484
|
+
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 } };
|
|
1485
|
+
var head = g.runs[0];
|
|
1486
|
+
var matching = active ? filterRuns(g.runs, filters, Date.now()) : [];
|
|
1487
|
+
var older = g.runs.slice(1);
|
|
1488
|
+
var isSel = function(r){ return r.runid === sel.runid; };
|
|
1489
|
+
// 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.
|
|
1490
|
+
var pool = active ? older.filter(function(r){ return matching.indexOf(r) !== -1; }) : older;
|
|
1491
|
+
var top = pool.slice(0, limits.olderRuns);
|
|
1492
|
+
var shown = top.concat(older.filter(function(r){ return isSel(r) && top.indexOf(r) === -1; }));
|
|
1493
|
+
var left = pool.filter(function(r){ return shown.indexOf(r) === -1; });
|
|
1494
|
+
// 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
|
|
1495
|
+
var auto = (active && older.some(function(r){ return matching.indexOf(r) !== -1; })) || older.some(isSel);
|
|
1496
|
+
return {
|
|
1497
|
+
job: g.job, group: g, head: head, empty: false, quiet: quiet, count: g.runCount, sel: sel,
|
|
1498
|
+
selected: g.runs.some(isSel),
|
|
1499
|
+
hits: matching.map(function(r){ return r.runid; }),
|
|
1500
|
+
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 },
|
|
1501
|
+
};
|
|
1502
|
+
});
|
|
1503
|
+
var quietCards = cards.filter(function(c){ return c.quiet; });
|
|
1504
|
+
return {
|
|
1505
|
+
running: data.runs.filter(function(r){ return r.live === true; }),
|
|
1506
|
+
needs: data.runs.filter(function(r){ return typeof r.needs === "string" && r.needs !== ""; }),
|
|
1507
|
+
scheduled: data.scheduled || [],
|
|
1508
|
+
gaveUp: data.gaveUp || [],
|
|
1509
|
+
jobs: cards.filter(function(c){ return !c.quiet; }),
|
|
1510
|
+
quiet: quietCards,
|
|
1511
|
+
quietOpen: Object.prototype.hasOwnProperty.call(open, "__quiet") ? open.__quiet : quietCards.some(function(c){ return c.selected; }),
|
|
1512
|
+
limits: limits,
|
|
1513
|
+
noRows: data.runs.length === 0,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function rlSectHtml(label, n, note){
|
|
1517
|
+
return '<div class="rl-sect">' + label + ' (' + n + ')' + (note ? ' ' + note : '') + '</div>';
|
|
1518
|
+
}
|
|
1519
|
+
// 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.
|
|
1520
|
+
function rlJumpCardHtml(r, need){
|
|
1521
|
+
var rn = runLabel(r.runNo, r.runid);
|
|
1522
|
+
var faint;
|
|
1523
|
+
if(r.liveSince){
|
|
1524
|
+
faint = '<span data-rl-since="' + escapeXml(r.liveSince) + '" data-rl-prior="' + (typeof r.priorWallMs === "number" ? r.priorWallMs : "leg") + '"></span>';
|
|
1525
|
+
} else faint = 'time unknown';
|
|
1526
|
+
var l2;
|
|
1527
|
+
if(need){
|
|
1528
|
+
var p = rlRunParts(r);
|
|
1529
|
+
var why = r.needs === "Accept" ? "passed · waiting for your Accept" : [p.word, r.endedLine].filter(function(x){ return x; }).join(" · ");
|
|
1530
|
+
l2 = escapeXml(why) + ' · ' + escapeXml(r.stoppedAt ? rlShortDate(r.stoppedAt) : runAtText(r));
|
|
1531
|
+
} else l2 = escapeXml(r.liveWhy || "running");
|
|
1532
|
+
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) + '">' +
|
|
1533
|
+
'<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>' +
|
|
1534
|
+
(need ? '<span class="rl-pill rl-pill-need">' + escapeXml(r.needs) + '</span>' : ' <span class="rl-rest">· ' + faint + ' · ' + escapeXml(rowSpendText(r, true)) + '</span>') + '</span>' +
|
|
1535
|
+
'<span class="rl-l2">' + l2 + '</span></button>';
|
|
1536
|
+
}
|
|
1537
|
+
// 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
|
|
1538
|
+
function rlRunTagHtml(c){ return typeof c.runNo === "number" ? ' <span class="rl-rn">· ' + escapeXml(runLabel(c.runNo, c.runId)) + '</span>' : ''; }
|
|
1539
|
+
// 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.
|
|
1540
|
+
function rlGaveUpCardHtml(g){
|
|
1541
|
+
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>' +
|
|
1542
|
+
'<span class="rl-l2">' + escapeXml(g.words) + ' · ' + escapeXml(g.at) + '</span>';
|
|
1543
|
+
if(typeof g.runId !== "string" || g.runId === "") return '<div class="rl-card rl-need" data-testid="rl-gaveup-' + escapeXml(g.job) + '">' + body + '</div>';
|
|
1544
|
+
return '<button class="rl-card rl-need" type="button" data-rl-jump="' + escapeXml(g.runId) + '" data-testid="rl-gaveup-' + escapeXml(g.job) + '">' + body + '</button>';
|
|
1545
|
+
}
|
|
1546
|
+
// Scheduled card (T Part 7): `[·] job today 15:00` / `next in 3h · Every day at 15:00`, all from the server. A file trigger has no clock time:
|
|
1547
|
+
// `at` / `in` are null and nothing is made up. A click does what Running now does (rlJump to the job's HEAD run, `runId` from the server and
|
|
1548
|
+
// scrolls to its job). A job with no run to open stays a plain box. Never filtered or searched.
|
|
1549
|
+
function rlScheduledCardHtml(s){
|
|
1550
|
+
var body = '<span class="rl-l1"><span class="dot grey"></span><span class="rl-nm" title="' + escapeXml(s.job) + '">' + escapeXml(s.job) + '</span>' + rlRunTagHtml(s) +
|
|
1551
|
+
(s.at ? ' <span class="rl-rest">· ' + escapeXml(s.at) + '</span>' : '') + '</span>' +
|
|
1552
|
+
'<span class="rl-l2">' + escapeXml([s.in, s.words].filter(function(x){ return x; }).join(" · ")) + '</span>';
|
|
1553
|
+
if(typeof s.runId !== "string" || s.runId === "") return '<div class="rl-card" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</div>';
|
|
1554
|
+
return '<button class="rl-card" type="button" data-rl-jump="' + escapeXml(s.runId) + '" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</button>';
|
|
1555
|
+
}
|
|
1556
|
+
// The sections above the filters, from ONE ordered list: the Scheduled section (PRD v1.99) is one more entry here after Needs you, plus one draw function.
|
|
1557
|
+
function rlTopHtml(m){
|
|
1558
|
+
var sections = [
|
|
1559
|
+
{ label: "Running now", items: m.running, draw: function(r){ return rlJumpCardHtml(r, false); } },
|
|
1560
|
+
{ label: "Needs you", items: m.needs.concat(m.gaveUp || []), draw: function(r){ return r.pill ? rlGaveUpCardHtml(r) : rlJumpCardHtml(r, true); } },
|
|
1561
|
+
{ label: "Scheduled", items: m.scheduled, draw: rlScheduledCardHtml },
|
|
1562
|
+
];
|
|
1563
|
+
return sections.map(function(s){
|
|
1564
|
+
return s.items.length ? rlSectHtml(s.label, s.items.length) + s.items.map(s.draw).join("") : "";
|
|
1565
|
+
}).join("");
|
|
1566
|
+
}
|
|
1567
|
+
// a run card inside a job: `[sign] run-N MM/DD HH:MM · model` / `word — why · $cost · wall`
|
|
1568
|
+
function rlRunCardHtml(r, selected, hit){
|
|
1569
|
+
var p = rlRunParts(r);
|
|
1570
|
+
var bits = [p.spend, p.wall].filter(function(b){ return b; });
|
|
1571
|
+
var l2;
|
|
1572
|
+
if(r.fileMissing) l2 = escapeXml(p.why);
|
|
1573
|
+
else if(r.starting) l2 = '<b class="st-word">' + escapeXml(p.word) + '</b> — ' + escapeXml(p.why);
|
|
1574
|
+
else l2 = (p.word ? '<b class="st-word">' + escapeXml(p.word) + '</b>' : '') + (p.why ? (p.word ? ' — ' : '') + escapeXml(p.why) : '') + (bits.length ? ' · ' + escapeXml(bits.join(" · ")) : '');
|
|
1575
|
+
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) + '">' +
|
|
1576
|
+
'<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>' +
|
|
1577
|
+
'<span class="rl-l2">' + l2 + '</span></button>';
|
|
1578
|
+
}
|
|
1579
|
+
// 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.
|
|
1580
|
+
function rlJobCardHtml(c){
|
|
1581
|
+
var h = c.head;
|
|
1582
|
+
var top = '<div class="rl-card' + (c.selected ? ' rl-sel' : '') + '" id="rl-job-' + escapeXml(c.job) + '" data-testid="rl-job-' + escapeXml(c.job) + '">';
|
|
1583
|
+
if(c.empty){
|
|
1584
|
+
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>';
|
|
1510
1585
|
}
|
|
1511
|
-
var
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
// above) is normally the latest run, so this is unchanged in the
|
|
1545
|
-
// common case; a filter/search that matched an older run instead
|
|
1546
|
-
// makes the parent follow THAT run once it's the selected one.
|
|
1547
|
-
var isSelected = currentRunid === targetRunid;
|
|
1548
|
-
row.className = "wf-row" + (isSelected ? " selected" : "");
|
|
1549
|
-
row.setAttribute("data-wf", g.job);
|
|
1550
|
-
row.setAttribute("data-testid", "wf-row-" + g.job);
|
|
1551
|
-
row.setAttribute("tabindex", "0");
|
|
1552
|
-
row.setAttribute("role", "button");
|
|
1553
|
-
row.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1554
|
-
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
|
1555
|
-
var wfFullName = runName(g.job, g.lastRunNo, g.lastRunid);
|
|
1556
|
-
row.innerHTML =
|
|
1557
|
-
'<span class="wf-line1 wf-compact"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1558
|
-
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span>' +
|
|
1559
|
-
(g.lastStatus ? '<span class="wf-ended wf-ended-inline" data-testid="wf-ended-' + escapeXml(g.job) + '">' + statusWordHtml(g.lastStatus, g.lastEndedLine) + '</span>' : '') + '</span>' +
|
|
1560
|
-
'<span class="wf-meta-line">' +
|
|
1561
|
-
'<span class="wf-meta">' + escapeXml(g.lastCheckType) + '</span>' +
|
|
1562
|
-
'<span class="wf-meta">' + escapeXml(g.lastSpend) + '</span>' +
|
|
1563
|
-
'<span class="wf-meta">' + escapeXml(g.lastWall) + '</span>' +
|
|
1564
|
-
'<span class="wf-meta">' + escapeXml(g.lastDate || "unknown") + '</span>' +
|
|
1565
|
-
'</span>';
|
|
1566
|
-
row.addEventListener("click", function(){
|
|
1567
|
-
// a single-run job has nothing to expand — the click still opens
|
|
1568
|
-
// the run, it just never toggles `wfExpanded` for it.
|
|
1569
|
-
if(hasOtherRuns) wfExpanded[g.job] = !expanded;
|
|
1570
|
-
// build item (2026-09-26): clicking the parent also opens its
|
|
1571
|
-
// latest run (as it did before the Workflows/History merge) — pass
|
|
1572
|
-
// rowEl null since renderWorkflows(jobs) below rebuilds every row
|
|
1573
|
-
// right after, so a class set directly on this element would be
|
|
1574
|
-
// discarded; the fresh render marks selection off `currentRunid`
|
|
1575
|
-
// instead (see `isSelected` above). build item (2026-09-26 fix):
|
|
1576
|
-
// opens `targetRunid`, not always `g.lastRunid` — with a filter/
|
|
1577
|
-
// search active that excludes the latest but matches an older run,
|
|
1578
|
-
// that's the run the search actually found (see `representedRun`).
|
|
1579
|
-
selectRun(targetRunid, null, ".hist-row");
|
|
1580
|
-
document.getElementById("tab-run").click();
|
|
1581
|
-
scrollRunIntoViewMobile();
|
|
1582
|
-
renderWorkflows(jobs);
|
|
1583
|
-
});
|
|
1584
|
-
row.addEventListener("keydown", function(e){ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); row.click(); } });
|
|
1585
|
-
wrap.appendChild(row);
|
|
1586
|
-
|
|
1587
|
-
if(expanded){
|
|
1588
|
-
var runsWrap = document.createElement("div");
|
|
1589
|
-
runsWrap.className = "wf-runs";
|
|
1590
|
-
var capped = capSubRuns(childRuns, active, olderSelected);
|
|
1591
|
-
capped.shown.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1592
|
-
if(capped.more){
|
|
1593
|
-
var olderHint = document.createElement("div");
|
|
1594
|
-
olderHint.className = "hint";
|
|
1595
|
-
olderHint.setAttribute("data-testid", "wf-older-" + g.job);
|
|
1596
|
-
olderHint.textContent = "older: use search.";
|
|
1597
|
-
runsWrap.appendChild(olderHint);
|
|
1586
|
+
var p = rlRunParts(h);
|
|
1587
|
+
var isSel = function(r){ return r.runid === c.sel.runid; };
|
|
1588
|
+
var isHit = function(r){ return c.hits.indexOf(r.runid) !== -1; };
|
|
1589
|
+
var type = h.checkType && h.checkType !== "unknown" ? ' · ' + escapeXml(h.checkType) : '';
|
|
1590
|
+
var l2 = h.fileMissing ? escapeXml(p.why) : '<b class="st-word">' + escapeXml(p.word) + '</b>' + (p.why ? ' · ' + escapeXml(p.why) : '');
|
|
1591
|
+
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>' +
|
|
1592
|
+
'<span class="rl-l2">' + l2 + '</span>' +
|
|
1593
|
+
'<div class="rl-runs">' + rlRunCardHtml(h, isSel(h), isHit(h));
|
|
1594
|
+
if(c.older.total > 0){
|
|
1595
|
+
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>';
|
|
1596
|
+
if(c.older.open){
|
|
1597
|
+
out += c.older.shown.map(function(r){ return rlRunCardHtml(r, isSel(r), isHit(r)); }).join("");
|
|
1598
|
+
if(c.older.hidden > 0){
|
|
1599
|
+
// M11 am7 item 4 + am9, the line after the 7: the hidden runs' (searching: matches') real start days (the time the card
|
|
1600
|
+
// 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
|
|
1601
|
+
// `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.
|
|
1602
|
+
var days = c.older.left.map(function(x){
|
|
1603
|
+
var dd = x.at ? new Date(x.at) : null;
|
|
1604
|
+
return dd && !isNaN(dd.getTime()) ? new Date(dd.getFullYear(), dd.getMonth(), dd.getDate()) : null;
|
|
1605
|
+
});
|
|
1606
|
+
var md = function(dd){ return ("0" + (dd.getMonth() + 1)).slice(-2) + "/" + ("0" + dd.getDate()).slice(-2); };
|
|
1607
|
+
var dated = days.length > 0 && days.indexOf(null) === -1;
|
|
1608
|
+
var lo = dated ? new Date(Math.min.apply(null, days)) : null, hi = dated ? new Date(Math.max.apply(null, days)) : null;
|
|
1609
|
+
var k = c.older.hidden, runWord = k === 1 ? "run" : "runs";
|
|
1610
|
+
var one = dated && md(lo) === md(hi);
|
|
1611
|
+
// am9 (fwd verbatim, `matches` for every k): searching, the same days as the no-search line
|
|
1612
|
+
out += '<div class="rl-hint">' + (c.older.active ?
|
|
1613
|
+
(!dated ? k + ' more matches. Add a date or run number to narrow it.' :
|
|
1614
|
+
one ? k + ' more matches on ' + md(lo) + '. Add a run number to narrow it.' :
|
|
1615
|
+
k + ' more matches, ' + md(lo) + ' to ' + md(hi) + '. Add a date or run number to narrow it.') :
|
|
1616
|
+
!dated ? k + ' more older ' + runWord + '. Search to find one.' :
|
|
1617
|
+
one ? k + ' more ' + runWord + ' on ' + md(lo) + '. Search to find one.' :
|
|
1618
|
+
k + ' more older runs, ' + md(lo) + ' to ' + md(hi) + '. Search to find one.') + '</div>';
|
|
1598
1619
|
}
|
|
1599
|
-
wrap.appendChild(runsWrap);
|
|
1600
1620
|
}
|
|
1601
|
-
|
|
1621
|
+
}
|
|
1622
|
+
return out + '</div></div>';
|
|
1623
|
+
}
|
|
1624
|
+
function rlJobsHtml(m){
|
|
1625
|
+
var h = "";
|
|
1626
|
+
if(m.jobs.length) h += rlSectHtml("Jobs", m.jobs.length, "· run in the last " + m.limits.quietDays + " days") + m.jobs.map(rlJobCardHtml).join("");
|
|
1627
|
+
if(m.quiet.length){
|
|
1628
|
+
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>';
|
|
1629
|
+
if(m.quietOpen) h += m.quiet.map(rlJobCardHtml).join("");
|
|
1630
|
+
}
|
|
1631
|
+
if(h === "" && m.noRows) h = '<div class="hint">no runs listed yet</div>';
|
|
1632
|
+
return h;
|
|
1633
|
+
}
|
|
1634
|
+
// 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
|
|
1635
|
+
// `priorWallMs`; when that is not exact the card says it counts this leg only.
|
|
1636
|
+
function tickSince(){
|
|
1637
|
+
document.querySelectorAll("[data-rl-since]").forEach(function(el){
|
|
1638
|
+
var t = Date.parse(el.getAttribute("data-rl-since"));
|
|
1639
|
+
if(isNaN(t)){ el.textContent = "time unknown"; return; }
|
|
1640
|
+
var prior = el.getAttribute("data-rl-prior");
|
|
1641
|
+
var leg = prior === "leg";
|
|
1642
|
+
el.textContent = duration(Math.max(0, Date.now() - t) + (leg ? 0 : Number(prior) || 0)) + (leg ? " this leg" : "");
|
|
1602
1643
|
});
|
|
1603
1644
|
}
|
|
1645
|
+
// keep focus on the same card (by testid) across a real repaint of a box
|
|
1646
|
+
function withFocusPreserved(box, fn){
|
|
1647
|
+
var a = document.activeElement;
|
|
1648
|
+
var tid = a && box.contains && box.contains(a) && a.getAttribute ? a.getAttribute("data-testid") : null;
|
|
1649
|
+
fn();
|
|
1650
|
+
if(!tid) return;
|
|
1651
|
+
var nodes = box.querySelectorAll("[data-testid]");
|
|
1652
|
+
for(var i = 0; i < nodes.length; i += 1){
|
|
1653
|
+
if(nodes[i].getAttribute("data-testid") === tid){ if(nodes[i].focus) nodes[i].focus(); return; }
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
// 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.
|
|
1657
|
+
function paintRunsList(){
|
|
1658
|
+
var filters = currentRunsFilters();
|
|
1659
|
+
var groups = filterWorkflows(groupRunsByJob(rlData.runs, rlData.jobs), filters, Date.now());
|
|
1660
|
+
var m = rlModel(rlData, groups, filters, rlData.limits, { runid: currentRunid }, rlOpen);
|
|
1661
|
+
var topEl = document.getElementById("rl-top");
|
|
1662
|
+
var listEl = document.getElementById("wf-list");
|
|
1663
|
+
var top = rlTopHtml(m);
|
|
1664
|
+
var jobs = rlJobsHtml(m);
|
|
1665
|
+
if(top !== rlLast.top){ rlLast.top = top; withFocusPreserved(topEl, function(){ topEl.innerHTML = top; }); }
|
|
1666
|
+
if(jobs !== rlLast.jobs){ rlLast.jobs = jobs; withFocusPreserved(listEl, function(){ listEl.innerHTML = jobs; }); }
|
|
1667
|
+
tickSince();
|
|
1668
|
+
}
|
|
1669
|
+
function rlRowOf(runid){
|
|
1670
|
+
return rlData.runs.filter(function(r){ return r.runid === runid; })[0] || null;
|
|
1671
|
+
}
|
|
1672
|
+
// Running now / Needs you click: select that run, show it on the Run tab and scroll its job card into view.
|
|
1673
|
+
function rlJump(runid){
|
|
1674
|
+
selectRun(runid);
|
|
1675
|
+
document.getElementById("tab-run").click();
|
|
1676
|
+
var row = rlRowOf(runid);
|
|
1677
|
+
var card = row ? document.getElementById("rl-job-" + row.job) : null;
|
|
1678
|
+
if(card && card.scrollIntoView) card.scrollIntoView({ block: "nearest" });
|
|
1679
|
+
}
|
|
1680
|
+
// one delegated click handler for the whole list: a fold, a jump card or a run card
|
|
1681
|
+
function rlClick(ev){
|
|
1682
|
+
var t = ev.target && ev.target.closest ? ev.target : null;
|
|
1683
|
+
if(!t) return;
|
|
1684
|
+
var fold = t.closest("[data-rl-fold]");
|
|
1685
|
+
if(fold){
|
|
1686
|
+
rlOpen[fold.getAttribute("data-rl-fold")] = fold.getAttribute("aria-expanded") !== "true";
|
|
1687
|
+
paintRunsList();
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
var jump = t.closest("[data-rl-jump]");
|
|
1691
|
+
if(jump){ rlJump(jump.getAttribute("data-rl-jump")); return; }
|
|
1692
|
+
var card = t.closest("[data-rl-run]");
|
|
1693
|
+
if(card){
|
|
1694
|
+
selectRun(card.getAttribute("data-rl-run"));
|
|
1695
|
+
document.getElementById("tab-run").click();
|
|
1696
|
+
scrollRunIntoViewMobile();
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
document.getElementById("panel-runs").addEventListener("click", rlClick);
|
|
1604
1700
|
|
|
1605
1701
|
// ---- Filter bar (item 5 + hamr's 2026-09-25 live-panel feedback) ----
|
|
1606
1702
|
// ONE component, ONE state shape ({checkTypes, results, time}), used by
|
|
@@ -1611,51 +1707,38 @@
|
|
|
1611
1707
|
// "[ ]"; ".fchip" turns that off), time is a short label, then "clear".
|
|
1612
1708
|
// No "unknown" check-type chip — runs/workflows with unknown type still
|
|
1613
1709
|
// show whenever no type chip is selected (checkTypes stays empty).
|
|
1710
|
+
// The chips, in the order they are drawn: ONE declared array, never object key order.
|
|
1711
|
+
var RL_CHIPS = [
|
|
1712
|
+
{ group: "checkType", value: "deterministic", title: "deterministic", id: "ct-deterministic", label: "D" },
|
|
1713
|
+
{ group: "checkType", value: "rubric", title: "rubric", id: "ct-rubric", label: "R" },
|
|
1714
|
+
{ group: "result", value: "\u2713", title: "pass", id: "res-pass", label: "✓" },
|
|
1715
|
+
{ group: "result", value: "\u2717", title: "fail", id: "res-fail", label: "✗" },
|
|
1716
|
+
{ group: "result", value: "?", title: "died", id: "res-died", label: "?" },
|
|
1717
|
+
{ group: "result", value: "\u25B6", title: "running", id: "res-running", label: "▶" },
|
|
1718
|
+
{ group: "time", value: "7d", title: "last 7 days", id: "time-7d", label: "7d" },
|
|
1719
|
+
{ group: "time", value: "30d", title: "last 30 days", id: "time-30d", label: "30d" },
|
|
1720
|
+
{ group: "time", value: "all", title: "all time", id: "time-all", label: "all", pressed: true },
|
|
1721
|
+
{ clear: true },
|
|
1722
|
+
];
|
|
1614
1723
|
function filterBarHTML(scope){
|
|
1615
1724
|
return (
|
|
1616
1725
|
'<div class="filterbar" data-scope="' + scope + '" data-testid="' + scope + '-filters">' +
|
|
1617
|
-
'<input type="text" class="search-input" placeholder="
|
|
1726
|
+
'<input type="text" class="search-input" placeholder="job, run-23, or date 10/13" ' +
|
|
1618
1727
|
'id="' + scope + '-filter-search" data-testid="' + scope + '-filter-search" autocomplete="off">' +
|
|
1619
1728
|
'<div class="chips" role="group" aria-label="Filter">' +
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
'<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>' +
|
|
1625
|
-
'<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>' +
|
|
1626
|
-
'<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>' +
|
|
1627
|
-
'<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>' +
|
|
1628
|
-
'<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>' +
|
|
1629
|
-
'<button class="chip fchip" type="button" id="' + scope + '-filter-clear" data-testid="' + scope + '-filter-clear">clear</button>' +
|
|
1729
|
+
RL_CHIPS.map(function(c){
|
|
1730
|
+
if(c.clear) return '<button class="chip fchip" type="button" id="' + scope + '-filter-clear" data-testid="' + scope + '-filter-clear">clear</button>';
|
|
1731
|
+
return '<button class="chip fchip" type="button" aria-pressed="' + (c.pressed ? 'true' : 'false') + '" data-filter-group="' + c.group + '" data-filter-value="' + escapeXml(c.value) + '" title="' + c.title + '" data-testid="chip-' + scope + '-' + c.id + '">' + c.label + '</button>';
|
|
1732
|
+
}).join("") +
|
|
1630
1733
|
'</div>' +
|
|
1631
1734
|
'<div class="hint" id="' + scope + '-filter-count" data-testid="' + scope + '-filter-count"></div>' +
|
|
1632
1735
|
'</div>'
|
|
1633
1736
|
);
|
|
1634
1737
|
}
|
|
1635
1738
|
|
|
1636
|
-
// Pure function: chips toggle on/off independently and combine AND across
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
// `time` is single-select, default "all". `nowMs` is injectable for tests.
|
|
1640
|
-
// Field names (checkType/glyph/at) match a History run row's own shape.
|
|
1641
|
-
// item 1 (2026-09-25): case-insensitive substring match on job name AND
|
|
1642
|
-
// runid, combined AND with the chip groups (an empty/whitespace-only query
|
|
1643
|
-
// means "no search filter" — everything passes it, same posture as an
|
|
1644
|
-
// empty chip group). item C (2026-09-26): also matches the run's model
|
|
1645
|
-
// name (e.g. "deepseek", "sonnet") — `model` is `null`/absent on an older
|
|
1646
|
-
// archived run, same posture as a missing job/runid. Also the run's shown
|
|
1647
|
-
// name: `run-N` and `job (run-N)` (N is the server's `runNo`, never counted here).
|
|
1648
|
-
function matchesSearch(job, runid, model, query, runNo){
|
|
1649
|
-
if(!query) return true;
|
|
1650
|
-
var q = String(query).trim().toLowerCase();
|
|
1651
|
-
if(!q) return true;
|
|
1652
|
-
var j = String(job || "").toLowerCase();
|
|
1653
|
-
var r = String(runid || "").toLowerCase();
|
|
1654
|
-
var m = String(model || "").toLowerCase();
|
|
1655
|
-
var label = typeof runNo === "number" ? "run-" + runNo : "";
|
|
1656
|
-
var shown = label ? j + " (" + label + ")" : "";
|
|
1657
|
-
return j.indexOf(q) !== -1 || r.indexOf(q) !== -1 || m.indexOf(q) !== -1 || (label !== "" && (label.indexOf(q) !== -1 || shown.indexOf(q) !== -1));
|
|
1658
|
-
}
|
|
1739
|
+
// Pure function: chips toggle on/off independently and combine AND across the three groups (checkType/result/time), OR within a group (an empty
|
|
1740
|
+
// group array means "no filter" for that group — everything passes it). `time` is single-select, default "all". `nowMs` is injectable for tests.
|
|
1741
|
+
// The search is `runMatchesSearch`: the whole visible row, case-insensitive; an empty query passes everything.
|
|
1659
1742
|
function filterRuns(runs, filters, nowMs){
|
|
1660
1743
|
nowMs = (typeof nowMs === "number") ? nowMs : Date.now();
|
|
1661
1744
|
var ct = (filters && filters.checkTypes) || [];
|
|
@@ -1666,7 +1749,7 @@
|
|
|
1666
1749
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1667
1750
|
// a stopped run ([■]) was a [✗] before hamr's 2026-10-07 sign ruling — the fail chip still finds it
|
|
1668
1751
|
if(rs.length && rs.indexOf(r.glyph) === -1 && !(r.glyph === "\u25A0" && rs.indexOf("\u2717") !== -1)) return false;
|
|
1669
|
-
if(!
|
|
1752
|
+
if(!runMatchesSearch(r, q)) return false;
|
|
1670
1753
|
if(time !== "all"){
|
|
1671
1754
|
var days = time === "7d" ? 7 : 30;
|
|
1672
1755
|
var t = typeof r.at === "string" ? Date.parse(r.at) : NaN;
|
|
@@ -1681,13 +1764,12 @@
|
|
|
1681
1764
|
* One filter-bar instance: its own localStorage key, its own DOM scope
|
|
1682
1765
|
* (`.filterbar[data-scope="..."]`), its own item list — but the same
|
|
1683
1766
|
* wiring as every other instance.
|
|
1684
|
-
* @param {{scope:string, storageKey:string,
|
|
1767
|
+
* @param {{scope:string, storageKey:string, onChange:Function}} opts
|
|
1685
1768
|
*/
|
|
1686
1769
|
function createFilterBar(opts){
|
|
1687
1770
|
var scope = opts.scope;
|
|
1688
1771
|
var storageKey = opts.storageKey;
|
|
1689
1772
|
var state = { checkTypes: [], results: [], time: "all", search: "" };
|
|
1690
|
-
var allItems = [];
|
|
1691
1773
|
|
|
1692
1774
|
function load(){
|
|
1693
1775
|
try{
|
|
@@ -1722,10 +1804,7 @@
|
|
|
1722
1804
|
if(searchEl && searchEl.value !== state.search) searchEl.value = state.search;
|
|
1723
1805
|
}
|
|
1724
1806
|
function apply(){
|
|
1725
|
-
|
|
1726
|
-
opts.applyFn(filtered);
|
|
1727
|
-
var countEl = document.getElementById(scope + "-filter-count");
|
|
1728
|
-
if(countEl) countEl.textContent = "showing " + filtered.length + " of " + allItems.length + " runs";
|
|
1807
|
+
opts.onChange(state);
|
|
1729
1808
|
}
|
|
1730
1809
|
document.querySelectorAll('.filterbar[data-scope="' + scope + '"] .chip[data-filter-group]').forEach(function(chip){
|
|
1731
1810
|
chip.addEventListener("click", function(){
|
|
@@ -1766,72 +1845,22 @@
|
|
|
1766
1845
|
paint();
|
|
1767
1846
|
|
|
1768
1847
|
return {
|
|
1769
|
-
setItems: function(items){ allItems = items || []; apply(); },
|
|
1770
1848
|
getState: function(){ return state; },
|
|
1771
1849
|
refresh: function(){ apply(); },
|
|
1772
1850
|
};
|
|
1773
1851
|
}
|
|
1774
1852
|
|
|
1775
|
-
//
|
|
1776
|
-
// per-viewer-convenience posture as every other localStorage read/write on
|
|
1777
|
-
// this page (try/catch, never assumed available).
|
|
1778
|
-
var RUNS_VIEW_KEY = "bareloop-panel-runs-view";
|
|
1779
|
-
function loadRunsViewMode(){
|
|
1780
|
-
try{
|
|
1781
|
-
var v = localStorage.getItem(RUNS_VIEW_KEY);
|
|
1782
|
-
if(v === "workflows" || v === "history") return v;
|
|
1783
|
-
}catch(e){ /* per-viewer convenience only */ }
|
|
1784
|
-
return "workflows";
|
|
1785
|
-
}
|
|
1786
|
-
function saveRunsViewMode(v){
|
|
1787
|
-
try{ localStorage.setItem(RUNS_VIEW_KEY, v); }catch(e){ /* best-effort only */ }
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// item 2 (2026-09-26 merge): ONE shared filter bar drives whichever view
|
|
1791
|
-
// (Workflows/History) is currently on — one scope, one localStorage key,
|
|
1792
|
-
// so switching views never loses the search/chip state.
|
|
1853
|
+
// ONE filter bar drives the runs list (the Workflows / History switch is gone, P9 v2): one scope, one localStorage key.
|
|
1793
1854
|
document.getElementById("runs-filterbar-anchor").innerHTML = filterBarHTML("runs");
|
|
1794
1855
|
|
|
1795
|
-
var runsViewMode = loadRunsViewMode();
|
|
1796
|
-
function runsFilterFn(items, filters, nowMs){
|
|
1797
|
-
if(runsViewMode === "history") return filterRuns(items, filters, nowMs);
|
|
1798
|
-
return filterWorkflows(groupRunsByJob(items), filters, nowMs);
|
|
1799
|
-
}
|
|
1800
|
-
function runsApplyFn(filtered){
|
|
1801
|
-
if(runsViewMode === "history") renderHistory(filtered);
|
|
1802
|
-
else renderWorkflows(filtered);
|
|
1803
|
-
}
|
|
1804
1856
|
var runsFilterBar = createFilterBar({
|
|
1805
1857
|
scope: "runs",
|
|
1806
1858
|
storageKey: "bareloop-panel-runs-filters",
|
|
1807
|
-
|
|
1808
|
-
applyFn: runsApplyFn,
|
|
1859
|
+
onChange: function(){ paintRunsList(); },
|
|
1809
1860
|
});
|
|
1810
1861
|
function currentRunsFilters(){
|
|
1811
1862
|
return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "" };
|
|
1812
1863
|
}
|
|
1813
|
-
function paintRunsViewToggle(){
|
|
1814
|
-
document.querySelectorAll('.chip[data-runs-view]').forEach(function(c){
|
|
1815
|
-
c.setAttribute("aria-pressed", c.getAttribute("data-runs-view") === runsViewMode ? "true" : "false");
|
|
1816
|
-
});
|
|
1817
|
-
document.getElementById("wf-list").hidden = runsViewMode !== "workflows";
|
|
1818
|
-
document.getElementById("history-list").hidden = runsViewMode !== "history";
|
|
1819
|
-
// P5 item 4: Import lives on the Workflows view only
|
|
1820
|
-
document.getElementById("wf-import").hidden = runsViewMode !== "workflows";
|
|
1821
|
-
document.getElementById("import-list").hidden = runsViewMode !== "workflows";
|
|
1822
|
-
if(runsViewMode !== "workflows") document.getElementById("import-box").hidden = true;
|
|
1823
|
-
}
|
|
1824
|
-
document.querySelectorAll('.chip[data-runs-view]').forEach(function(chip){
|
|
1825
|
-
chip.addEventListener("click", function(){
|
|
1826
|
-
var next = chip.getAttribute("data-runs-view");
|
|
1827
|
-
if(next === runsViewMode) return;
|
|
1828
|
-
runsViewMode = next;
|
|
1829
|
-
saveRunsViewMode(runsViewMode);
|
|
1830
|
-
paintRunsViewToggle();
|
|
1831
|
-
runsFilterBar.refresh();
|
|
1832
|
-
});
|
|
1833
|
-
});
|
|
1834
|
-
paintRunsViewToggle();
|
|
1835
1864
|
|
|
1836
1865
|
// item 4: phone-only "scroll the run view into view" on row select, and
|
|
1837
1866
|
// the matching "↑ list" jump back. Desktop never runs either — both panes
|
|
@@ -1853,8 +1882,8 @@
|
|
|
1853
1882
|
});
|
|
1854
1883
|
}
|
|
1855
1884
|
|
|
1856
|
-
function clearSelection(
|
|
1857
|
-
document.querySelectorAll(".wf-row
|
|
1885
|
+
function clearSelection(){
|
|
1886
|
+
document.querySelectorAll(".wf-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
1858
1887
|
}
|
|
1859
1888
|
|
|
1860
1889
|
// ---- P5 item 4: IMPORT (read only) — the folder browser, the imported rows, the view ----
|
|
@@ -1969,13 +1998,14 @@
|
|
|
1969
1998
|
currentImportId = null;
|
|
1970
1999
|
document.getElementById("import-view").hidden = true;
|
|
1971
2000
|
document.getElementById("run-counters").hidden = false;
|
|
1972
|
-
document.getElementById("run-empty").textContent = "select a run from
|
|
2001
|
+
document.getElementById("run-empty").textContent = "select a run from the Runs list";
|
|
1973
2002
|
document.getElementById("audit-select-empty").textContent = "select a run first";
|
|
1974
2003
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed", "false"); });
|
|
1975
2004
|
}
|
|
1976
2005
|
function selectImport(id){
|
|
1977
2006
|
clearSelection();
|
|
1978
2007
|
currentRunid = null;
|
|
2008
|
+
paintRunsList();
|
|
1979
2009
|
runIsLive = false;
|
|
1980
2010
|
currentImportId = id;
|
|
1981
2011
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
@@ -2159,8 +2189,9 @@
|
|
|
2159
2189
|
* complete run whose drafting share is itself incomplete — hamr's ruling:
|
|
2160
2190
|
* a reader summing the two must never read the leading number as exact
|
|
2161
2191
|
* when the bracket next to it isn't). `draftSpendComplete === false` marks
|
|
2162
|
-
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX". `shortForm`
|
|
2163
|
-
* "drafting" (the Runs list cards only — "$5.00 ($0.08)"
|
|
2192
|
+
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX". `shortForm` moves the word
|
|
2193
|
+
* "drafting" to lead inside the bracket (the Runs list cards only — "$5.00 (drafting $0.08)",
|
|
2194
|
+
* floor "(drafting at least $0.08)").
|
|
2164
2195
|
*/
|
|
2165
2196
|
function panelMoneyWithDraft(spendCoreVal, spendCoreFloor, draftSpentUsd, draftSpendComplete, budgetUsd, shortForm){
|
|
2166
2197
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
@@ -2168,7 +2199,7 @@
|
|
|
2168
2199
|
var core = (spendCoreVal === null || spendCoreVal === undefined)
|
|
2169
2200
|
? "unknown"
|
|
2170
2201
|
: ((spendCoreFloor || draftFloor) ? "at least " + panelMoney(spendCoreVal) : panelMoney(spendCoreVal));
|
|
2171
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2202
|
+
var draftPart = hasDraft ? (shortForm ? " (drafting " : " (") + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2172
2203
|
var capPart = (budgetUsd === undefined) ? "" : (" of " + panelMoney(budgetUsd));
|
|
2173
2204
|
return core + draftPart + capPart;
|
|
2174
2205
|
}
|
|
@@ -2226,7 +2257,7 @@
|
|
|
2226
2257
|
if(spendFloorUsd === null || spendFloorUsd === undefined) return "unknown";
|
|
2227
2258
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
2228
2259
|
var draftFloor = hasDraft && draftSpendComplete === false;
|
|
2229
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2260
|
+
var draftPart = hasDraft ? (shortForm ? " (drafting " : " (") + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2230
2261
|
return panelMoney(spendFloorUsd) + " so far" + draftPart;
|
|
2231
2262
|
}
|
|
2232
2263
|
/** build item 6 — the matching wall-clock phrase: "running <duration>",
|
|
@@ -2369,55 +2400,70 @@
|
|
|
2369
2400
|
else { row.hidden = true; val.textContent = ""; }
|
|
2370
2401
|
}
|
|
2371
2402
|
|
|
2372
|
-
// ---- P5: the ENDED block (
|
|
2373
|
-
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2374
|
-
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2403
|
+
// ---- P5: the ENDED block (Run tab, first block above the action row, every finished run) and Resume ----
|
|
2375
2404
|
var lastEndedSig = null;
|
|
2405
|
+
function summaryRow(label, text, testid, red, extraHtml){
|
|
2406
|
+
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>';
|
|
2407
|
+
}
|
|
2376
2408
|
// 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
|
|
2377
2409
|
// in RESUME mode, only the money cap and the time cap open (applyResume / signResume). The Job tab is read-only on
|
|
2378
2410
|
// every run — values only, no inputs, no buttons. A resume is the SAME run: no new card, no new job.
|
|
2411
|
+
// 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
|
|
2412
|
+
// (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.
|
|
2413
|
+
// 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.
|
|
2379
2414
|
function renderEnded(detail){
|
|
2380
2415
|
var box = document.getElementById("ended-block");
|
|
2381
2416
|
var e = detail.ended;
|
|
2382
|
-
|
|
2417
|
+
var sc = detail.schedule;
|
|
2418
|
+
if(!e && !sc){ box.hidden = true; box.innerHTML = ""; lastEndedSig = null; return; }
|
|
2383
2419
|
// rebuilt only when what it shows changed
|
|
2384
|
-
var sig2 = detail.runid + "|" + JSON.stringify(e) + "|" + JSON.stringify(detail.resume);
|
|
2420
|
+
var sig2 = detail.runid + "|" + JSON.stringify(e) + "|" + JSON.stringify(detail.resume) + "|" + (detail.endedText || "") + "|" + JSON.stringify(sc && [sc.scheduled, sc.text, sc.line2, sc.problem]);
|
|
2385
2421
|
if(sig2 === lastEndedSig) return;
|
|
2386
2422
|
lastEndedSig = sig2;
|
|
2387
2423
|
box.hidden = false;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2424
|
+
var problem = !!(sc && sc.scheduled && sc.problem === true);
|
|
2425
|
+
box.className = "ended-block " + (problem ? "red" : (!e ? "" : (detail.died ? "magenta" : (detail.glyph === "✓" ? "green" : (detail.glyph === "■" ? "grey" : "red")))));
|
|
2426
|
+
var h = "";
|
|
2427
|
+
if(e){
|
|
2428
|
+
// text only: the ONE place for [Stop] / [Resume] / [Reuse workflow] is the Run tab's top action row (renderRunActions)
|
|
2429
|
+
h += summaryRow("ENDED", detail.endedText || e.reason, "ended-reason", false) + summaryRow("NEXT", e.next, "ended-next", false);
|
|
2430
|
+
}
|
|
2431
|
+
if(sc){
|
|
2432
|
+
var w = sc.words || {};
|
|
2433
|
+
var label = w.scheduledLabel || "SCHEDULE";
|
|
2434
|
+
if(sc.scheduled){
|
|
2435
|
+
h += summaryRow(label, sc.text, "sched-text", problem, sc.line2 ? '<span class="sched-line2" data-testid="sched-line2">' + escapeXml(sc.line2) + '</span>' : "");
|
|
2436
|
+
} else {
|
|
2437
|
+
h += summaryRow(label, w.byHand || "By hand", "sched-text", false);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
box.innerHTML = h;
|
|
2393
2441
|
}
|
|
2394
2442
|
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2395
|
-
// it ended in a way the engine would accept (opens the Chat card's resume mode),
|
|
2396
|
-
// The Ended block (
|
|
2443
|
+
// it ended in a way the engine would accept (opens the Chat card's resume mode), Reuse workflow on every ending.
|
|
2444
|
+
// The Ended block (Run tab, above this row) is text only and carries no buttons.
|
|
2397
2445
|
var stopAsked = {};
|
|
2398
2446
|
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
2399
2447
|
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2400
2448
|
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2401
|
-
function reuseWorkflow(runid, model){
|
|
2449
|
+
function reuseWorkflow(runid, model, label){
|
|
2402
2450
|
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&" + model : "")).then(function(r){
|
|
2403
2451
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2404
2452
|
document.getElementById("tab-chat").click();
|
|
2405
|
-
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2453
|
+
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r, label: label || ""}}));
|
|
2406
2454
|
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2407
2455
|
}
|
|
2408
|
-
//
|
|
2409
|
-
//
|
|
2410
|
-
//
|
|
2411
|
-
function
|
|
2412
|
-
var
|
|
2413
|
-
|
|
2414
|
-
". Every box is open. Change anything and it drafts as a new job; change nothing and it is another run of the same job.";
|
|
2415
|
-
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2456
|
+
// Schedule run / Cancel / modify schedule (T Part 7): starts nothing and signs nothing. It opens the SAME Chat card as the neighbouring Reuse
|
|
2457
|
+
// workflow button, in schedule mode (the When field showing the current schedule). The run it copies from is the server's answer
|
|
2458
|
+
// (`detail.schedule.fromRunid`: the green the schedule was made from), so a red run of a scheduled job still opens the card.
|
|
2459
|
+
function scheduleInChat(detail){
|
|
2460
|
+
var from = (detail.schedule && detail.schedule.fromRunid) || detail.runid;
|
|
2461
|
+
authorGet("/api/author/start-from?mode=schedule&runid=" + encodeURIComponent(from)).then(function(r){
|
|
2416
2462
|
document.getElementById("tab-chat").click();
|
|
2417
|
-
document.dispatchEvent(new CustomEvent("bareloop-
|
|
2463
|
+
document.dispatchEvent(new CustomEvent("bareloop-schedule", {detail: (r && r.ok) ? {runid: from, prefill: r, job: detail.job} : {error: (r && r.error) || "nothing to schedule"}}));
|
|
2418
2464
|
}).catch(function(x){
|
|
2419
2465
|
document.getElementById("tab-chat").click();
|
|
2420
|
-
document.dispatchEvent(new CustomEvent("bareloop-
|
|
2466
|
+
document.dispatchEvent(new CustomEvent("bareloop-schedule", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2421
2467
|
});
|
|
2422
2468
|
}
|
|
2423
2469
|
// Resume (hamr 2026-10-07, option B — moved from the run's Job tab): the Chat card opens in RESUME mode, filled from the
|
|
@@ -2447,20 +2493,46 @@
|
|
|
2447
2493
|
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2448
2494
|
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2449
2495
|
}
|
|
2450
|
-
//
|
|
2451
|
-
|
|
2452
|
-
|
|
2496
|
+
// Accept (T Part 8): right after Resume, before Reuse workflow. Shown only when the SERVER says this run waits at its review door
|
|
2497
|
+
// (`detail.accept`: a door record, not expired, no honoured decision after it). It records the person's accept through the engine's own door.
|
|
2498
|
+
if(!live && detail.accept === true){
|
|
2499
|
+
html += '<button class="btn primary" type="button" data-testid="btn-accept-run">Accept</button>';
|
|
2453
2500
|
}
|
|
2454
|
-
// Reuse workflow:
|
|
2455
|
-
if(!live &&
|
|
2501
|
+
// Reuse workflow: ONE door on every ending (v1.101), a died run included - the Chat card opens with every box open
|
|
2502
|
+
if(!live && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2456
2503
|
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
2457
2504
|
}
|
|
2505
|
+
// T Part 7: the schedule button is LAST, right after Reuse workflow. The SERVER decides (detail.schedule): `scheduled` shows
|
|
2506
|
+
// Cancel / modify schedule (every run of the job, also while one is live: cancel is never refused); else `canSchedule` shows
|
|
2507
|
+
// Schedule run; else no button. The label never changes in place — a different state is a different button.
|
|
2508
|
+
var sc = detail.schedule || null;
|
|
2509
|
+
var scw = (sc && sc.words) || {};
|
|
2510
|
+
if(sc && sc.scheduled){
|
|
2511
|
+
html += '<button class="btn" type="button" data-testid="run-cancel-schedule">' + escapeXml(scw.cancelModify || "") + '</button>';
|
|
2512
|
+
} else if(sc && sc.canSchedule){
|
|
2513
|
+
html += '<button class="btn" type="button" data-testid="run-schedule">' + escapeXml(scw.scheduleRun || "") + '</button>';
|
|
2514
|
+
}
|
|
2515
|
+
// 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).
|
|
2516
|
+
if(!live && detail.canRemoveFromNeeds === true){
|
|
2517
|
+
html += '<button class="btn" type="button" data-testid="btn-needs-remove">Remove from Needs you</button>';
|
|
2518
|
+
}
|
|
2458
2519
|
bar.innerHTML = html;
|
|
2459
2520
|
bar.hidden = html === "";
|
|
2521
|
+
var needsBtn = bar.querySelector('[data-testid="btn-needs-remove"]');
|
|
2522
|
+
if(needsBtn){
|
|
2523
|
+
needsBtn.addEventListener("click", function(){
|
|
2524
|
+
needsBtn.disabled = true;
|
|
2525
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/needs-remove", {}).then(function(res){
|
|
2526
|
+
if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
|
|
2527
|
+
needsBtn.disabled = false;
|
|
2528
|
+
bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="needs-remove-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
|
|
2529
|
+
}).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>'); });
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
var schedBtn = bar.querySelector('[data-testid="run-schedule"], [data-testid="run-cancel-schedule"]');
|
|
2533
|
+
if(schedBtn) schedBtn.addEventListener("click", function(){ scheduleInChat(detail); });
|
|
2460
2534
|
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2461
|
-
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2462
|
-
var editBtn = bar.querySelector('[data-testid="btn-edit-chat"]');
|
|
2463
|
-
if(editBtn) editBtn.addEventListener("click", function(){ editInChat(detail); });
|
|
2535
|
+
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid, "", runName(detail.job, detail.runNo, detail.runid)); });
|
|
2464
2536
|
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2465
2537
|
if(stopBtn){
|
|
2466
2538
|
stopBtn.addEventListener("click", function(){
|
|
@@ -2471,6 +2543,21 @@
|
|
|
2471
2543
|
}).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>'); });
|
|
2472
2544
|
});
|
|
2473
2545
|
}
|
|
2546
|
+
var accBtn = bar.querySelector('[data-testid="btn-accept-run"]');
|
|
2547
|
+
if(accBtn){
|
|
2548
|
+
accBtn.addEventListener("click", function(){
|
|
2549
|
+
accBtn.disabled = true;
|
|
2550
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/accept", {}).then(function(res){
|
|
2551
|
+
if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
|
|
2552
|
+
if(res.status === 202 && res.body && res.body.pending){
|
|
2553
|
+
bar.insertAdjacentHTML("beforeend", '<span class="stopping" data-testid="accept-pending">still re-checking the tree; look again in a moment</span>');
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
accBtn.disabled = false;
|
|
2557
|
+
bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
|
|
2558
|
+
}).catch(function(x){ accBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2474
2561
|
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2475
2562
|
if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
|
|
2476
2563
|
}
|
|
@@ -2482,12 +2569,25 @@
|
|
|
2482
2569
|
if(currentRunid !== runid) return;
|
|
2483
2570
|
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(d){
|
|
2484
2571
|
if(currentRunid !== runid) return;
|
|
2485
|
-
if(d.glyph === "▶" && !d.died){ selectRun(runid
|
|
2572
|
+
if(d.glyph === "▶" && !d.died){ selectRun(runid); return; }
|
|
2486
2573
|
if(tries > 0) afterResumeRefresh(runid, tries - 1);
|
|
2487
2574
|
}).catch(function(){ if(tries > 0) afterResumeRefresh(runid, tries - 1); });
|
|
2488
2575
|
}, 1500);
|
|
2489
2576
|
}
|
|
2490
2577
|
|
|
2578
|
+
// M3 (Part 1c full-run fixes): after Sign & run the new run's row is written a few seconds later (the runner starts, then
|
|
2579
|
+
// appends its row), and the Runs list only re-fetches every RUNS_LIST_POLL_MS — so a run just signed showed only after a page
|
|
2580
|
+
// refresh. Like a resume, a sign forces list refreshes until a run that was not listed before the click appears; bounded.
|
|
2581
|
+
function afterSignRefresh(knownIds, tries){
|
|
2582
|
+
refreshRunsList(true).then(function(runs){
|
|
2583
|
+
var fresh = Array.isArray(runs) && runs.some(function(r){ return knownIds.indexOf(r.runid) === -1; });
|
|
2584
|
+
if(!fresh && tries > 0) setTimeout(function(){ afterSignRefresh(knownIds, tries - 1); }, 1500);
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
function currentRunIds(){
|
|
2588
|
+
try { return (JSON.parse(lastRunsSig || "[]") || []).map(function(r){ return r.runid; }); } catch(e){ return []; }
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2491
2591
|
function renderRun(detail){
|
|
2492
2592
|
if(detail.fileMissing){
|
|
2493
2593
|
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
@@ -2514,7 +2614,7 @@
|
|
|
2514
2614
|
document.getElementById("audit-body").hidden = false;
|
|
2515
2615
|
renderEnded(detail);
|
|
2516
2616
|
|
|
2517
|
-
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), runHeaderBody(detail));
|
|
2617
|
+
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), runHeaderBody(detail), detail.accept ? "Accept" : null);
|
|
2518
2618
|
|
|
2519
2619
|
document.getElementById("run-empty").hidden = true;
|
|
2520
2620
|
document.getElementById("run-content").hidden = false;
|
|
@@ -2566,7 +2666,7 @@
|
|
|
2566
2666
|
" · " + wallPhrase;
|
|
2567
2667
|
|
|
2568
2668
|
var box = document.getElementById("run-summary");
|
|
2569
|
-
box.className = "summary-box " + (detail.died ? "magenta" : (
|
|
2669
|
+
box.className = "summary-box " + (detail.died ? "magenta" : (detail.glyph === "✗" ? "red" : (detail.glyph === "■" ? "grey" : (detail.glyph === "▶" ? "amber" : ""))));
|
|
2570
2670
|
// F196: a synthetic placeholder box (e.g. "died during planning") is a
|
|
2571
2671
|
// LABEL, not a real step — excluded from this count so a run that died
|
|
2572
2672
|
// before any step started reads "0 of 0 done" / "none started", never
|
|
@@ -2646,7 +2746,7 @@
|
|
|
2646
2746
|
// 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
|
|
2647
2747
|
function stepStateHTML(box, testid){
|
|
2648
2748
|
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")));
|
|
2649
|
-
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
|
|
2749
|
+
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + (cls === "amber" ? " pulse" : "") + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
|
|
2650
2750
|
}
|
|
2651
2751
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2652
2752
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
@@ -3041,6 +3141,57 @@
|
|
|
3041
3141
|
var isLiveRun = detail.glyph === "▶" && !detail.died;
|
|
3042
3142
|
var boxes = buildOrderedBoxes(parts, isLiveRun);
|
|
3043
3143
|
var numberMap = stepNumberIndices(boxes);
|
|
3144
|
+
// 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
|
|
3145
|
+
// opens, worded on the server (`detail.scheduleSection`: title, sign, head, rows [{label,text}]). A run a CANCELLED schedule started keeps its one old line.
|
|
3146
|
+
if(typeof detail.startedAudit === "string" && detail.startedAudit !== ""){
|
|
3147
|
+
var startedEl = document.createElement("div");
|
|
3148
|
+
startedEl.className = "hint";
|
|
3149
|
+
startedEl.setAttribute("data-testid", "audit-started");
|
|
3150
|
+
startedEl.style.overflowWrap = "anywhere";
|
|
3151
|
+
startedEl.textContent = detail.startedAudit;
|
|
3152
|
+
container.appendChild(startedEl);
|
|
3153
|
+
}
|
|
3154
|
+
var sec = detail.scheduleSection;
|
|
3155
|
+
if(sec && Array.isArray(sec.rows) && sec.rows.length){
|
|
3156
|
+
var gWrap = document.createElement("div");
|
|
3157
|
+
gWrap.className = "audit-part";
|
|
3158
|
+
gWrap.setAttribute("data-testid", "audit-schedule-group");
|
|
3159
|
+
var gBtn = document.createElement("button");
|
|
3160
|
+
gBtn.type = "button";
|
|
3161
|
+
gBtn.className = "btn audit-part-toggle";
|
|
3162
|
+
gBtn.setAttribute("aria-expanded", "false");
|
|
3163
|
+
gBtn.setAttribute("data-testid", "audit-schedule-toggle");
|
|
3164
|
+
var secHead = function(open){ return (open ? "\u25BE " : "\u25B8 ") + escapeXml(sec.title) + " [" + glyphSpan(sec.sign) + "] " + escapeXml(sec.head); };
|
|
3165
|
+
gBtn.innerHTML = secHead(false);
|
|
3166
|
+
var gBody = document.createElement("div");
|
|
3167
|
+
gBody.className = "audit-part-body";
|
|
3168
|
+
gBody.hidden = true;
|
|
3169
|
+
gBody.setAttribute("data-testid", "audit-schedule-body");
|
|
3170
|
+
// same row look as the drafting/step tables: the `rounds-table` in the `audit-table-scroll` wrapper (label column, dotted row lines)
|
|
3171
|
+
var schedRows = "";
|
|
3172
|
+
sec.rows.forEach(function(l){
|
|
3173
|
+
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>';
|
|
3174
|
+
});
|
|
3175
|
+
gBody.innerHTML = '<div class="audit-table-scroll"><table class="rounds-table" data-testid="audit-schedule-table"><tbody>' + schedRows + '</tbody></table></div>';
|
|
3176
|
+
gBtn.addEventListener("click", function(){
|
|
3177
|
+
var open = gBtn.getAttribute("aria-expanded") === "true";
|
|
3178
|
+
gBtn.setAttribute("aria-expanded", open ? "false" : "true");
|
|
3179
|
+
gBody.hidden = open;
|
|
3180
|
+
gBtn.innerHTML = secHead(!open);
|
|
3181
|
+
});
|
|
3182
|
+
gWrap.appendChild(gBtn);
|
|
3183
|
+
gWrap.appendChild(gBody);
|
|
3184
|
+
container.appendChild(gWrap);
|
|
3185
|
+
}
|
|
3186
|
+
// RULED 2026-10-15: how the run left Needs you (by you, or after 7 days), worded on the server
|
|
3187
|
+
if(typeof detail.needsAudit === "string" && detail.needsAudit !== ""){
|
|
3188
|
+
var needsEl = document.createElement("div");
|
|
3189
|
+
needsEl.className = "hint";
|
|
3190
|
+
needsEl.setAttribute("data-testid", "audit-needs-removed");
|
|
3191
|
+
needsEl.style.overflowWrap = "anywhere";
|
|
3192
|
+
needsEl.textContent = detail.needsAudit;
|
|
3193
|
+
container.appendChild(needsEl);
|
|
3194
|
+
}
|
|
3044
3195
|
// P5-R: a resumed run is ONE run — its legs are told apart by a divider line (code-owned text from the server)
|
|
3045
3196
|
var dividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
3046
3197
|
boxes.forEach(function(box, idx){
|
|
@@ -3235,14 +3386,15 @@
|
|
|
3235
3386
|
// (src/panel/jobplan.js): numbered lines with their rule, the checks under the line each serves (folded), the AI's own
|
|
3236
3387
|
// "Not checked", the always-on guards. Only the marker is blue (--plan). Returns HTML (every text is escaped).
|
|
3237
3388
|
function renderJobPlan(plan){
|
|
3238
|
-
var h = '<div class="jp" data-testid="job-plan">' + (plan.older ? '<div class="older" data-testid="plan-older">older job \u2014 numbered lines were not saved</div>' : "") + '<div class="jp-legend"
|
|
3389
|
+
var h = '<div class="jp" data-testid="job-plan">' + (plan.older ? '<div class="older" data-testid="plan-older">older job \u2014 numbered lines were not saved</div>' : "") + '<div class="jp-legend">Done when every <b>\u203A</b> check passes</div>' +
|
|
3239
3390
|
(plan.goal && !plan.older ? '<div class="jp-sum" data-testid="plan-goal"><span class="lbl">AI\'s summary</span>' + escapeXml(plan.goal) + "</div>" : "");
|
|
3240
3391
|
var checkLine = function(c){
|
|
3241
3392
|
return '<div class="fold" data-testid="plan-check"><span class="mk">\u203A</span> check \u00B7 ' + escapeXml(c.name) + (c.cls ? " \u00B7 " + escapeXml(c.cls) : "") + "</div>";
|
|
3242
3393
|
};
|
|
3243
3394
|
(plan.lines || []).forEach(function(l){
|
|
3244
3395
|
h += '<div class="jl" data-testid="plan-line"><span class="num">' + escapeXml(String(l.n)) + "</span>" + escapeXml(l.text);
|
|
3245
|
-
|
|
3396
|
+
// one `~` row per rule (fwdloop's shape); an older plan that carries only the joined `rule` still shows it
|
|
3397
|
+
(l.rules && l.rules.length ? l.rules : (l.rule ? [l.rule] : [])).forEach(function(r){ h += '<div class="sub">~ ' + escapeXml(r) + "</div>"; });
|
|
3246
3398
|
(l.checks || []).forEach(function(c){ h += checkLine(c); });
|
|
3247
3399
|
(l.refused || []).forEach(function(r){ h += '<div class="fold" data-testid="plan-refused"><span class="mk">\u203A</span> not checked \u00B7 ' + escapeXml(r) + "</div>"; });
|
|
3248
3400
|
h += "</div>";
|
|
@@ -3256,7 +3408,7 @@
|
|
|
3256
3408
|
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3257
3409
|
};
|
|
3258
3410
|
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3259
|
-
h += fold("Always
|
|
3411
|
+
h += fold("Always checked", plan.alwaysOn, "plan-alwayson");
|
|
3260
3412
|
return h + "</div>";
|
|
3261
3413
|
}
|
|
3262
3414
|
|
|
@@ -3272,6 +3424,12 @@
|
|
|
3272
3424
|
var verdictEl = document.getElementById("details-verdict");
|
|
3273
3425
|
if(job.checkTypeTitle){ verdictEl.title = job.checkTypeTitle; } else { verdictEl.removeAttribute("title"); }
|
|
3274
3426
|
document.getElementById("details-model").textContent = job.model;
|
|
3427
|
+
// RULED 2026-10-15: how the run began and the job's schedule NOW, both worded on the server (absent on an imported job)
|
|
3428
|
+
[["started", job.started], ["schedule", job.schedule]].forEach(function(p){
|
|
3429
|
+
var f = document.getElementById("details-" + p[0] + "-field");
|
|
3430
|
+
if(typeof p[1] === "string" && p[1] !== ""){ f.hidden = false; document.getElementById("details-" + p[0]).textContent = p[1]; }
|
|
3431
|
+
else { f.hidden = true; }
|
|
3432
|
+
});
|
|
3275
3433
|
var descField = document.getElementById("details-description-field");
|
|
3276
3434
|
if(job.description){ descField.hidden = false; document.getElementById("details-description").textContent = job.description; }
|
|
3277
3435
|
else { descField.hidden = true; }
|
|
@@ -3303,21 +3461,27 @@
|
|
|
3303
3461
|
// from the run's own `glyph`/`died` fields, same rule `renderRun` already
|
|
3304
3462
|
// uses for `isLive` — never re-derived a second way).
|
|
3305
3463
|
var runIsLive = false;
|
|
3306
|
-
|
|
3464
|
+
// a FINISHED run of a scheduled job is still refetched on the list cadence: its SCHEDULED / NEXT block changes without the run
|
|
3465
|
+
// changing (a refused fire turns NEXT red, a slot passes), and nothing else would repaint it
|
|
3466
|
+
var openIsScheduled = false;
|
|
3467
|
+
var lastOpenFetchAt = 0;
|
|
3468
|
+
function selectRun(runid){
|
|
3307
3469
|
hideImportView();
|
|
3308
3470
|
clearSelection();
|
|
3309
|
-
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
3310
|
-
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
3311
3471
|
currentRunid = runid;
|
|
3472
|
+
paintRunsList(); // the list marks the selected run itself (rl-sel); it writes only when the markup changed
|
|
3312
3473
|
lastLegDividers = [];
|
|
3313
3474
|
lastJobToolsList = null;
|
|
3314
3475
|
lastRunDetailSig = null;
|
|
3315
3476
|
runIsLive = false;
|
|
3477
|
+
openIsScheduled = false;
|
|
3478
|
+
lastOpenFetchAt = Date.now();
|
|
3316
3479
|
var myToken = ++selectToken;
|
|
3317
3480
|
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(detail){
|
|
3318
3481
|
if(myToken !== selectToken) return; // a different run was selected before this resolved
|
|
3319
3482
|
lastRunDetailSig = sig(detail);
|
|
3320
3483
|
runIsLive = detail.glyph === "▶" && !detail.died;
|
|
3484
|
+
openIsScheduled = !!(detail.schedule && detail.schedule.scheduled);
|
|
3321
3485
|
renderRun(detail);
|
|
3322
3486
|
}).catch(function(e){
|
|
3323
3487
|
if(myToken !== selectToken) return;
|
|
@@ -3370,6 +3534,7 @@
|
|
|
3370
3534
|
var RUNS_LIST_POLL_MS = 10000;
|
|
3371
3535
|
var pollTimer = null;
|
|
3372
3536
|
var lastRunsSig = null;
|
|
3537
|
+
var lastRunsPayloadSig = null;
|
|
3373
3538
|
var lastRunDetailSig = null;
|
|
3374
3539
|
var lastRunsListRefreshAt = 0;
|
|
3375
3540
|
var pollFailStreak = 0;
|
|
@@ -3401,15 +3566,16 @@
|
|
|
3401
3566
|
return getJSON("/api/runs").then(function(result){
|
|
3402
3567
|
pollFailStreak = 0;
|
|
3403
3568
|
var runs = result.runs || [];
|
|
3404
|
-
var next = sig(
|
|
3405
|
-
if(next ===
|
|
3406
|
-
|
|
3569
|
+
var next = sig(result);
|
|
3570
|
+
if(next === lastRunsPayloadSig) return runs; // unchanged payload — never rebuild the DOM for nothing
|
|
3571
|
+
lastRunsPayloadSig = next;
|
|
3572
|
+
lastRunsSig = sig(runs);
|
|
3407
3573
|
withScrollPreserved(["left-pane-body"], function(){
|
|
3408
|
-
//
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
runsFilterBar.setItems(runs);
|
|
3574
|
+
// 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.
|
|
3575
|
+
setRunsData(result);
|
|
3576
|
+
runsFilterBar.refresh();
|
|
3412
3577
|
});
|
|
3578
|
+
return runs;
|
|
3413
3579
|
}).catch(function(){
|
|
3414
3580
|
pollFailStreak++;
|
|
3415
3581
|
// Keep the last good render; a transient failure is invisible. Only a
|
|
@@ -3423,7 +3589,10 @@
|
|
|
3423
3589
|
}
|
|
3424
3590
|
|
|
3425
3591
|
function refreshOpenRun(){
|
|
3426
|
-
if(!currentRunid
|
|
3592
|
+
if(!currentRunid) return Promise.resolve();
|
|
3593
|
+
var scheduleDue = openIsScheduled && (Date.now() - lastOpenFetchAt) >= RUNS_LIST_POLL_MS;
|
|
3594
|
+
if(!runIsLive && !scheduleDue) return Promise.resolve();
|
|
3595
|
+
lastOpenFetchAt = Date.now();
|
|
3427
3596
|
var runid = currentRunid;
|
|
3428
3597
|
var myToken = selectToken;
|
|
3429
3598
|
return getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(detail){
|
|
@@ -3434,6 +3603,7 @@
|
|
|
3434
3603
|
var changed = next !== lastRunDetailSig;
|
|
3435
3604
|
lastRunDetailSig = next;
|
|
3436
3605
|
runIsLive = detail.glyph === "▶" && !detail.died;
|
|
3606
|
+
openIsScheduled = !!(detail.schedule && detail.schedule.scheduled);
|
|
3437
3607
|
if(changed){
|
|
3438
3608
|
withScrollPreserved(["right-pane-body"], function(){ renderRun(detail); });
|
|
3439
3609
|
}
|
|
@@ -3442,6 +3612,7 @@
|
|
|
3442
3612
|
|
|
3443
3613
|
function pollTick(forceList){
|
|
3444
3614
|
if(document.hidden) return;
|
|
3615
|
+
tickSince(); // Running now: the wall counts up in place
|
|
3445
3616
|
refreshRunsList(forceList);
|
|
3446
3617
|
var wasLive = runIsLive;
|
|
3447
3618
|
return refreshOpenRun().then(function(){
|
|
@@ -3467,25 +3638,17 @@
|
|
|
3467
3638
|
getJSON("/api/runs").then(function(result){
|
|
3468
3639
|
var runs = result.runs || [];
|
|
3469
3640
|
lastRunsSig = sig(runs);
|
|
3470
|
-
|
|
3641
|
+
lastRunsPayloadSig = sig(result);
|
|
3642
|
+
setRunsData(result);
|
|
3643
|
+
runsFilterBar.refresh();
|
|
3471
3644
|
if(runs.length && !runs[0].fileMissing){
|
|
3472
|
-
//
|
|
3473
|
-
|
|
3474
|
-
// away from Chat on every load that had a run). The RIGHT pane still
|
|
3475
|
-
// loads the newest run's details so they're ready the moment a
|
|
3476
|
-
// person switches to the Runs tab themselves — the Workflows view
|
|
3477
|
-
// starts collapsed, but its job row still renders "selected" (build
|
|
3478
|
-
// item, 2026-09-26: a job row shows selected state like a child row)
|
|
3479
|
-
// since `runsFilterBar.refresh()` re-renders AFTER `currentRunid` is
|
|
3480
|
-
// set.
|
|
3481
|
-
selectRun(runs[0].runid, null, ".hist-row");
|
|
3645
|
+
// 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.
|
|
3646
|
+
selectRun(runs[0].runid);
|
|
3482
3647
|
document.getElementById("tab-run").click();
|
|
3483
|
-
runsFilterBar.refresh();
|
|
3484
3648
|
}
|
|
3485
3649
|
startPolling();
|
|
3486
3650
|
}).catch(function(e){
|
|
3487
3651
|
document.getElementById("wf-list").innerHTML = '<div class="hint">failed to load: ' + escapeXml(e.message) + '</div>';
|
|
3488
|
-
document.getElementById("history-list").innerHTML = '<div class="hint">failed to load: ' + escapeXml(e.message) + '</div>';
|
|
3489
3652
|
startPolling(); // still start — a transient initial failure shouldn't block all future polling
|
|
3490
3653
|
});
|
|
3491
3654
|
|
|
@@ -3906,6 +4069,7 @@
|
|
|
3906
4069
|
var mainHint = document.getElementById("chat-main-hint");
|
|
3907
4070
|
var reopenBtn = document.getElementById("chat-reopen-btn");
|
|
3908
4071
|
var mainAction = "none";
|
|
4072
|
+
var knownRunIdsAtSign = null; // the run ids listed when Sign & run was pressed (M3: afterSignRefresh waits for a new one)
|
|
3909
4073
|
var lastState = null;
|
|
3910
4074
|
var startOk = false;
|
|
3911
4075
|
var errEl = document.getElementById("chat-card-error");
|
|
@@ -3936,6 +4100,18 @@
|
|
|
3936
4100
|
// Reuse workflow — set when the card was opened by [Reuse workflow]: the run (or imported job) it came from.
|
|
3937
4101
|
// Only Source, Destination and the two caps are open on that card; the SERVER refuses any other change.
|
|
3938
4102
|
var startFrom = null;
|
|
4103
|
+
// ONE door (v1.101): what the card was prefilled from, so the page can tell whether a JOB box changed. {card, green, name, nextName}
|
|
4104
|
+
// `name` = the origin's job name (shown while no job box changed), `nextName` = the server's next free <base>-N (shown once one does).
|
|
4105
|
+
var reuseOrigin = null;
|
|
4106
|
+
var nameTouched = false; // the person typed their own name for a changed job
|
|
4107
|
+
// T Part 7: the card in SCHEDULE mode (opened by Schedule run / Cancel-modify schedule): the Reuse card plus the When field.
|
|
4108
|
+
// `sched` = {job, runid, words, cur, scheduled, refusal, now} while it is on, else null. `cur` = the signed trigger (the When field's
|
|
4109
|
+
// starting value); every word comes from `words` (the server's one table, src/schedulewords.js SCHEDULE_WORDS).
|
|
4110
|
+
var sched = null;
|
|
4111
|
+
var whenHash = null; // the hash click 1 returned; null = click 1 not done (or a When / card box was edited since)
|
|
4112
|
+
var whenConfirm = null; // the lower-cased words the label shows after click 1: "Confirm: every day at 13:15"
|
|
4113
|
+
var whenBusy = false; // a request is out: the main button dims and cannot be pressed
|
|
4114
|
+
var whenLocked = false; // ~1.5 s after click 2 the main button is dead (hamr clicked Run twice thinking it was the trigger)
|
|
3939
4115
|
// 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
|
|
3940
4116
|
var reuseCheckType = "deterministic";
|
|
3941
4117
|
var reusePickedName = "";
|
|
@@ -3962,7 +4138,8 @@
|
|
|
3962
4138
|
// the one exception on a resume card: an older spec that names no model leaves the Model box open (picked at resume)
|
|
3963
4139
|
var modelOpen = resuming && resumeRun.pickModel === true;
|
|
3964
4140
|
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1 && !(modelOpen && id === "jf-model"))); });
|
|
3965
|
-
|
|
4141
|
+
// a schedule card keeps the Model the green run had (T9): only Inputs, Destination, When and the two caps are open
|
|
4142
|
+
document.getElementById("jf-model").disabled = live || (resuming && !modelOpen) || !!sched;
|
|
3966
4143
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3967
4144
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3968
4145
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
@@ -3977,7 +4154,8 @@
|
|
|
3977
4154
|
NUMBERED_IDS.forEach(gutterSync);
|
|
3978
4155
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3979
4156
|
}
|
|
3980
|
-
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
4157
|
+
startFrom = null; reuseOrigin = null; nameTouched = false; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
4158
|
+
scheduleOff();
|
|
3981
4159
|
sfLine.hidden = true; sfNote.hidden = true; originLine.hidden = true;
|
|
3982
4160
|
setReuseLocked(false);
|
|
3983
4161
|
renderPicker();
|
|
@@ -4019,7 +4197,6 @@
|
|
|
4019
4197
|
rwSearch.hidden = picked;
|
|
4020
4198
|
rwPicked.hidden = !picked;
|
|
4021
4199
|
rwPickedTxt.innerHTML = picked ? "<b>" + escapeXml(reusePickedName) + "</b>" : "";
|
|
4022
|
-
if(!picked) sfLine.hidden = true;
|
|
4023
4200
|
}
|
|
4024
4201
|
function rwMark(name, q){
|
|
4025
4202
|
var i = q ? name.toLowerCase().indexOf(q.toLowerCase()) : -1;
|
|
@@ -4205,13 +4382,18 @@
|
|
|
4205
4382
|
// Check type: Deterministic / Rubric / Reuse workflow. Reuse opens the picker on an empty card; Deterministic or
|
|
4206
4383
|
// Rubric while the picker is showing (picked or not) LEAVES reuse — boxes untouched (clearStartFrom(true)).
|
|
4207
4384
|
function onVerdictChange(v){
|
|
4208
|
-
if(v === "reuse"){
|
|
4385
|
+
if(v === "reuse"){
|
|
4386
|
+
if(startFrom !== null){ clearStartFrom(true); setVerdict("reuse"); openPicker(); return; }
|
|
4387
|
+
if(rwBox.hidden) openPicker();
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4209
4390
|
if(!rwBox.hidden){
|
|
4210
4391
|
clearStartFrom(true);
|
|
4211
4392
|
setVerdict(v);
|
|
4212
4393
|
renderPicker();
|
|
4213
4394
|
}
|
|
4214
4395
|
setJobPlaceholders(v);
|
|
4396
|
+
syncReuseName();
|
|
4215
4397
|
refreshStartEnabled();
|
|
4216
4398
|
}
|
|
4217
4399
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
@@ -4458,6 +4640,22 @@
|
|
|
4458
4640
|
var ask = st.pendingAsk;
|
|
4459
4641
|
var atMenu = !!ask && ask.kind === "menu";
|
|
4460
4642
|
var hint = "";
|
|
4643
|
+
// 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,
|
|
4644
|
+
// reading "Start drafting" — never a Run / Sign & run invitation
|
|
4645
|
+
if(!st.session && st.whenLocked){
|
|
4646
|
+
return {text: "Start drafting", action: "none", disabled: true, hint: ""};
|
|
4647
|
+
}
|
|
4648
|
+
// 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
|
|
4649
|
+
// nothing. After click 1 it reads "Confirm: <words>". It never reaches the run routes. Unchanged = disabled, with the plain words that nothing changed.
|
|
4650
|
+
if(!st.session && st.when){
|
|
4651
|
+
var ww = st.when.words || {};
|
|
4652
|
+
var wtext = st.when.confirm ? (ww.confirmPrefix || "") + st.when.confirm : (st.when.cancel ? ww.cancelSchedule : ww.signSchedule);
|
|
4653
|
+
if(st.when.changed){
|
|
4654
|
+
return {text: wtext, action: st.when.confirm ? "when-sign" : "when-sign-prepare", disabled: !!st.when.busy, hint: ""};
|
|
4655
|
+
}
|
|
4656
|
+
// 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)
|
|
4657
|
+
return {text: ww.signSchedule, action: "none", disabled: true, hint: st.when.refused ? "" : (ww.nothingChanged || "")};
|
|
4658
|
+
}
|
|
4461
4659
|
if(!st.session){
|
|
4462
4660
|
return {text: reuse ? "Sign & run" : "Start drafting", action: "start", disabled: !st.startOk, hint: ""};
|
|
4463
4661
|
}
|
|
@@ -4486,7 +4684,8 @@
|
|
|
4486
4684
|
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind === "menu";
|
|
4487
4685
|
}
|
|
4488
4686
|
function renderMain(){
|
|
4489
|
-
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce};
|
|
4687
|
+
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce, whenLocked: whenLocked};
|
|
4688
|
+
if(sched){ st.when = {changed: whenChanged(), refused: !!sched.refusal, cancel: byId("jf-when").value === "hand", confirm: whenConfirm, busy: whenBusy, words: sched.words}; }
|
|
4490
4689
|
if(sessionLive && lastState){
|
|
4491
4690
|
st.phase = lastState.phase; st.pendingAsk = lastState.pendingAsk;
|
|
4492
4691
|
st.specHash = lastState.specHash;
|
|
@@ -4495,7 +4694,7 @@
|
|
|
4495
4694
|
var open = askBoxOpenFor(st);
|
|
4496
4695
|
msgInput.disabled = !open;
|
|
4497
4696
|
if(!open) msgInput.value = "";
|
|
4498
|
-
var b = mainButtonFor(st, !msgInput.value.trim(),
|
|
4697
|
+
var b = mainButtonFor(st, !msgInput.value.trim(), reusePathNow() || verdictValue() === "reuse");
|
|
4499
4698
|
mainBtn.textContent = b.text;
|
|
4500
4699
|
mainBtn.disabled = b.disabled;
|
|
4501
4700
|
mainAction = b.action;
|
|
@@ -4596,6 +4795,7 @@
|
|
|
4596
4795
|
renderActions(j.state);
|
|
4597
4796
|
if(reuseSession && !autoSigned && j.state.phase === "prepared" && j.state.specHash){
|
|
4598
4797
|
autoSigned = true;
|
|
4798
|
+
knownRunIdsAtSign = currentRunIds();
|
|
4599
4799
|
authorPost("/api/author/" + sessionId + "/sign", {specHash: j.state.specHash}).then(function(r){
|
|
4600
4800
|
var o = chatPostOutcome(r);
|
|
4601
4801
|
if(o.ok){ chatActionOk(); } else { autoSigned = false; chatActionFailed(o.error); }
|
|
@@ -4604,6 +4804,7 @@
|
|
|
4604
4804
|
}
|
|
4605
4805
|
if(j.state.phase === "signed"){
|
|
4606
4806
|
clearInterval(pollTimer);
|
|
4807
|
+
afterSignRefresh(knownRunIdsAtSign || currentRunIds(), 12);
|
|
4607
4808
|
document.getElementById("tab-runs").click();
|
|
4608
4809
|
// hamr's ruling 2026-10-05: the run has started, so the Chat card goes back to the empty card (the one reset
|
|
4609
4810
|
// Clear uses) — it must not keep showing a signed job that invites a rerun. The run itself is untouched.
|
|
@@ -4626,6 +4827,8 @@
|
|
|
4626
4827
|
authorPost("/api/author/start", body).then(function(r){
|
|
4627
4828
|
if(r.status !== 200 || !r.body.ok){
|
|
4628
4829
|
errEl.textContent = (r.body && r.body.error) || "refused";
|
|
4830
|
+
// the refusal is the answer to the click: bring it into view (it sits under the cap fields, above the progress list)
|
|
4831
|
+
try { errEl.scrollIntoView({block: "nearest"}); } catch(e){}
|
|
4629
4832
|
sessionLive = false;
|
|
4630
4833
|
refreshStartEnabled();
|
|
4631
4834
|
return;
|
|
@@ -4684,7 +4887,7 @@
|
|
|
4684
4887
|
|
|
4685
4888
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4686
4889
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4687
|
-
// ONE owner of filling the card from a prefill (a re-attached session, Reuse,
|
|
4890
|
+
// ONE owner of filling the card from a prefill (a re-attached session, Reuse, Resume): the six boxes P7 left
|
|
4688
4891
|
// (the old Goal/Source/Success/Guardrails/Judge boxes are gone), then every box re-fits and its number gutter re-measures.
|
|
4689
4892
|
function fillCard(c){
|
|
4690
4893
|
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
@@ -4694,20 +4897,27 @@
|
|
|
4694
4897
|
fitCardBoxes();
|
|
4695
4898
|
}
|
|
4696
4899
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4697
|
-
// `bareloop-reuse` event)
|
|
4900
|
+
// `bareloop-reuse` event). ONE DOOR (v1.101): every box is open and prefilled; nothing is locked. Only the schedule card
|
|
4901
|
+
// (`d.lock`) keeps the job boxes locked - it signs the green run's plan as it is.
|
|
4698
4902
|
function applyReuse(d){
|
|
4699
4903
|
var pre = d.prefill || {};
|
|
4700
4904
|
var c = pre.card || {};
|
|
4905
|
+
var lock = d.lock === true;
|
|
4701
4906
|
clearStartFrom();
|
|
4702
4907
|
openNewCard();
|
|
4703
4908
|
startFrom = d.importId ? {importId: d.importId} : {runid: d.runid};
|
|
4704
4909
|
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4705
4910
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4706
|
-
setVerdict("reuse");
|
|
4911
|
+
setVerdict(lock ? "reuse" : reuseCheckType);
|
|
4707
4912
|
if(c.model){ setPick(c); }
|
|
4708
4913
|
fillCard(c);
|
|
4709
|
-
|
|
4710
|
-
|
|
4914
|
+
reuseOrigin = {card: c, green: pre.green === true, name: reusePickedName, nextName: pre.nextName || ""};
|
|
4915
|
+
nameTouched = false;
|
|
4916
|
+
setReuseLocked(lock);
|
|
4917
|
+
if(!lock){
|
|
4918
|
+
originLine.textContent = "Reuse: " + (d.label || reusePickedName);
|
|
4919
|
+
originLine.hidden = false;
|
|
4920
|
+
}
|
|
4711
4921
|
renderPicker();
|
|
4712
4922
|
sfLine.hidden = false;
|
|
4713
4923
|
sfLine.textContent = pre.line ? rwMeta({checkType: reuseCheckType, line: pre.line}) : "";
|
|
@@ -4720,27 +4930,33 @@
|
|
|
4720
4930
|
refreshStartEnabled();
|
|
4721
4931
|
}
|
|
4722
4932
|
document.addEventListener("bareloop-reuse", function(e){ applyReuse(e.detail || {}); });
|
|
4723
|
-
|
|
4724
|
-
//
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
clearStartFrom();
|
|
4731
|
-
openNewCard();
|
|
4732
|
-
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4733
|
-
if(c.model){ setPick(c); }
|
|
4734
|
-
fillCard(c);
|
|
4735
|
-
|
|
4736
|
-
originLine.textContent = d.origin || "";
|
|
4737
|
-
originLine.hidden = !d.origin;
|
|
4738
|
-
renderPicker();
|
|
4739
|
-
refreshModelStatus();
|
|
4740
|
-
refreshCapNote();
|
|
4741
|
-
refreshStartEnabled();
|
|
4933
|
+
// 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
|
|
4934
|
+
// server makes on the start (whitespace ignored): the Run boxes (Inputs, Destination, Model, caps) never count.
|
|
4935
|
+
function jobBoxesChangedNow(){
|
|
4936
|
+
if(reuseOrigin === null) return false;
|
|
4937
|
+
var cur = currentCard(), o = reuseOrigin.card;
|
|
4938
|
+
var t = function(v){ return typeof v === "string" ? v.replace(/\s+/g, "") : ""; };
|
|
4939
|
+
return t(cur.jobText) !== t(o.jobText) || cur.checkType !== (o.checkType === "rubric" ? "rubric" : "deterministic") || (cur.thinking === true) !== (o.thinking === true);
|
|
4742
4940
|
}
|
|
4743
|
-
|
|
4941
|
+
// the reuse path (signed plan verbatim, no drafting, $0): a card made by Reuse workflow from a GREEN run with no job box changed
|
|
4942
|
+
function reusePathNow(){
|
|
4943
|
+
return startFrom !== null && (reuseOn || (reuseOrigin !== null && reuseOrigin.green === true && !jobBoxesChangedNow()));
|
|
4944
|
+
}
|
|
4945
|
+
// 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
|
|
4946
|
+
// again when the boxes go back. A name typed while nothing changed is put back (the origin's name is forced).
|
|
4947
|
+
function syncReuseName(){
|
|
4948
|
+
if(reuseOrigin === null || reuseOn) return;
|
|
4949
|
+
var nameEl = document.getElementById("jf-name");
|
|
4950
|
+
if(!jobBoxesChangedNow()){ nameEl.value = reuseOrigin.name; nameTouched = false; }
|
|
4951
|
+
else if(!nameTouched){ nameEl.value = reuseOrigin.nextName; }
|
|
4952
|
+
}
|
|
4953
|
+
function reuseBoxesEdited(){ syncReuseName(); renderMain(); }
|
|
4954
|
+
document.getElementById("jf-job").addEventListener("input", reuseBoxesEdited);
|
|
4955
|
+
thinkingBox.addEventListener("change", reuseBoxesEdited);
|
|
4956
|
+
document.getElementById("jf-name").addEventListener("input", function(){
|
|
4957
|
+
if(reuseOrigin === null || reuseOn) return;
|
|
4958
|
+
if(jobBoxesChangedNow()) nameTouched = true; else syncReuseName();
|
|
4959
|
+
});
|
|
4744
4960
|
|
|
4745
4961
|
// Resume — the card in RESUME mode: the signed job filled and locked, only Cap $ and Time cap open (prefilled from the
|
|
4746
4962
|
// engine's own resume plan, the values the Job tab used to show). A live drafting session is handled exactly as
|
|
@@ -4906,6 +5122,7 @@
|
|
|
4906
5122
|
authorGet("/api/author/" + sessionId).then(function(j){
|
|
4907
5123
|
var hash = j.state && j.state.specHash;
|
|
4908
5124
|
if(!hash) return;
|
|
5125
|
+
knownRunIdsAtSign = currentRunIds();
|
|
4909
5126
|
authorPost("/api/author/" + sessionId + "/sign", {specHash: hash}).then(function(r){
|
|
4910
5127
|
var o = chatPostOutcome(r);
|
|
4911
5128
|
if(o.ok){ chatActionOk(); } else { chatActionFailed(o.error); return; }
|
|
@@ -4914,12 +5131,150 @@
|
|
|
4914
5131
|
});
|
|
4915
5132
|
}
|
|
4916
5133
|
|
|
5134
|
+
// ---- T Part 7: the schedule card (Schedule run / Cancel-modify schedule, PANEL-BUILD.md T addendum C.7, design/schedule-mock.html) ----
|
|
5135
|
+
// 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
|
|
5136
|
+
// server's words, refusals and hashes. Click 1 (schedule-prepare) is $0 and signs nothing; click 2 (schedule) signs; NOTHING runs.
|
|
5137
|
+
function byId(id){ return document.getElementById(id); }
|
|
5138
|
+
var WHEN_KINDS = ["hand", "daily", "weekdays", "weekly", "file-updated", "new-file"];
|
|
5139
|
+
function whenTriggerFromBoxes(){
|
|
5140
|
+
var kind = byId("jf-when").value;
|
|
5141
|
+
if(kind === "hand") return null;
|
|
5142
|
+
if(kind === "daily" || kind === "weekdays" || kind === "weekly"){
|
|
5143
|
+
var m = /^(\d{2}):(\d{2})$/.exec(byId("jf-when-time").value);
|
|
5144
|
+
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
|
|
5145
|
+
if(kind === "weekly") o.day = byId("jf-when-day").value;
|
|
5146
|
+
return o;
|
|
5147
|
+
}
|
|
5148
|
+
var q = {kind: kind};
|
|
5149
|
+
q[kind === "new-file" ? "folder" : "path"] = byId("jf-when-path").value.trim();
|
|
5150
|
+
return q;
|
|
5151
|
+
}
|
|
5152
|
+
// one comparable spelling of a trigger (or By hand), so "changed" is a plain string compare
|
|
5153
|
+
function whenCanon(t){
|
|
5154
|
+
if(!t) return "hand";
|
|
5155
|
+
return [t.kind, t.hour === undefined ? "" : t.hour, t.minute === undefined ? "" : t.minute, t.day || "", t.path || t.folder || ""].join("|");
|
|
5156
|
+
}
|
|
5157
|
+
// 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
|
|
5158
|
+
// 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
|
|
5159
|
+
// (a re-sign); on an unscheduled job only When does (nothing to re-sign until a schedule is picked).
|
|
5160
|
+
function whenChanged(){
|
|
5161
|
+
if(!sched || sched.refusal) return false;
|
|
5162
|
+
if(whenCanon(whenTriggerFromBoxes()) !== whenCanon(sched.cur)) return true;
|
|
5163
|
+
if(sched.scheduled !== true || !sched.base) return false;
|
|
5164
|
+
var b = sched.base;
|
|
5165
|
+
return byId("jf-inputs").value.trim() !== b.inputs || byId("jf-dest").value.trim() !== b.dest ||
|
|
5166
|
+
byId("jf-cap-money").value.trim() !== b.capUsd || byId("jf-cap-time").value.trim() !== b.capTime;
|
|
5167
|
+
}
|
|
5168
|
+
function whenShowSubs(){
|
|
5169
|
+
var kind = byId("jf-when").value;
|
|
5170
|
+
byId("jf-when-clock").hidden = !(kind === "daily" || kind === "weekdays" || kind === "weekly");
|
|
5171
|
+
byId("jf-when-day").hidden = kind !== "weekly";
|
|
5172
|
+
byId("jf-when-filebox").hidden = !(kind === "file-updated" || kind === "new-file");
|
|
5173
|
+
byId("jf-when-path").placeholder = kind === "new-file" ? "/full/path/to/folder" : "/full/path/to/file";
|
|
5174
|
+
}
|
|
5175
|
+
function paintWhen(){
|
|
5176
|
+
var field = byId("jf-when-field");
|
|
5177
|
+
field.hidden = !sched;
|
|
5178
|
+
if(!sched) return;
|
|
5179
|
+
var w = sched.words || {};
|
|
5180
|
+
var opts = w.options || {};
|
|
5181
|
+
byId("jf-when-label").textContent = w.whenLabel || "";
|
|
5182
|
+
var sel = byId("jf-when");
|
|
5183
|
+
sel.innerHTML = "";
|
|
5184
|
+
WHEN_KINDS.forEach(function(k){
|
|
5185
|
+
var op = document.createElement("option");
|
|
5186
|
+
op.value = k;
|
|
5187
|
+
// on a scheduled job the first option reads Cancel schedule instead of By hand
|
|
5188
|
+
op.textContent = k === "hand" ? (sched.scheduled ? w.cancelSchedule : w.byHand) : (opts[k] || k);
|
|
5189
|
+
sel.appendChild(op);
|
|
5190
|
+
});
|
|
5191
|
+
var t = sched.cur;
|
|
5192
|
+
sel.value = t ? t.kind : "hand";
|
|
5193
|
+
if(t && t.hour !== undefined){ byId("jf-when-time").value = ("0" + t.hour).slice(-2) + ":" + ("0" + t.minute).slice(-2); }
|
|
5194
|
+
if(t && t.day) byId("jf-when-day").value = t.day;
|
|
5195
|
+
byId("jf-when-path").value = t ? (t.path || t.folder || "") : "";
|
|
5196
|
+
// refused = the select and every sub-box are DISABLED and the reason stands where "Now:" would be
|
|
5197
|
+
var refused = !!sched.refusal;
|
|
5198
|
+
sel.disabled = byId("jf-when-day").disabled = byId("jf-when-time").disabled = byId("jf-when-path").disabled = refused;
|
|
5199
|
+
byId("jf-when-now").textContent = refused ? sched.refusal : (sched.now ? (w.nowPrefix || "") + sched.now : "");
|
|
5200
|
+
byId("jf-err-when").textContent = "";
|
|
5201
|
+
whenShowSubs();
|
|
5202
|
+
}
|
|
5203
|
+
function scheduleOff(){
|
|
5204
|
+
sched = null; whenHash = null; whenConfirm = null; whenBusy = false;
|
|
5205
|
+
paintWhen();
|
|
5206
|
+
}
|
|
5207
|
+
// editing any When box between the two clicks DROPS the hash: the label returns to Sign schedule
|
|
5208
|
+
function whenEdited(){
|
|
5209
|
+
whenHash = null; whenConfirm = null;
|
|
5210
|
+
byId("jf-err-when").textContent = "";
|
|
5211
|
+
whenShowSubs();
|
|
5212
|
+
renderMain();
|
|
5213
|
+
}
|
|
5214
|
+
["jf-when", "jf-when-day", "jf-when-time"].forEach(function(id){ byId(id).addEventListener("change", whenEdited); });
|
|
5215
|
+
byId("jf-when-path").addEventListener("input", whenEdited);
|
|
5216
|
+
// 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)
|
|
5217
|
+
["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(); }); });
|
|
5218
|
+
function doWhenSign(){
|
|
5219
|
+
if(!sched || whenBusy || whenLocked) return;
|
|
5220
|
+
var timeCapMin = byId("jf-cap-time").value.trim();
|
|
5221
|
+
var body = {
|
|
5222
|
+
runid: sched.runid, when: whenTriggerFromBoxes(), inputs: byId("jf-inputs").value.trim(), destination: byId("jf-dest").value.trim(),
|
|
5223
|
+
capUsd: Number(byId("jf-cap-money").value), maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
5224
|
+
};
|
|
5225
|
+
var second = whenHash !== null;
|
|
5226
|
+
if(second) body.hash = whenHash;
|
|
5227
|
+
whenBusy = true; renderMain();
|
|
5228
|
+
authorPost(second ? "/api/author/schedule" : "/api/author/schedule-prepare", body).then(function(r){
|
|
5229
|
+
whenBusy = false;
|
|
5230
|
+
if(!(r.status >= 200 && r.status < 300 && r.body && r.body.ok === true)){
|
|
5231
|
+
whenHash = null; whenConfirm = null;
|
|
5232
|
+
byId("jf-err-when").textContent = (r.body && (r.body.say || r.body.error)) || ("refused (" + r.status + ")");
|
|
5233
|
+
renderMain(); return;
|
|
5234
|
+
}
|
|
5235
|
+
var words = typeof r.body.words === "string" ? r.body.words : "";
|
|
5236
|
+
if(!second){
|
|
5237
|
+
whenHash = r.body.hash;
|
|
5238
|
+
whenConfirm = words ? words.charAt(0).toLowerCase() + words.slice(1) : "";
|
|
5239
|
+
byId("jf-err-when").textContent = "";
|
|
5240
|
+
renderMain(); return;
|
|
5241
|
+
}
|
|
5242
|
+
// after click 2 the card clears to the empty New job card and NOTHING runs; the button is dead for 1.5 s
|
|
5243
|
+
var say = typeof r.body.say === "string" ? r.body.say : "";
|
|
5244
|
+
whenHash = null; whenConfirm = null;
|
|
5245
|
+
whenLocked = true;
|
|
5246
|
+
setTimeout(function(){ whenLocked = false; renderMain(); }, 1500);
|
|
5247
|
+
resetCard();
|
|
5248
|
+
if(say) chatActionFailed(say);
|
|
5249
|
+
refreshRunsList(true);
|
|
5250
|
+
if(currentRunid) selectRun(currentRunid);
|
|
5251
|
+
}).catch(function(){
|
|
5252
|
+
whenBusy = false; whenHash = null; whenConfirm = null;
|
|
5253
|
+
byId("jf-err-when").textContent = "network error — could not reach the panel server, nothing was signed";
|
|
5254
|
+
renderMain();
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5257
|
+
// the card opened by Schedule run / Cancel-modify schedule: the SAME Reuse fill (applyReuse), then the When field
|
|
5258
|
+
function applyScheduleCard(d){
|
|
5259
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
5260
|
+
if(sessionLive || resumeRun !== null){ errEl.textContent = "Finish or clear what is on the card first, then try again."; return; }
|
|
5261
|
+
applyReuse({runid: d.runid, prefill: d.prefill, lock: true});
|
|
5262
|
+
var sc = (d.prefill || {}).scheduleCard || {};
|
|
5263
|
+
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,
|
|
5264
|
+
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()}};
|
|
5265
|
+
whenHash = null; whenConfirm = null; whenBusy = false;
|
|
5266
|
+
paintWhen();
|
|
5267
|
+
refreshStartEnabled();
|
|
5268
|
+
}
|
|
5269
|
+
document.addEventListener("bareloop-schedule", function(e){ applyScheduleCard(e.detail || {}); });
|
|
5270
|
+
|
|
4917
5271
|
mainBtn.addEventListener("click", function(){
|
|
4918
5272
|
if(mainBtn.disabled) return;
|
|
4919
5273
|
if(mainAction === "start") doStart();
|
|
4920
5274
|
else if(mainAction === "check-deps") doCheckDeps();
|
|
4921
5275
|
else if(mainAction === "send") doSend();
|
|
4922
5276
|
else if(mainAction === "sign-prepare" || mainAction === "sign") doSign();
|
|
5277
|
+
else if(mainAction === "when-sign-prepare" || mainAction === "when-sign") doWhenSign();
|
|
4923
5278
|
});
|
|
4924
5279
|
|
|
4925
5280
|
refreshStartEnabled();
|