bareloop 0.33.0 → 0.35.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 +47 -0
- package/bareloop.context.md +263 -27
- package/package.json +1 -1
- package/src/bundlerun.js +13 -4
- package/src/draftspend.js +82 -0
- package/src/exits.js +10 -1
- package/src/interviewrun.js +10 -9
- package/src/job.js +39 -2
- package/src/judged.js +25 -5
- package/src/kinds.js +2 -0
- package/src/ledger.js +104 -9
- package/src/legs.js +171 -0
- package/src/monthly.js +133 -87
- package/src/panel/authorroutes.js +95 -15
- package/src/panel/authorsession.js +408 -113
- package/src/panel/importroutes.js +359 -0
- package/src/panel/importrun.js +203 -0
- package/src/panel/index.html +1071 -184
- package/src/panel/runroutes.js +219 -0
- package/src/panel/server.js +855 -81
- package/src/panel/status.js +51 -0
- package/src/planrun.js +63 -31
- package/src/providerrows.js +21 -2
- package/src/providers.js +4 -4
- package/src/ralph.js +3 -0
- package/src/replay.js +139 -19
- package/src/replayio.js +11 -4
- package/src/reuse.js +20 -3
- package/src/run.js +6 -3
- package/src/runlist.js +90 -7
- package/src/text.js +20 -0
- package/src/u-readout.js +9 -1
- package/src/u-watchdog.mjs +4 -2
- package/src/userrun.js +166 -65
- package/types/draftspend.d.ts +53 -0
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +5 -2
- package/types/ledger.d.ts +44 -0
- package/types/legs.d.ts +103 -0
- package/types/monthly.d.ts +39 -22
- package/types/panel/authorroutes.d.ts +38 -1
- package/types/panel/authorsession.d.ts +109 -2
- package/types/panel/importroutes.d.ts +129 -0
- package/types/panel/importrun.d.ts +77 -0
- package/types/panel/runroutes.d.ts +32 -0
- package/types/panel/server.d.ts +255 -2
- package/types/panel/status.d.ts +51 -0
- package/types/planrun.d.ts +13 -21
- package/types/providerrows.d.ts +14 -1
- package/types/providers.d.ts +1 -2
- package/types/replay.d.ts +25 -1
- package/types/replayio.d.ts +2 -2
- package/types/reuse.d.ts +5 -0
- package/types/run.d.ts +6 -2
- package/types/runlist.d.ts +66 -2
- package/types/text.d.ts +19 -0
package/src/panel/index.html
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
--magenta:#bb9af7;
|
|
24
24
|
--accent:#7aa2f7;
|
|
25
25
|
--btn-bg:#1f2335; --btn-text:#c0caf5;
|
|
26
|
+
--field-bg:var(--bg); --field-soft-bg:var(--bg); --field-border:var(--text-dim);
|
|
26
27
|
--primary-bg:#7aa2f7; --primary-text:#1a1b26; --primary-border:#7aa2f7;
|
|
27
28
|
font: 14px/1.5 "Courier Prime", "Courier New", Courier, ui-monospace, monospace;
|
|
28
29
|
}
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
--magenta:#9854f1;
|
|
69
70
|
--accent:#2668c2;
|
|
70
71
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
72
|
+
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
71
73
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
--magenta:#9854f1;
|
|
83
85
|
--accent:#2668c2;
|
|
84
86
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
87
|
+
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
85
88
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
86
89
|
}
|
|
87
90
|
.theme-toggle{padding:4px 9px;min-height:32px;border:1px solid var(--border-strong);background:var(--btn-bg);color:var(--text);}
|
|
@@ -171,6 +174,14 @@
|
|
|
171
174
|
glyph and its own color (magenta), never red or amber. */
|
|
172
175
|
.dot.magenta::before{content:"[?]";color:var(--magenta);}
|
|
173
176
|
|
|
177
|
+
.rp-header-title h2{text-transform:none;letter-spacing:0;}
|
|
178
|
+
.rp-status{font-size:13px;color:var(--text-dim);min-width:0;overflow-wrap:anywhere;}
|
|
179
|
+
.rp-sep{color:var(--text-faint);margin-right:6px;}
|
|
180
|
+
.st-word{font-weight:700;color:var(--text);}
|
|
181
|
+
.wf-ended-inline{flex:1 1 auto;white-space:normal;}
|
|
182
|
+
.wf-compact{flex-wrap:wrap;}
|
|
183
|
+
.wf-rowmeta{margin-left:auto;}
|
|
184
|
+
.step-state{display:inline-flex;align-items:center;gap:2px;font-size:12px;}
|
|
174
185
|
.badge{display:inline-block;font-size:11px;padding:1px 4px;color:var(--text-dim);background:none;font-weight:700;}
|
|
175
186
|
.badge::before{content:"[";color:var(--text-faint);}
|
|
176
187
|
.badge::after{content:"]";color:var(--text-faint);}
|
|
@@ -207,6 +218,7 @@
|
|
|
207
218
|
than wrapping onto a 3rd/4th line. */
|
|
208
219
|
.wf-meta-line{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
209
220
|
.wf-meta{font-size:12px;color:var(--text-dim);}
|
|
221
|
+
.wf-ended{display:block;font-size:12px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
210
222
|
.wf-meta + .wf-meta::before{content:" · ";color:var(--text-faint);}
|
|
211
223
|
.wf-row.selected,.hist-row.selected{border:2px solid var(--accent);border-radius:4px;}
|
|
212
224
|
/* item 2 (2026-09-26 merge): a Workflows-view job row expands inline to
|
|
@@ -220,8 +232,6 @@
|
|
|
220
232
|
|
|
221
233
|
.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;}
|
|
222
234
|
.rp-header h2{margin:0;font-size:15px;}
|
|
223
|
-
.rp-header h2::before{content:"┤ ";color:var(--text-faint);}
|
|
224
|
-
.rp-header h2::after{content:" ├";color:var(--text-faint);}
|
|
225
235
|
.rp-header-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
226
236
|
/* item 4: phone-only jump-back link — hidden on desktop (the two panes
|
|
227
237
|
are already both on screen there, nothing to jump back to). */
|
|
@@ -237,6 +247,47 @@
|
|
|
237
247
|
.counters b,.counters span{color:var(--accent);}
|
|
238
248
|
|
|
239
249
|
.summary-box{position:relative;border:1px solid var(--border-strong);padding:16px 12px 12px;margin-top:16px;background:var(--panel2);font-size:12px;}
|
|
250
|
+
.ended-block{position:relative;border:1px solid var(--border-strong);padding:14px 12px 12px;margin-bottom:14px;background:var(--panel2);font-size:13px;}
|
|
251
|
+
.ended-block.magenta{border-color:var(--magenta);}
|
|
252
|
+
.ended-block.red{border-color:var(--red);}
|
|
253
|
+
.ended-block.green{border-color:var(--green);}
|
|
254
|
+
.ended-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;margin-bottom:6px;}
|
|
255
|
+
.ended-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:52px;}
|
|
256
|
+
.ended-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
257
|
+
.ended-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px;}
|
|
258
|
+
/* P5 item 4: Import (read only) — the folder browser, the imported rows, the view */
|
|
259
|
+
.import-box{border:1px dashed var(--border-strong);padding:10px;margin-bottom:10px;display:grid;gap:8px;}
|
|
260
|
+
.import-pathrow{display:flex;gap:8px;flex-wrap:wrap;}
|
|
261
|
+
.import-pathrow input{flex:1 1 180px;min-width:0;min-height:36px;background:var(--bg);color:var(--text);border:1px solid var(--border-strong);padding:4px 8px;font:inherit;}
|
|
262
|
+
.import-entries{display:grid;gap:2px;max-height:240px;overflow-y:auto;overflow-x:hidden;padding:2px;}
|
|
263
|
+
.import-entry{display:flex;gap:8px;align-items:baseline;text-align:left;background:none;border:1px solid transparent;color:var(--text);padding:3px 2px;min-height:32px;cursor:pointer;font:inherit;overflow-wrap:anywhere;}
|
|
264
|
+
.import-entry:hover{background:var(--panel);}
|
|
265
|
+
.import-up{font-weight:700;}
|
|
266
|
+
.import-entry.selected{background:var(--panel);border-color:var(--accent);}
|
|
267
|
+
.import-entry .tag{color:var(--accent);font-size:11px;}
|
|
268
|
+
.import-view[hidden]{display:none;}
|
|
269
|
+
.imp-bad{color:var(--red);font-size:12px;font-weight:700;overflow-wrap:anywhere;}
|
|
270
|
+
.wf-imported{color:var(--accent);}
|
|
271
|
+
.import-view{display:grid;gap:10px;font-size:13px;}
|
|
272
|
+
.import-view .imp-row{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;}
|
|
273
|
+
.import-view .imp-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:84px;}
|
|
274
|
+
.import-view .imp-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
275
|
+
.startfrom-line{margin:2px 0 8px;font-size:12px;color:var(--accent);overflow-wrap:anywhere;}
|
|
276
|
+
.job-card .locked{opacity:.55;cursor:not-allowed;}
|
|
277
|
+
.job-card.reuse .radio-row label{opacity:.55;cursor:not-allowed;}
|
|
278
|
+
.startfrom-origin{font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 4px;padding-right:112px;overflow-wrap:anywhere;}
|
|
279
|
+
.run-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
|
|
280
|
+
.run-actions[hidden]{display:none;}
|
|
281
|
+
.run-actions .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
282
|
+
.resume-err{color:var(--red);white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
283
|
+
/* P5-R: a resumed run is ONE card; its legs are told apart by a divider line, a dotted map connector, a small tag */
|
|
284
|
+
.wf-resumed{color:var(--amber);}
|
|
285
|
+
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
286
|
+
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
287
|
+
/* Resume opens the run's own Job tab: only the two caps turn into inputs, everything else stays read-only */
|
|
288
|
+
.resume-job{border:1px dashed var(--border-strong);margin-top:12px;padding:10px;display:grid;gap:8px;}
|
|
289
|
+
.resume-cap{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
|
|
290
|
+
.resume-cap input{width:7em;max-width:100%;min-height:36px;background:var(--panel);color:var(--text);border:1px solid var(--border-strong);padding:4px 8px;font:inherit;}
|
|
240
291
|
.summary-box.red{border-color:var(--red);}
|
|
241
292
|
.summary-box.amber{border-color:var(--amber);}
|
|
242
293
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
@@ -246,16 +297,20 @@
|
|
|
246
297
|
.sum-label{flex:none;width:66px;color:var(--text-faint);}
|
|
247
298
|
.sum-val{color:var(--text);}
|
|
248
299
|
.summary-box .num{color:var(--accent);font-weight:700;}
|
|
300
|
+
.summary-box.imported-box::before{content:"┤ IMPORTED ├";}
|
|
301
|
+
.imported-box .sum-label{width:104px;}
|
|
302
|
+
.imported-box .summary-row{flex-wrap:wrap;}
|
|
303
|
+
.imported-box .sum-val{flex:1 1 180px;min-width:0;overflow-wrap:anywhere;}
|
|
249
304
|
|
|
250
305
|
.map-box{border:1px solid var(--border-strong);padding:10px;overflow-x:auto;margin-top:16px;margin-bottom:14px;background:var(--panel2);}
|
|
251
306
|
.map-box::before{content:"┤ MAP ├";display:block;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;margin:-2px 0 8px 0;}
|
|
252
307
|
.map-box svg{display:block;max-width:100%;height:auto;}
|
|
253
|
-
.map-
|
|
308
|
+
.map-key{display:flex;gap:14px;font-size:11px;color:var(--text-dim);margin-top:6px;flex-wrap:wrap;}
|
|
254
309
|
|
|
255
310
|
.step-card{position:relative;border:1px solid var(--border);padding:10px;margin-bottom:8px;background:var(--panel2);}
|
|
256
311
|
.step-card.running{border:1px double var(--amber);}
|
|
257
312
|
.step-card .step-head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;}
|
|
258
|
-
.step-card h4{margin:0;font-size:13px;}
|
|
313
|
+
.step-card h4{margin:0;font-size:13px;text-transform:none;letter-spacing:0;}
|
|
259
314
|
.step-meta{font-size:12px;color:var(--text-dim);margin-top:4px;}
|
|
260
315
|
/* build item C: Audit tab attempt/rounds detail (reused by audit-part-toggle expansion) */
|
|
261
316
|
.attempts-list{margin-top:8px;border-top:1px dotted var(--border-strong);padding-top:8px;}
|
|
@@ -307,6 +362,7 @@
|
|
|
307
362
|
|
|
308
363
|
.job-card{position:relative;border:1px solid var(--border-strong);padding:14px 10px 10px;background:var(--panel2);margin-top:10px;margin-bottom:10px;}
|
|
309
364
|
.job-card h4{margin:0 0 8px;font-size:13px;position:absolute;top:-10px;left:10px;background:var(--panel2);padding:0 6px;}
|
|
365
|
+
.job-card .card-clear{position:absolute;top:-13px;right:10px;padding:1px 8px;font-size:11px;line-height:1.4;background:var(--panel2);}
|
|
310
366
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
311
367
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
312
368
|
.field{margin-bottom:10px;}
|
|
@@ -326,8 +382,6 @@
|
|
|
326
382
|
.job-card.compact label{margin-bottom:1px;font-size:11px;}
|
|
327
383
|
.job-card.compact input,
|
|
328
384
|
.job-card.compact select{padding:5px 7px;}
|
|
329
|
-
.expand-field{text-overflow:ellipsis;transition:height 0.12s ease;}
|
|
330
|
-
.expand-field:focus{height:52px;white-space:normal;}
|
|
331
385
|
.radio-row{display:flex;gap:14px;align-items:center;margin-bottom:10px;}
|
|
332
386
|
.radio-row label{display:flex;align-items:center;gap:5px;font-size:13px;color:var(--text);margin:0;text-transform:none;}
|
|
333
387
|
.cap-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:6px;margin-bottom:6px;}
|
|
@@ -343,15 +397,23 @@
|
|
|
343
397
|
.btn.primary:disabled{background:var(--btn-bg);color:var(--text-faint);border-color:var(--border);}
|
|
344
398
|
.btn.primary:disabled::before,.btn.primary:disabled::after{color:var(--text-faint);opacity:0.4;}
|
|
345
399
|
|
|
346
|
-
/*
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.chat-
|
|
351
|
-
.chat-
|
|
352
|
-
.chat-
|
|
353
|
-
.chat-
|
|
354
|
-
.chat-
|
|
400
|
+
/* the ONE progress list: a step per line — label, then dots while running / check / cross, then its reason */
|
|
401
|
+
.chat-steps{list-style:none;margin:2px 0 10px;padding:0;font-size:12px;}
|
|
402
|
+
.chat-steps .step{padding:1px 0;}
|
|
403
|
+
.chat-steps .step-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 10px;}
|
|
404
|
+
.chat-steps .step-label{color:var(--text);min-width:11em;}
|
|
405
|
+
.chat-steps .step-sign{font-weight:700;}
|
|
406
|
+
.chat-steps .step.run .step-sign{color:var(--amber);}
|
|
407
|
+
.chat-steps .step.ok .step-sign{color:var(--green);}
|
|
408
|
+
.chat-steps .step.bad .step-sign{color:var(--red);}
|
|
409
|
+
.chat-steps .step.wait .step-label{color:var(--amber);}
|
|
410
|
+
.chat-steps .step-detail{display:block;flex-basis:100%;min-width:0;padding-left:1.2em;color:var(--text-faint);word-break:break-word;white-space:pre-wrap;}
|
|
411
|
+
.chat-steps .step.bad .step-detail{color:var(--red);}
|
|
412
|
+
.chat-steps .step.run .step-sign::after{content:"\2026";}
|
|
413
|
+
@keyframes step-dots{0%{content:"";}25%{content:".";}50%{content:"..";}75%{content:"...";}}
|
|
414
|
+
@media (prefers-reduced-motion: no-preference){
|
|
415
|
+
.chat-steps .step.run .step-sign::after{content:"";animation:step-dots 1.2s steps(1,end) infinite;}
|
|
416
|
+
}
|
|
355
417
|
/* build item 7 — the two $0 readiness lines under the Model field. */
|
|
356
418
|
.jf-readiness{font-size:11px;margin-top:2px;}
|
|
357
419
|
.jf-readiness.ok{color:var(--green);}
|
|
@@ -368,12 +430,24 @@
|
|
|
368
430
|
.msg .who{font-size:11px;color:var(--text-faint);margin-bottom:3px;text-transform:none;font-weight:400;}
|
|
369
431
|
.chat-input-row{margin-top:10px;border-top:1px dotted var(--border-strong);padding-top:10px;}
|
|
370
432
|
.chat-input-row input{width:100%;box-sizing:border-box;}
|
|
433
|
+
/* the ask box wraps long text (textarea, hamr 2026-10-05): grows a few rows (fitBox), then scrolls inside; Enter sends, Shift+Enter is a newline */
|
|
434
|
+
textarea[data-testid="chat-msg"]{width:100%;box-sizing:border-box;font:inherit;color:var(--text);resize:vertical;overflow-y:auto;max-height:200px;}
|
|
435
|
+
/* hamr 2026-10-05: the card's free-text boxes wrap like the ask box (one shared fitBox); a few rows, then they scroll inside */
|
|
436
|
+
/* same look as the card's one-line inputs (white when editable, ordinary 1px border) — a textarea has its own browser border otherwise */
|
|
437
|
+
.job-card textarea.jf-wrap{width:100%;box-sizing:border-box;font:inherit;padding:6px 8px;border:1px solid var(--border-strong);border-radius:0;background:var(--bg);color:var(--text);resize:vertical;overflow-y:auto;max-height:200px;}
|
|
438
|
+
/* hamr 2026-10-05: the focused/selected Chat-card field gets a blue border (the accent), no outline ring */
|
|
439
|
+
#panel-chat :is(input,select,textarea):focus{border-color:var(--accent);outline:none;}
|
|
440
|
+
/* hamr 2026-10-05: the ask box is dimmed until something asks for a reply (placeholder stays readable) */
|
|
441
|
+
#chat-msg:disabled{background:var(--bg);opacity:.55;cursor:not-allowed;}
|
|
371
442
|
/* chat message box reads as an unmistakable text box: ~2x the default
|
|
372
443
|
input height, thicker 2px border (ported verbatim from the mockup). */
|
|
373
|
-
#chat-msg{min-height:64px;padding:8px 12px;border:2px solid var(--border-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
444
|
+
#chat-msg{min-height:64px;padding:8px 12px;border:2px solid var(--field-border);background:var(--field-bg);}
|
|
445
|
+
/* hamr 2026-10-05: editable Chat-card fields get the standard soft white (a step below the ask box); disabled/readonly (dimmed) keep the base colour */
|
|
446
|
+
#panel-chat input:not(:disabled):not([readonly]):not(#chat-msg):not([type="radio"]):not([type="checkbox"]),
|
|
447
|
+
#panel-chat select:not(:disabled),
|
|
448
|
+
#panel-chat textarea:not(:disabled):not([readonly]):not(#chat-msg){background:var(--field-soft-bg);}
|
|
449
|
+
/* ONE wide button below the message box (hamr 2026-10-05); its wording comes from mainButtonFor */
|
|
450
|
+
.btn.wide{width:100%;margin-top:10px;white-space:normal;}
|
|
377
451
|
/* Settings view (P4a) — same tokens as the rest of the page; the mockup's settings styles */
|
|
378
452
|
.settings-view{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;}
|
|
379
453
|
.settings-header{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:2px solid var(--border-strong);background:var(--panel);}
|
|
@@ -441,10 +515,14 @@
|
|
|
441
515
|
<div class="pane-body" id="left-pane-body">
|
|
442
516
|
|
|
443
517
|
<section id="panel-chat" class="tabpanel active" role="tabpanel" aria-labelledby="tab-chat">
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
<div class="job-card compact" id="job-card" data-testid="job-card"
|
|
518
|
+
<!-- hamr 2026-10-05: the empty card is ALWAYS there when Chat opens (no + New). The top-right button reads
|
|
519
|
+
"Abandon" while a session is live and unsigned, else "Clear"; both leave an empty card. -->
|
|
520
|
+
<div class="job-card compact" id="job-card" data-testid="job-card">
|
|
521
|
+
<div class="startfrom-origin" id="chat-startfrom-origin" data-testid="startfrom-origin" hidden></div>
|
|
447
522
|
<h4 id="job-card-title">Job card — draft</h4>
|
|
523
|
+
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
524
|
+
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
525
|
+
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
448
526
|
|
|
449
527
|
<div class="field">
|
|
450
528
|
<label id="jf-verdict-label">Check type</label>
|
|
@@ -466,27 +544,27 @@
|
|
|
466
544
|
</div>
|
|
467
545
|
<div class="field">
|
|
468
546
|
<label for="jf-goal">Goal</label>
|
|
469
|
-
<
|
|
547
|
+
<textarea class="jf-wrap" id="jf-goal" rows="2"></textarea>
|
|
470
548
|
</div>
|
|
471
549
|
<div class="field">
|
|
472
550
|
<label for="jf-source">Source</label>
|
|
473
|
-
<
|
|
551
|
+
<textarea class="jf-wrap" id="jf-source" rows="1" placeholder="absolute path to a local repo"></textarea>
|
|
474
552
|
</div>
|
|
475
553
|
<div class="field">
|
|
476
554
|
<label for="jf-dest">Destination</label>
|
|
477
|
-
<
|
|
555
|
+
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
478
556
|
</div>
|
|
479
557
|
<div class="field">
|
|
480
558
|
<label for="jf-success">Success</label>
|
|
481
|
-
<
|
|
559
|
+
<textarea class="jf-wrap" id="jf-success" rows="2"></textarea>
|
|
482
560
|
</div>
|
|
483
561
|
<div class="field">
|
|
484
562
|
<label for="jf-guardrails">Guardrails</label>
|
|
485
|
-
<
|
|
563
|
+
<textarea class="jf-wrap" id="jf-guardrails" rows="2"></textarea>
|
|
486
564
|
</div>
|
|
487
565
|
<div class="field">
|
|
488
566
|
<label for="jf-judge">Judge examples <span class="hint" style="display:inline">(rubric only)</span></label>
|
|
489
|
-
<
|
|
567
|
+
<textarea class="jf-wrap" id="jf-judge" rows="2" disabled></textarea>
|
|
490
568
|
</div>
|
|
491
569
|
|
|
492
570
|
<div class="cap-row" data-testid="cap-row">
|
|
@@ -500,21 +578,15 @@
|
|
|
500
578
|
<input id="jf-cap-time" type="text" placeholder="or blank = no wall">
|
|
501
579
|
</div>
|
|
502
580
|
</div>
|
|
503
|
-
<button class="btn primary" type="button" id="chat-start-btn" data-testid="chat-start" disabled>Start drafting</button>
|
|
504
581
|
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
505
|
-
<div class="hint" id="chat-hash-line" data-testid="chat-hash-line" hidden></div>
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
<!-- build item 3 — the label comes FIRST, then the animated
|
|
509
|
-
indicator, so the cycling dots ([progress.] .. [progress....])
|
|
510
|
-
never push the label around. -->
|
|
511
|
-
<div class="chat-progress-row" id="chat-progress-row" data-testid="chat-progress-row" hidden>
|
|
512
|
-
<span class="chat-progress-label" id="chat-progress-label" data-testid="chat-progress-label"></span>
|
|
513
|
-
<span class="chat-progress-glyph" id="chat-progress-glyph" data-testid="chat-progress-glyph"></span>
|
|
514
582
|
</div>
|
|
515
583
|
|
|
516
584
|
<div class="chat-thread" id="chat-thread" data-testid="chat-thread"></div>
|
|
517
585
|
|
|
586
|
+
<!-- ONE progress list (hamr 2026-10-04): every pipeline step is one line that updates in place, running with
|
|
587
|
+
animated dots, then a check or a cross with its code-owned reason; chat turns stay in the thread above it, right over the ask box. -->
|
|
588
|
+
<ol class="chat-steps" id="chat-progress-row" data-testid="chat-steps" aria-live="polite" hidden></ol>
|
|
589
|
+
|
|
518
590
|
<!-- build item 1 — a failed/rejected send, revise, sign-prepare,
|
|
519
591
|
sign or check-deps POST shows here and STAYS shown: it is set
|
|
520
592
|
only by this tab's own action handlers below and is never
|
|
@@ -525,15 +597,10 @@
|
|
|
525
597
|
|
|
526
598
|
<div class="chat-input-row">
|
|
527
599
|
<label for="chat-msg" style="display:none">Message</label>
|
|
528
|
-
<
|
|
529
|
-
</div>
|
|
530
|
-
<button class="btn" type="button" id="chat-check-deps-btn" data-testid="chat-check-deps" hidden>Check again</button>
|
|
531
|
-
|
|
532
|
-
<div class="actions-row">
|
|
533
|
-
<button class="btn" type="button" id="chat-send-btn" data-testid="chat-send" disabled>Send</button>
|
|
534
|
-
<button class="btn primary" type="button" id="chat-sign-btn" data-testid="btn-sign-run" disabled>Sign & run</button>
|
|
535
|
-
<button class="btn" type="button" id="chat-revise-btn" data-testid="btn-revise" disabled>Revise (2 left)</button>
|
|
600
|
+
<textarea id="chat-msg" rows="2" placeholder="Ask for a change, or reply to the plan…" data-testid="chat-msg"></textarea>
|
|
536
601
|
</div>
|
|
602
|
+
<button class="btn primary wide" type="button" id="chat-main-btn" data-testid="chat-main" disabled>Start drafting</button>
|
|
603
|
+
<div class="hint" id="chat-main-hint" data-testid="chat-main-hint"></div>
|
|
537
604
|
<div class="hint">Only your click signs. The chat can't.</div>
|
|
538
605
|
</section>
|
|
539
606
|
|
|
@@ -541,9 +608,25 @@
|
|
|
541
608
|
<div class="chips" role="group" aria-label="Runs view" style="margin-bottom:8px;">
|
|
542
609
|
<button class="chip" type="button" aria-pressed="true" data-runs-view="workflows" data-testid="runs-view-workflows">Workflows</button>
|
|
543
610
|
<button class="chip" type="button" aria-pressed="false" data-runs-view="history" data-testid="runs-view-history">History</button>
|
|
611
|
+
<button class="btn small" type="button" id="wf-import" data-testid="wf-import" title="look at an exported job folder (read only)">Import</button>
|
|
612
|
+
</div>
|
|
613
|
+
<div class="import-box" id="import-box" data-testid="import-box" hidden>
|
|
614
|
+
<label for="import-path">Import a job folder</label>
|
|
615
|
+
<div class="import-pathrow">
|
|
616
|
+
<input id="import-path" data-testid="import-path" type="text" placeholder="/path/to/job.bareloop — or pick below" autocomplete="off">
|
|
617
|
+
<button class="btn small" type="button" id="import-open" data-testid="import-open">Go</button>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="resume-err" id="import-err" data-testid="import-err" hidden></div>
|
|
620
|
+
<div class="hint" id="import-resolved" data-testid="import-resolved"></div>
|
|
621
|
+
<div class="import-entries" id="import-entries" data-testid="import-entries"></div>
|
|
622
|
+
<div class="ended-actions">
|
|
623
|
+
<button class="btn primary" type="button" id="import-go" data-testid="import-go">Import</button>
|
|
624
|
+
<button class="btn" type="button" id="import-cancel" data-testid="import-cancel">Cancel</button>
|
|
625
|
+
</div>
|
|
544
626
|
</div>
|
|
545
627
|
<div id="runs-filterbar-anchor"></div>
|
|
546
628
|
<div id="wf-list" data-testid="wf-list"></div>
|
|
629
|
+
<div id="import-list" data-testid="import-list"></div>
|
|
547
630
|
<div id="history-list" data-testid="history-list" hidden></div>
|
|
548
631
|
</section>
|
|
549
632
|
|
|
@@ -556,8 +639,7 @@
|
|
|
556
639
|
<div class="rp-header-title">
|
|
557
640
|
<span class="dot grey" id="active-wf-dot"></span>
|
|
558
641
|
<h2 id="active-wf-name">(none selected)</h2>
|
|
559
|
-
<span class="
|
|
560
|
-
<span class="badge" id="active-wf-date">—</span>
|
|
642
|
+
<span class="rp-status" id="active-wf-verdict" data-testid="active-wf-verdict"></span>
|
|
561
643
|
</div>
|
|
562
644
|
</div>
|
|
563
645
|
|
|
@@ -570,8 +652,10 @@
|
|
|
570
652
|
<div class="pane-body" id="right-pane-body">
|
|
571
653
|
|
|
572
654
|
<section id="panel-run" class="tabpanel active" role="tabpanel" aria-labelledby="tab-run">
|
|
655
|
+
<div class="import-view" id="import-view" data-testid="import-view" hidden></div>
|
|
573
656
|
<div id="run-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run from Workflows or History</div>
|
|
574
657
|
<div id="run-content" hidden>
|
|
658
|
+
<div class="run-actions" id="run-actions" data-testid="run-actions" hidden></div>
|
|
575
659
|
<div class="counters" id="run-counters" data-testid="live-counters"></div>
|
|
576
660
|
<div class="summary-box" id="run-summary" data-testid="run-summary"></div>
|
|
577
661
|
<div class="map-box" data-testid="step-map" id="map-box"><div id="map-mount"></div></div>
|
|
@@ -582,6 +666,7 @@
|
|
|
582
666
|
<section id="panel-audit" class="tabpanel" role="tabpanel" aria-labelledby="tab-audit" hidden>
|
|
583
667
|
<div id="audit-select-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
584
668
|
<div id="audit-body" hidden>
|
|
669
|
+
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
585
670
|
<div class="audit-toolbar" style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
|
586
671
|
<div class="chips" role="group" aria-label="Filter audit rows" style="margin-bottom:0;">
|
|
587
672
|
<button class="chip" aria-pressed="true" data-filter="all" data-testid="chip-all">All</button>
|
|
@@ -619,8 +704,8 @@
|
|
|
619
704
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
620
705
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
621
706
|
<div class="field-row">
|
|
622
|
-
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
623
|
-
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
707
|
+
<div class="field"><label id="details-cap-money-label">$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
708
|
+
<div class="field"><label id="details-cap-time-label">Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
624
709
|
</div>
|
|
625
710
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
626
711
|
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
@@ -630,6 +715,7 @@
|
|
|
630
715
|
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
631
716
|
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
632
717
|
<div class="hint" id="job-note"></div>
|
|
718
|
+
<div id="resume-job" class="resume-job" data-testid="resume-job" hidden></div>
|
|
633
719
|
</div>
|
|
634
720
|
</section>
|
|
635
721
|
|
|
@@ -656,7 +742,7 @@
|
|
|
656
742
|
<section id="panel-providers" class="tabpanel active" role="tabpanel" aria-labelledby="tab-providers">
|
|
657
743
|
<div class="keyfile-strip" data-testid="keyfile-box">
|
|
658
744
|
<div class="keyfile-strip-main">
|
|
659
|
-
<span>Put <code>NAME=key</code> lines in <code id="pv-keyfile-path"
|
|
745
|
+
<span>Put <code>NAME=key</code> lines in <code id="pv-keyfile-path" data-testid="keyfile-path">the keys file (path loading…)</code>, then Reload keys — every key that has a value gets a row. Only names are listed; keys never reach this page.</span>
|
|
660
746
|
<span class="hint" id="pv-keyfile-note" data-testid="keyfile-warning"></span>
|
|
661
747
|
</div>
|
|
662
748
|
<div class="keyfile-strip-actions">
|
|
@@ -720,15 +806,28 @@
|
|
|
720
806
|
var TOKEN = "__BARELOOP_PANEL_TOKEN__";
|
|
721
807
|
document.getElementById("host-line").textContent = "127.0.0.1:" + PORT;
|
|
722
808
|
|
|
809
|
+
// After a panel restart this page's token is dead: the server refuses EVERY call with 403 and the exact reason
|
|
810
|
+
// below (checkHumanGuard, src/panel/authorroutes.js). The ONE place the page makes calls swaps that raw text for
|
|
811
|
+
// one clear line and tells the Chat poll to stop (staleTokenHook, set by the Chat code). No auto-reload.
|
|
812
|
+
var STALE_TOKEN_REFUSAL = "refused \u2014 missing or wrong token";
|
|
813
|
+
var STALE_TOKEN_LINE = "The panel was restarted \u2014 reload this page. A drafting session that was open is gone (nothing more is spent).";
|
|
814
|
+
var staleTokenHook = function(){};
|
|
815
|
+
function staleTokenCheck(j, status){
|
|
816
|
+
if(j && j.ok === false && j.error === STALE_TOKEN_REFUSAL && (status === undefined || status === 403)){
|
|
817
|
+
j.error = STALE_TOKEN_LINE;
|
|
818
|
+
staleTokenHook();
|
|
819
|
+
}
|
|
820
|
+
return j;
|
|
821
|
+
}
|
|
723
822
|
function authorPost(path, body){
|
|
724
823
|
return fetch(path, {
|
|
725
824
|
method: "POST",
|
|
726
825
|
headers: {"content-type": "application/json", "x-bareloop-token": TOKEN},
|
|
727
826
|
body: JSON.stringify(body || {}),
|
|
728
|
-
}).then(function(r){ return r.json().then(function(j){ return {status: r.status, body: j}; }); });
|
|
827
|
+
}).then(function(r){ return r.json().then(function(j){ return {status: r.status, body: staleTokenCheck(j, r.status)}; }); });
|
|
729
828
|
}
|
|
730
829
|
function authorGet(path){
|
|
731
|
-
return fetch(path, {headers: {"x-bareloop-token": TOKEN}}).then(function(r){ return r.json(); });
|
|
830
|
+
return fetch(path, {headers: {"x-bareloop-token": TOKEN}}).then(function(r){ return r.json().then(function(j){ return staleTokenCheck(j, r.status); }); });
|
|
732
831
|
}
|
|
733
832
|
|
|
734
833
|
function wireTabs(tabButtons){
|
|
@@ -832,6 +931,8 @@
|
|
|
832
931
|
var n = 0;
|
|
833
932
|
return steps.map(function(s){
|
|
834
933
|
if(s.noNumber) return -1;
|
|
934
|
+
// P5-R: a step continuing from the leg before is the SAME step — it keeps its number
|
|
935
|
+
if(s.continued && n > 0) return n - 1;
|
|
835
936
|
n++;
|
|
836
937
|
return n - 1;
|
|
837
938
|
});
|
|
@@ -847,6 +948,8 @@
|
|
|
847
948
|
function partBoxState(part, isLastLivePart){
|
|
848
949
|
if(part.outcome === "green" || part.outcome === "satisfied" || part.outcome === "already-green") return "done";
|
|
849
950
|
if(typeof part.outcome === "string") return "stopped";
|
|
951
|
+
// P5-R: its leg's halt cut this part off — the map's existing "stopped" state, never "done"
|
|
952
|
+
if(part.stopReason) return "stopped";
|
|
850
953
|
if(isLastLivePart) return "running";
|
|
851
954
|
return "done"; // scout/plan/replan/judge: no per-part verdict, but the part itself already happened
|
|
852
955
|
}
|
|
@@ -875,7 +978,9 @@
|
|
|
875
978
|
var list = Array.isArray(parts) ? parts : [];
|
|
876
979
|
return list.map(function(part, idx){
|
|
877
980
|
var isLastLivePart = !!isLive && idx === list.length - 1;
|
|
878
|
-
|
|
981
|
+
// "(try N)" is for real in-leg retries only; a step re-entered across a leg boundary is the same step continuing
|
|
982
|
+
var tryN = typeof part.tryNumber === "number" ? part.tryNumber : part.occurrence;
|
|
983
|
+
var occSuffix = part.kind === "step" && part.continued ? " (continued)" : (part.kind === "step" && tryN > 1 ? " (try " + tryN + ")" : "");
|
|
879
984
|
return {
|
|
880
985
|
kind: part.kind,
|
|
881
986
|
partIndex: idx,
|
|
@@ -888,6 +993,9 @@
|
|
|
888
993
|
// Audit tab's expand-to-rounds, read off `box.part.attempts` directly).
|
|
889
994
|
attempts: partHasNoVerdict(part) ? [] : (Array.isArray(part.attempts) ? part.attempts : []),
|
|
890
995
|
blocked: typeof part.blocked === "number" ? part.blocked : 0,
|
|
996
|
+
// P5-R: the leg that ran this part ENDED here and the next part is where the next leg picked up
|
|
997
|
+
resumedNext: part.resumedNext === true,
|
|
998
|
+
continued: part.continued === true,
|
|
891
999
|
part: part,
|
|
892
1000
|
};
|
|
893
1001
|
});
|
|
@@ -1031,34 +1139,35 @@
|
|
|
1031
1139
|
'" text-anchor="middle" font-size="10" fill="var(--text-faint)">try ' + retryTry + '</text>';
|
|
1032
1140
|
}
|
|
1033
1141
|
if(idx < n - 1){
|
|
1142
|
+
// P5-R: where a leg ended and the next leg picked up, the connector is DOTTED (and says so) —
|
|
1143
|
+
// one run, one map; the dotted line is the only mark of the resume.
|
|
1144
|
+
var dash = s.resumedNext ? ' stroke-dasharray="2,4" data-resumed="1"' : '';
|
|
1034
1145
|
if(nextRow === row){
|
|
1035
1146
|
var midY = y + boxH / 2;
|
|
1036
1147
|
if(!rtl){
|
|
1037
1148
|
var nx = x + boxW + gap;
|
|
1038
1149
|
body += '<line x1="' + (x + boxW) + '" y1="' + midY + '" x2="' + (nx - 2) + '" y2="' + midY +
|
|
1039
|
-
'" stroke="var(--text-dim)" stroke-width="2" marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1150
|
+
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1040
1151
|
} else {
|
|
1041
1152
|
var px = x - gap - boxW;
|
|
1042
1153
|
body += '<line x1="' + x + '" y1="' + midY + '" x2="' + (px + boxW + 2) + '" y2="' + midY +
|
|
1043
|
-
'" stroke="var(--text-dim)" stroke-width="2" marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1154
|
+
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1044
1155
|
}
|
|
1045
1156
|
} else {
|
|
1046
1157
|
var cx = x + boxW / 2;
|
|
1047
1158
|
body += '<line x1="' + cx + '" y1="' + (y + boxH) + '" x2="' + cx + '" y2="' + (y + rowH - 2) +
|
|
1048
|
-
'" stroke="var(--text-dim)" stroke-width="2" marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1159
|
+
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1049
1160
|
}
|
|
1050
1161
|
}
|
|
1051
1162
|
});
|
|
1052
1163
|
return '<svg width="' + totalW + '" height="' + totalH + '" viewBox="0 0 ' + totalW + ' ' + totalH + '" role="img" aria-label="step map">' + body + '</svg>';
|
|
1053
1164
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
'<span><span class="dot red"></span>stopped</span>' +
|
|
1059
|
-
'<span><span class="dot grey"></span>waiting</span>' +
|
|
1060
|
-
'<span><span class="dot magenta"></span>died</span>' +
|
|
1165
|
+
// the line-style key under the MAP (hamr 2026-10-04): only the two line styles, never a sign legend
|
|
1166
|
+
function stepMapKeyHTML(steps){
|
|
1167
|
+
var resumed = (steps || []).some(function(b){ return b.resumedNext; });
|
|
1168
|
+
return '<div class="map-key" data-testid="map-key">' +
|
|
1061
1169
|
'<span>⤾ dashed = retry</span>' +
|
|
1170
|
+
(resumed ? '<span>··· dotted = resumed</span>' : '') +
|
|
1062
1171
|
'</div>';
|
|
1063
1172
|
}
|
|
1064
1173
|
var lastSteps = null;
|
|
@@ -1081,7 +1190,7 @@
|
|
|
1081
1190
|
wrapper.hidden = false;
|
|
1082
1191
|
var w = mapAvailWidth(wrapper);
|
|
1083
1192
|
lastWidth = w;
|
|
1084
|
-
mount.innerHTML = buildStepMapSVG(steps, w) +
|
|
1193
|
+
mount.innerHTML = buildStepMapSVG(steps, w) + stepMapKeyHTML(steps);
|
|
1085
1194
|
// build item B: a map box click jumps to the Audit tab, that same part
|
|
1086
1195
|
// expanded and scrolled into view — same click-through as the card list,
|
|
1087
1196
|
// sharing partIndex so the two can never disagree about which row a box
|
|
@@ -1101,7 +1210,7 @@
|
|
|
1101
1210
|
var w = mapAvailWidth(wrapper);
|
|
1102
1211
|
if(w === null || w === lastWidth) return;
|
|
1103
1212
|
lastWidth = w;
|
|
1104
|
-
document.getElementById("map-mount").innerHTML = buildStepMapSVG(lastSteps, w) +
|
|
1213
|
+
document.getElementById("map-mount").innerHTML = buildStepMapSVG(lastSteps, w) + stepMapKeyHTML(lastSteps);
|
|
1105
1214
|
}
|
|
1106
1215
|
if(typeof ResizeObserver !== "undefined"){
|
|
1107
1216
|
var ro = new ResizeObserver(function(){ reflowMap(); });
|
|
@@ -1126,11 +1235,51 @@
|
|
|
1126
1235
|
if(g === "?") return "magenta";
|
|
1127
1236
|
return "grey";
|
|
1128
1237
|
}
|
|
1238
|
+
// ONE spelling of "**word** — reason": the sign and the word come from the server's status table
|
|
1239
|
+
// (src/panel/status.js) — the page never maps an outcome to a word itself. No status = nothing to say.
|
|
1240
|
+
function statusWordHtml(status, reason){
|
|
1241
|
+
if(!status || !status.word) return "";
|
|
1242
|
+
return '<b class="st-word">' + escapeXml(status.word) + '</b>' + (reason ? " \u2014 " + escapeXml(reason) : "");
|
|
1243
|
+
}
|
|
1244
|
+
// local date + time as the header shows it: "10/4/2026, 6:01 PM" ("unknown" when the spine carried no time)
|
|
1245
|
+
function fmtLocalDateTime(iso){
|
|
1246
|
+
var d = iso ? new Date(iso) : null;
|
|
1247
|
+
if(!d || isNaN(d.getTime())) return "unknown";
|
|
1248
|
+
return d.toLocaleString("en-US", {year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "2-digit"});
|
|
1249
|
+
}
|
|
1250
|
+
// the right-side header: `[sign] job (runid) │ **word** — reason · ended 10/4/2026, 6:01 PM` (no capitals: the job
|
|
1251
|
+
// name is shown as written). `bodyHtml` is already escaped.
|
|
1252
|
+
function setRunHeader(sign, nameText, bodyHtml){
|
|
1253
|
+
document.getElementById("active-wf-name").textContent = nameText;
|
|
1254
|
+
document.getElementById("active-wf-dot").className = "dot " + glyphClass(sign);
|
|
1255
|
+
var st = document.getElementById("active-wf-verdict");
|
|
1256
|
+
st.innerHTML = bodyHtml ? '<span class="rp-sep">\u2502</span>' + bodyHtml : "";
|
|
1257
|
+
st.removeAttribute("title");
|
|
1258
|
+
}
|
|
1259
|
+
// "step 2 of 3" for a live run: the step it is on (first one not yet done) of the real steps
|
|
1260
|
+
function liveStepText(detail){
|
|
1261
|
+
var rs = realSteps(detail.steps || []);
|
|
1262
|
+
if(rs.length === 0) return "";
|
|
1263
|
+
var done = rs.filter(function(s){ return s.state === "done"; }).length;
|
|
1264
|
+
return "step " + Math.min(done + 1, rs.length) + " of " + rs.length;
|
|
1265
|
+
}
|
|
1266
|
+
function runHeaderBody(detail){
|
|
1267
|
+
var live = detail.status && detail.status.word === "running";
|
|
1268
|
+
var reason = detail.ended ? detail.ended.line : (live ? liveStepText(detail) : "");
|
|
1269
|
+
var when = detail.endedAt ? ("ended " + fmtLocalDateTime(detail.endedAt)) : ("started " + fmtLocalDateTime(detail.startedAt || detail.at));
|
|
1270
|
+
return statusWordHtml(detail.status, reason) + " \u00B7 " + escapeXml(when);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// P5-R: a resumed run is the SAME run — one card, never two. This is only a small tag on it.
|
|
1274
|
+
function resumedTagHtml(count, testid){
|
|
1275
|
+
return (typeof count === "number" && count > 0)
|
|
1276
|
+
? '<span class="wf-meta wf-resumed" data-testid="' + escapeXml(testid) + '">resumed ×' + count + '</span>' : '';
|
|
1277
|
+
}
|
|
1129
1278
|
|
|
1130
1279
|
// ---- Runs tab: ONE run-row builder shared by History (flat) and the
|
|
1131
1280
|
// Workflows view's per-job expansion (item 2, 2026-09-26 merge) — the two
|
|
1132
1281
|
// never drift on how a run row reads.
|
|
1133
|
-
function buildRunRowEl(r){
|
|
1282
|
+
function buildRunRowEl(r, compact){
|
|
1134
1283
|
var row = document.createElement("div");
|
|
1135
1284
|
// build item (2026-09-26 fix): compute selected state here, off the
|
|
1136
1285
|
// module's own `currentRunid`, so a row freshly built by a RE-RENDER
|
|
@@ -1146,7 +1295,15 @@
|
|
|
1146
1295
|
row.setAttribute("role", "button");
|
|
1147
1296
|
row.setAttribute("aria-pressed", selected ? "true" : "false");
|
|
1148
1297
|
var histFullName = r.job + ' (' + r.runid + ')';
|
|
1149
|
-
if(r.
|
|
1298
|
+
if(r.starting){
|
|
1299
|
+
row.innerHTML =
|
|
1300
|
+
'<span class="wf-line1"><span class="dot amber"></span>' +
|
|
1301
|
+
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
1302
|
+
'<span class="wf-meta-line">' +
|
|
1303
|
+
'<span class="wf-meta" data-testid="hist-starting-' + escapeXml(r.runid) + '">starting…</span>' +
|
|
1304
|
+
'<span class="wf-meta">' + escapeXml((r.at || "").slice(0,10) || "unknown") + '</span>' +
|
|
1305
|
+
'</span>';
|
|
1306
|
+
} else if(r.fileMissing){
|
|
1150
1307
|
row.innerHTML =
|
|
1151
1308
|
'<span class="wf-line1"><span class="dot grey"></span>' +
|
|
1152
1309
|
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
@@ -1154,15 +1311,25 @@
|
|
|
1154
1311
|
'<span class="wf-meta">file missing</span>' +
|
|
1155
1312
|
'<span class="wf-meta">' + escapeXml((r.at || "").slice(0,10) || "unknown") + '</span>' +
|
|
1156
1313
|
'</span>';
|
|
1314
|
+
} else if(compact){
|
|
1315
|
+
// an EXPANDED per-run row under a workflow: `[sign] **word** — reason runid · $ · date`
|
|
1316
|
+
row.innerHTML =
|
|
1317
|
+
'<span class="wf-line1 wf-compact"><span class="dot ' + glyphClass(r.glyph) + '"></span>' +
|
|
1318
|
+
'<span class="wf-ended wf-ended-inline" title="' + escapeXml(histFullName) + '" data-testid="hist-ended-' + escapeXml(r.runid) + '">' + statusWordHtml(r.status, r.endedLine) + '</span>' +
|
|
1319
|
+
'<span class="wf-meta wf-rowmeta">' + escapeXml(r.runid) + ' \u00B7 ' + escapeXml(rowSpendText(r)) + ' \u00B7 ' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1320
|
+
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) + '</span>';
|
|
1157
1321
|
} else {
|
|
1322
|
+
// History card: line 1 sign + job (runid); line 2 **word** — reason; line 3 the facts
|
|
1158
1323
|
row.innerHTML =
|
|
1159
1324
|
'<span class="wf-line1"><span class="dot ' + glyphClass(r.glyph) + '"></span>' +
|
|
1160
1325
|
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
1326
|
+
(r.status ? '<span class="wf-ended" data-testid="hist-ended-' + escapeXml(r.runid) + '">' + statusWordHtml(r.status, r.endedLine) + '</span>' : '') +
|
|
1161
1327
|
'<span class="wf-meta-line">' +
|
|
1162
1328
|
'<span class="wf-meta">' + escapeXml(r.checkType) + '</span>' +
|
|
1163
1329
|
'<span class="wf-meta">' + escapeXml(rowSpendText(r)) + '</span>' +
|
|
1164
|
-
'<span class="wf-meta">' + escapeXml(r
|
|
1330
|
+
'<span class="wf-meta">' + escapeXml(rowWallText(r)) + '</span>' +
|
|
1165
1331
|
'<span class="wf-meta">' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1332
|
+
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) +
|
|
1166
1333
|
'</span>';
|
|
1167
1334
|
}
|
|
1168
1335
|
row.addEventListener("click", function(){ selectRun(r.runid, row, ".hist-row"); document.getElementById("tab-run").click(); scrollRunIntoViewMobile(); });
|
|
@@ -1205,10 +1372,13 @@
|
|
|
1205
1372
|
lastRunid: last.runid,
|
|
1206
1373
|
lastAt: last.at,
|
|
1207
1374
|
lastGlyph: last.glyph || "▶",
|
|
1375
|
+
lastStatus: last.status || null,
|
|
1376
|
+
lastEndedLine: last.endedLine || null,
|
|
1377
|
+
lastResumedCount: last.resumedCount || 0,
|
|
1208
1378
|
lastCheckType: last.checkType || "unknown",
|
|
1209
1379
|
lastModel: last.model || null,
|
|
1210
1380
|
lastSpend: rowSpendText(last),
|
|
1211
|
-
lastWall: last
|
|
1381
|
+
lastWall: rowWallText(last) || "unknown",
|
|
1212
1382
|
lastDate: last.date,
|
|
1213
1383
|
};
|
|
1214
1384
|
});
|
|
@@ -1337,11 +1507,13 @@
|
|
|
1337
1507
|
row.innerHTML =
|
|
1338
1508
|
'<span class="wf-line1"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1339
1509
|
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span></span>' +
|
|
1510
|
+
(g.lastStatus ? '<span class="wf-ended" data-testid="wf-ended-' + escapeXml(g.job) + '">' + statusWordHtml(g.lastStatus, g.lastEndedLine) + '</span>' : '') +
|
|
1340
1511
|
'<span class="wf-meta-line">' +
|
|
1341
1512
|
'<span class="wf-meta">' + escapeXml(g.lastCheckType) + '</span>' +
|
|
1342
1513
|
'<span class="wf-meta">' + escapeXml(g.lastSpend) + '</span>' +
|
|
1343
1514
|
'<span class="wf-meta">' + escapeXml(g.lastWall) + '</span>' +
|
|
1344
1515
|
'<span class="wf-meta">' + escapeXml(g.lastDate || "unknown") + '</span>' +
|
|
1516
|
+
resumedTagHtml(g.lastResumedCount, "wf-resumed-" + g.job) +
|
|
1345
1517
|
'<span class="wf-meta">' + g.runCount + ' run' + (g.runCount === 1 ? '' : 's') + '</span>' +
|
|
1346
1518
|
'</span>';
|
|
1347
1519
|
row.addEventListener("click", function(){
|
|
@@ -1368,7 +1540,7 @@
|
|
|
1368
1540
|
if(expanded){
|
|
1369
1541
|
var runsWrap = document.createElement("div");
|
|
1370
1542
|
runsWrap.className = "wf-runs";
|
|
1371
|
-
childRuns.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r)); });
|
|
1543
|
+
childRuns.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1372
1544
|
wrap.appendChild(runsWrap);
|
|
1373
1545
|
}
|
|
1374
1546
|
el.appendChild(wrap);
|
|
@@ -1585,6 +1757,10 @@
|
|
|
1585
1757
|
});
|
|
1586
1758
|
document.getElementById("wf-list").hidden = runsViewMode !== "workflows";
|
|
1587
1759
|
document.getElementById("history-list").hidden = runsViewMode !== "history";
|
|
1760
|
+
// P5 item 4: Import lives on the Workflows view only
|
|
1761
|
+
document.getElementById("wf-import").hidden = runsViewMode !== "workflows";
|
|
1762
|
+
document.getElementById("import-list").hidden = runsViewMode !== "workflows";
|
|
1763
|
+
if(runsViewMode !== "workflows") document.getElementById("import-box").hidden = true;
|
|
1588
1764
|
}
|
|
1589
1765
|
document.querySelectorAll('.chip[data-runs-view]').forEach(function(chip){
|
|
1590
1766
|
chip.addEventListener("click", function(){
|
|
@@ -1622,6 +1798,264 @@
|
|
|
1622
1798
|
document.querySelectorAll(".wf-row, .hist-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
1623
1799
|
}
|
|
1624
1800
|
|
|
1801
|
+
// ---- P5 item 4: IMPORT (read only) — the folder browser, the imported rows, the view ----
|
|
1802
|
+
// The folder listing, the import and every re-read are SERVER routes behind the human guard
|
|
1803
|
+
// (src/panel/importroutes.js): the page only asks and paints. An imported job has ONE button: Reuse workflow.
|
|
1804
|
+
var currentImportId = null;
|
|
1805
|
+
var importBox = document.getElementById("import-box");
|
|
1806
|
+
var importPathEl = document.getElementById("import-path");
|
|
1807
|
+
var importErrEl = document.getElementById("import-err");
|
|
1808
|
+
function importShowErr(msg){ importErrEl.textContent = msg; importErrEl.hidden = !msg; }
|
|
1809
|
+
// the last folder the browser showed — per-viewer convenience only (try/catch, the browser works without it)
|
|
1810
|
+
var IMPORT_DIR_KEY = "bareloop.importDir";
|
|
1811
|
+
function rememberImportDir(path){
|
|
1812
|
+
try{ localStorage.setItem(IMPORT_DIR_KEY, path); }catch(e){ /* best-effort only */ }
|
|
1813
|
+
}
|
|
1814
|
+
function recallImportDir(){
|
|
1815
|
+
try{ var v = localStorage.getItem(IMPORT_DIR_KEY); if(typeof v === "string") return v; }catch(e){ /* per-viewer convenience only */ }
|
|
1816
|
+
return "";
|
|
1817
|
+
}
|
|
1818
|
+
// one directory: its folders (names + a "bundle" tag), a way up, and which folder the server actually chose
|
|
1819
|
+
function importLoadDir(path){
|
|
1820
|
+
importShowErr("");
|
|
1821
|
+
return authorGet("/api/fs/list?path=" + encodeURIComponent(path || "")).then(function(r){
|
|
1822
|
+
if(!r || !r.ok){
|
|
1823
|
+
// a refused path leaves ONLY the error: no stale "showing <folder>" line, no stale list
|
|
1824
|
+
document.getElementById("import-resolved").textContent = "";
|
|
1825
|
+
document.getElementById("import-entries").innerHTML = "";
|
|
1826
|
+
importShowErr((r && r.error) || "could not open that folder");
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
rememberImportDir(r.path);
|
|
1830
|
+
importPathEl.value = r.path;
|
|
1831
|
+
document.getElementById("import-resolved").textContent = "showing " + r.path + (r.requested !== r.path ? " (you asked for " + r.requested + ")" : "") + (r.bundle ? " — this folder is an exported job" : "");
|
|
1832
|
+
var el = document.getElementById("import-entries");
|
|
1833
|
+
el.innerHTML = "";
|
|
1834
|
+
if(r.parent){
|
|
1835
|
+
var up = document.createElement("button");
|
|
1836
|
+
up.type = "button"; up.className = "import-entry import-up"; up.setAttribute("data-testid", "import-up");
|
|
1837
|
+
up.setAttribute("aria-label", "up one folder"); up.setAttribute("title", "up one folder");
|
|
1838
|
+
up.textContent = "..";
|
|
1839
|
+
up.addEventListener("click", function(){ importLoadDir(r.parent); });
|
|
1840
|
+
el.appendChild(up);
|
|
1841
|
+
}
|
|
1842
|
+
r.entries.forEach(function(e){
|
|
1843
|
+
var b = document.createElement("button");
|
|
1844
|
+
b.type = "button"; b.className = "import-entry"; b.setAttribute("data-testid", "import-entry-" + e.name);
|
|
1845
|
+
b.setAttribute("aria-pressed", "false");
|
|
1846
|
+
b.innerHTML = escapeXml(e.name) + ' <span class="tag">(bundle)</span>';
|
|
1847
|
+
b.addEventListener("click", function(){
|
|
1848
|
+
el.querySelectorAll(".import-entry").forEach(function(x){ x.classList.remove("selected"); x.setAttribute("aria-pressed", "false"); });
|
|
1849
|
+
b.classList.add("selected"); b.setAttribute("aria-pressed", "true");
|
|
1850
|
+
importPathEl.value = e.path; importShowErr("");
|
|
1851
|
+
});
|
|
1852
|
+
el.appendChild(b);
|
|
1853
|
+
});
|
|
1854
|
+
if(!r.entries.length) el.insertAdjacentHTML("beforeend", '<div class="hint" data-testid="import-empty">no job bundles here (searched ' + r.depth + ' folders deep)</div>');
|
|
1855
|
+
if(r.stopped) el.insertAdjacentHTML("beforeend", '<div class="hint" data-testid="import-capped">stopped after ' + r.visited + ' folders \u2014 go into a narrower folder</div>');
|
|
1856
|
+
else if(r.truncated) el.insertAdjacentHTML("beforeend", '<div class="hint" data-testid="import-capped">stopped after ' + r.entries.length + ' job bundles \u2014 go into a narrower folder</div>');
|
|
1857
|
+
return true;
|
|
1858
|
+
}).catch(function(x){ importShowErr("could not reach the panel: " + x.message); });
|
|
1859
|
+
}
|
|
1860
|
+
document.getElementById("wf-import").addEventListener("click", function(){
|
|
1861
|
+
importBox.hidden = !importBox.hidden;
|
|
1862
|
+
if(importBox.hidden) return;
|
|
1863
|
+
var want = importPathEl.value || recallImportDir();
|
|
1864
|
+
importLoadDir(want).then(function(ok){ if(ok === false && want && !importPathEl.value) { importShowErr(""); return importLoadDir(""); } });
|
|
1865
|
+
});
|
|
1866
|
+
document.getElementById("import-open").addEventListener("click", function(){ importLoadDir(importPathEl.value); });
|
|
1867
|
+
importPathEl.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); importLoadDir(importPathEl.value); } });
|
|
1868
|
+
document.getElementById("import-cancel").addEventListener("click", function(){ importBox.hidden = true; importShowErr(""); });
|
|
1869
|
+
document.getElementById("import-go").addEventListener("click", function(){
|
|
1870
|
+
importShowErr("");
|
|
1871
|
+
authorPost("/api/imports", {path: importPathEl.value}).then(function(res){
|
|
1872
|
+
if(!res.body || !res.body.ok){ importShowErr((res.body && res.body.error) || ("refused (" + res.status + ")")); return; }
|
|
1873
|
+
importBox.hidden = true;
|
|
1874
|
+
return loadImports().then(function(){ return selectImport(res.body.id); }).then(function(){ revealImportRow(res.body.id); scrollRunIntoViewMobile(); });
|
|
1875
|
+
}).catch(function(x){ importShowErr("could not reach the panel: " + x.message); });
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
// after [Import] the box above the list closes and the left pane shrinks: bring the job just opened into view
|
|
1879
|
+
function revealImportRow(id){
|
|
1880
|
+
var row = document.querySelector('[data-testid="import-row-' + id + '"]');
|
|
1881
|
+
if(row && row.scrollIntoView) row.scrollIntoView({block: "nearest"});
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// the imported rows (Workflows view): every bundle is re-read by the server each time this list is fetched
|
|
1885
|
+
function loadImports(){
|
|
1886
|
+
return authorGet("/api/imports").then(function(r){ renderImports((r && r.imports) || []); }).catch(function(){ /* keep what is shown */ });
|
|
1887
|
+
}
|
|
1888
|
+
function renderImports(list){
|
|
1889
|
+
var el = document.getElementById("import-list");
|
|
1890
|
+
el.innerHTML = "";
|
|
1891
|
+
list.forEach(function(imp){
|
|
1892
|
+
var row = document.createElement("div");
|
|
1893
|
+
row.className = "wf-row" + (imp.id === currentImportId ? " selected" : "");
|
|
1894
|
+
row.setAttribute("data-testid", "import-row-" + imp.id);
|
|
1895
|
+
row.setAttribute("tabindex", "0");
|
|
1896
|
+
row.setAttribute("role", "button");
|
|
1897
|
+
row.setAttribute("aria-pressed", imp.id === currentImportId ? "true" : "false");
|
|
1898
|
+
row.innerHTML =
|
|
1899
|
+
'<span class="wf-line1"><span class="wf-caret"></span><span class="dot ' + (imp.runStatus ? glyphClass(imp.runStatus.sign) : "grey") + '"></span>' +
|
|
1900
|
+
'<span class="wf-name" title="' + escapeXml(imp.dir) + '">' + escapeXml(imp.job) + '</span></span>' +
|
|
1901
|
+
(imp.runStatus ? '<span class="wf-ended" data-testid="import-ended-' + escapeXml(imp.id) + '">' + statusWordHtml(imp.runStatus, imp.runReason) + '</span>' : '') +
|
|
1902
|
+
'<span class="wf-meta-line"><span class="wf-meta wf-imported" data-testid="import-tag-' + escapeXml(imp.id) + '">imported · view only</span></span>' +
|
|
1903
|
+
(imp.status !== "ok" ? '<span class="imp-bad" data-testid="import-changed-' + escapeXml(imp.id) + '">' + escapeXml(imp.statusText || "changed since import") + '</span>' : "");
|
|
1904
|
+
row.addEventListener("click", function(){ selectImport(imp.id); scrollRunIntoViewMobile(); });
|
|
1905
|
+
row.addEventListener("keydown", function(e){ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); row.click(); } });
|
|
1906
|
+
el.appendChild(row);
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
function hideImportView(){
|
|
1910
|
+
currentImportId = null;
|
|
1911
|
+
document.getElementById("import-view").hidden = true;
|
|
1912
|
+
document.getElementById("run-counters").hidden = false;
|
|
1913
|
+
document.getElementById("run-empty").textContent = "select a run from Workflows or History";
|
|
1914
|
+
document.getElementById("audit-select-empty").textContent = "select a run first";
|
|
1915
|
+
document.querySelectorAll("#import-list .wf-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed", "false"); });
|
|
1916
|
+
}
|
|
1917
|
+
function selectImport(id){
|
|
1918
|
+
clearSelection();
|
|
1919
|
+
currentRunid = null;
|
|
1920
|
+
runIsLive = false;
|
|
1921
|
+
resumeMode = null;
|
|
1922
|
+
currentImportId = id;
|
|
1923
|
+
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
1924
|
+
var on = r.getAttribute("data-testid") === "import-row-" + id;
|
|
1925
|
+
r.classList.toggle("selected", on); r.setAttribute("aria-pressed", on ? "true" : "false");
|
|
1926
|
+
});
|
|
1927
|
+
return authorGet("/api/imports/" + encodeURIComponent(id)).then(function(v){
|
|
1928
|
+
if(currentImportId !== id) return;
|
|
1929
|
+
renderImportView(v);
|
|
1930
|
+
}).catch(function(x){ if(currentImportId === id) renderImportView({ok: false, error: x.message}); });
|
|
1931
|
+
}
|
|
1932
|
+
// An imported job reads like a normal run page: the SAME Run / Audit / Job tabs. [Reuse workflow] sits in the top
|
|
1933
|
+
// action row, then the IMPORTED box (what the folder says about itself), then the latest GREEN run in the bundle
|
|
1934
|
+
// painted by the very same renderRun a run uses — its SUMMARY and MAP, and its Audit when the bundle carries the
|
|
1935
|
+
// run's log; a bundle with only a bridge paints the bridge's numbers through that same renderer ("not recorded"
|
|
1936
|
+
// where the bridge is silent); a bundle with no green says so. The Job tab carries the spec. Nothing here writes.
|
|
1937
|
+
function importedHeader(v){
|
|
1938
|
+
var tag = "imported \u00B7 view only";
|
|
1939
|
+
if(v.runStatus) setRunHeader(v.runStatus.sign, v.job, statusWordHtml(v.runStatus, v.runReason) + " \u00B7 " + tag);
|
|
1940
|
+
else setRunHeader("\u00B7", v.job, tag);
|
|
1941
|
+
}
|
|
1942
|
+
function importedNoLog(){
|
|
1943
|
+
document.getElementById("audit-body").hidden = true;
|
|
1944
|
+
var auditEmpty = document.getElementById("audit-select-empty");
|
|
1945
|
+
auditEmpty.hidden = false;
|
|
1946
|
+
auditEmpty.textContent = "no log — this job ran on another machine";
|
|
1947
|
+
}
|
|
1948
|
+
// the green run below the IMPORTED box: spine (read through the ordinary run routes), bridge, or nothing
|
|
1949
|
+
function paintImportedRun(v){
|
|
1950
|
+
var run = v.run || {kind: "none"};
|
|
1951
|
+
var content = document.getElementById("run-content");
|
|
1952
|
+
var guard = function(){ return currentImportId === v.id; };
|
|
1953
|
+
document.getElementById("step-list").innerHTML = "";
|
|
1954
|
+
if(run.kind === "spine"){
|
|
1955
|
+
currentRunid = run.id;
|
|
1956
|
+
getJSON("/api/runs/" + encodeURIComponent(run.id)).then(function(d){
|
|
1957
|
+
if(!guard()) return;
|
|
1958
|
+
d.ended = null; d.resume = null; d.live = false; d.stopping = false;
|
|
1959
|
+
renderRun(d);
|
|
1960
|
+
importedHeader(v);
|
|
1961
|
+
document.getElementById("run-actions").hidden = true;
|
|
1962
|
+
document.getElementById("run-counters").hidden = true; // the SUMMARY headline already says it
|
|
1963
|
+
content.hidden = false;
|
|
1964
|
+
}).catch(function(e){ if(guard()) paintImportedRunPlain(v, "could not read this run: " + e.message); });
|
|
1965
|
+
getJSON("/api/runs/" + encodeURIComponent(run.id) + "/audit").then(function(result){
|
|
1966
|
+
if(guard()) renderAudit(result);
|
|
1967
|
+
}).catch(function(){ if(guard()) renderAudit(null); });
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
importedNoLog();
|
|
1971
|
+
if(run.kind === "bridge" && run.detail){
|
|
1972
|
+
renderRun(run.detail);
|
|
1973
|
+
importedHeader(v);
|
|
1974
|
+
importedNoLog();
|
|
1975
|
+
document.getElementById("run-actions").hidden = true;
|
|
1976
|
+
document.getElementById("run-counters").hidden = true;
|
|
1977
|
+
document.getElementById("step-list").innerHTML = "";
|
|
1978
|
+
if(!run.detail.parts || run.detail.parts.length === 0){
|
|
1979
|
+
document.getElementById("map-box").hidden = false;
|
|
1980
|
+
document.getElementById("map-mount").innerHTML = '<div class="hint" data-testid="import-no-steps">no steps recorded</div>';
|
|
1981
|
+
}
|
|
1982
|
+
content.hidden = false;
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
paintImportedRunPlain(v, "no green run in this bundle");
|
|
1986
|
+
}
|
|
1987
|
+
// a plain sentence in the SUMMARY box and nothing else (no counters, no map, no cards)
|
|
1988
|
+
function paintImportedRunPlain(v, text){
|
|
1989
|
+
var box = document.getElementById("run-summary");
|
|
1990
|
+
box.className = "summary-box";
|
|
1991
|
+
box.innerHTML = '<div class="summary-row"><span class="sum-val" data-testid="import-no-green">' + escapeXml(text) + '</span></div>';
|
|
1992
|
+
document.getElementById("run-counters").hidden = true;
|
|
1993
|
+
document.getElementById("map-box").hidden = true;
|
|
1994
|
+
document.getElementById("step-list").innerHTML = "";
|
|
1995
|
+
document.getElementById("run-actions").hidden = true;
|
|
1996
|
+
importedNoLog();
|
|
1997
|
+
document.getElementById("run-content").hidden = false;
|
|
1998
|
+
}
|
|
1999
|
+
function renderImportView(v){
|
|
2000
|
+
var el = document.getElementById("import-view");
|
|
2001
|
+
el.hidden = false;
|
|
2002
|
+
document.getElementById("run-empty").hidden = true;
|
|
2003
|
+
document.getElementById("run-content").hidden = true;
|
|
2004
|
+
importedNoLog();
|
|
2005
|
+
if(!v || !v.ok){
|
|
2006
|
+
setRunHeader("\u00B7", "imported job", "imported \u00B7 view only");
|
|
2007
|
+
el.innerHTML = '<div class="imp-bad">' + escapeXml((v && v.error) || "could not read this imported job") + '</div>';
|
|
2008
|
+
renderJob(null);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
importedHeader(v);
|
|
2012
|
+
var h = v.history || {greens: 0, reds: 0, total: 0, recent: []};
|
|
2013
|
+
var row = function(label, text, testid){
|
|
2014
|
+
return '<div class="summary-row"><span class="sum-label">' + label + '</span><span class="sum-val" data-testid="' + testid + '">' + escapeXml(text) + '</span></div>';
|
|
2015
|
+
};
|
|
2016
|
+
var recent = (h.recent || []).map(function(r){
|
|
2017
|
+
var line = (r.at || "").slice(0, 10) || "—";
|
|
2018
|
+
line += " · " + (r.outcome || "?") + (typeof r.costUsd === "number" ? " · " + panelMoney(r.costUsd) : "") + (r.runid ? " · run " + r.runid : "");
|
|
2019
|
+
return '<div class="summary-row"><span class="sum-label"></span><span class="sum-val">' + escapeXml(line) + '</span></div>';
|
|
2020
|
+
}).join("");
|
|
2021
|
+
var run = v.run || {kind: "none"};
|
|
2022
|
+
var runsHere = typeof v.runsHere === "number" ? v.runsHere : 0;
|
|
2023
|
+
var shown = run.kind === "spine" ? ("latest green run here, " + (run.runid || "")) :
|
|
2024
|
+
(run.kind === "bridge" ? ("the exported green run" + (run.runid ? " " + run.runid : "") + (run.at ? ", " + run.at.slice(0, 10) : "")) : "no green run");
|
|
2025
|
+
el.innerHTML =
|
|
2026
|
+
'<div class="run-actions" data-testid="run-actions-import"><button class="btn" type="button" data-testid="btn-reuse-import">Reuse workflow</button></div>' +
|
|
2027
|
+
'<div class="summary-box imported-box" data-testid="import-box">' +
|
|
2028
|
+
(v.status !== "ok" ? '<div class="imp-bad" data-testid="import-view-changed">' + escapeXml(v.changedLine || v.statusText || "changed since import") + '</div>' : "") +
|
|
2029
|
+
row("runs here:", runsHere === 0 ? "none yet" : (runsHere + (runsHere === 1 ? " run" : " runs")), "import-no-runs") +
|
|
2030
|
+
row("exported:", h.greens + " green · " + h.reds + " not green", "import-history") +
|
|
2031
|
+
recent +
|
|
2032
|
+
row("this machine:", v.approvedText || "unknown", "import-approved") +
|
|
2033
|
+
row("shown below:", shown, "import-shown") +
|
|
2034
|
+
'</div>';
|
|
2035
|
+
el.querySelector('[data-testid="btn-reuse-import"]').addEventListener("click", function(){ reuseImportedWorkflow(v.id); });
|
|
2036
|
+
var nr = "not recorded";
|
|
2037
|
+
renderJob({
|
|
2038
|
+
job: v.job, resolved: false, checkType: v.checkType || nr, model: v.model || nr,
|
|
2039
|
+
goal: v.goal || nr, success: v.success || nr, guardrails: v.guardrails || nr,
|
|
2040
|
+
source: nr, destination: nr, tools: nr,
|
|
2041
|
+
budgetUsd: (typeof v.budgetUsd === "number") ? v.budgetUsd : null,
|
|
2042
|
+
maxWallMs: (typeof v.maxWallMs === "number") ? v.maxWallMs : null,
|
|
2043
|
+
note: "imported · view only — the exported job's own spec",
|
|
2044
|
+
});
|
|
2045
|
+
paintImportedRun(v);
|
|
2046
|
+
document.getElementById("tab-run").click();
|
|
2047
|
+
}
|
|
2048
|
+
// Reuse workflow, on an IMPORTED job: the server reads the bundle again now and prefills from its signed spec.
|
|
2049
|
+
function reuseImportedWorkflow(id){
|
|
2050
|
+
authorGet("/api/author/start-from?import=" + encodeURIComponent(id)).then(function(r){
|
|
2051
|
+
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2052
|
+
document.getElementById("tab-chat").click();
|
|
2053
|
+
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: null, importId: id, prefill: r}}));
|
|
2054
|
+
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2055
|
+
}
|
|
2056
|
+
loadImports();
|
|
2057
|
+
document.getElementById("tab-runs").addEventListener("click", function(){ loadImports(); });
|
|
2058
|
+
|
|
1625
2059
|
// ---- Run tab ----
|
|
1626
2060
|
// panelMoney — hamr's ruling 2026-09-28 ("panel money 2-decimals"): every
|
|
1627
2061
|
// money display in THIS page (Run tab summary/counters, Runs list rows,
|
|
@@ -1679,6 +2113,17 @@
|
|
|
1679
2113
|
var capPart = (budgetUsd === undefined) ? "" : (" of " + panelMoney(budgetUsd));
|
|
1680
2114
|
return core + draftPart + capPart;
|
|
1681
2115
|
}
|
|
2116
|
+
// P5 item 6: a LIVE row (no job-end yet, runner alive) reads the same running floors the
|
|
2117
|
+
// right pane reads — `liveSpendText`/`liveWallPhrase` are the one spelling — and a row whose
|
|
2118
|
+
// spine is not written yet reads "starting…", never "file missing" or "unknown".
|
|
2119
|
+
function rowIsLive(r){
|
|
2120
|
+
return r.glyph === "▶" && !r.died && !r.starting && (r.spentUsd === null || r.spentUsd === undefined);
|
|
2121
|
+
}
|
|
2122
|
+
function rowWallText(r){
|
|
2123
|
+
if(r.starting) return "just started";
|
|
2124
|
+
if(rowIsLive(r)) return liveWallPhrase(r.wallFloorMs);
|
|
2125
|
+
return r.wall;
|
|
2126
|
+
}
|
|
1682
2127
|
/**
|
|
1683
2128
|
* The Runs list row / Workflows grouping's own spend text — off the
|
|
1684
2129
|
* numeric fields `/api/runs`' row now carries (never `r.spend`, which is
|
|
@@ -1690,6 +2135,8 @@
|
|
|
1690
2135
|
* @param {any} r a `/api/runs` row (or a Workflows group's `last` row)
|
|
1691
2136
|
*/
|
|
1692
2137
|
function rowSpendText(r){
|
|
2138
|
+
if(r.starting) return "starting…";
|
|
2139
|
+
if(rowIsLive(r)) return liveSpendText(r.spendFloorUsd, r.draftSpentUsd, r.draftSpendComplete);
|
|
1693
2140
|
var draftIncomplete = typeof r.draftSpentUsd === "number" && r.draftSpentUsd !== null && r.draftSpendComplete === false;
|
|
1694
2141
|
var coreVal = r.died ? r.spendFloorUsd : r.spentUsd;
|
|
1695
2142
|
var coreFloor = !!r.died || draftIncomplete;
|
|
@@ -1807,6 +2254,11 @@
|
|
|
1807
2254
|
if(!detail.model) return "not recorded";
|
|
1808
2255
|
return detail.provider ? (detail.model + " (" + detail.provider + ")") : detail.model;
|
|
1809
2256
|
}
|
|
2257
|
+
// an imported job's bridge names the tools its green run used, with no counts
|
|
2258
|
+
function bridgeToolsLine(list){
|
|
2259
|
+
if(!Array.isArray(list) || list.length === 0) return "not recorded";
|
|
2260
|
+
return list.map(function(k){ return toolDisplayLabel(k); }).join(" \u00b7 ");
|
|
2261
|
+
}
|
|
1810
2262
|
function toolsLine(behaviour){
|
|
1811
2263
|
if(!behaviour) return "unknown (no log saved)";
|
|
1812
2264
|
if(behaviour.totalCalls === 0) return "0 calls";
|
|
@@ -1858,36 +2310,199 @@
|
|
|
1858
2310
|
else { row.hidden = true; val.textContent = ""; }
|
|
1859
2311
|
}
|
|
1860
2312
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2313
|
+
// ---- P5: the ENDED block (Audit tab, first block, every finished run) and Resume ----
|
|
2314
|
+
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2315
|
+
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2316
|
+
var lastEndedSig = null;
|
|
2317
|
+
// P5-R (hamr 2026-10-02): Resume opens the run's OWN Job tab. There ONLY the money cap and the time cap are
|
|
2318
|
+
// editable; every other field is the signed job, shown read-only exactly as the tab already shows it. The click
|
|
2319
|
+
// on [Sign & resume] is the signature when a cap changed. A resume is the SAME run: no new card, no new job.
|
|
2320
|
+
var resumeMode = null;
|
|
2321
|
+
var lastJobShown = null;
|
|
2322
|
+
function renderEnded(detail){
|
|
2323
|
+
var box = document.getElementById("ended-block");
|
|
2324
|
+
var e = detail.ended;
|
|
2325
|
+
if(!e){ box.hidden = true; box.innerHTML = ""; lastEndedSig = null; return; }
|
|
2326
|
+
// rebuilt only when what it shows changed
|
|
2327
|
+
var sig2 = detail.runid + "|" + JSON.stringify(e) + "|" + JSON.stringify(detail.resume);
|
|
2328
|
+
if(sig2 === lastEndedSig) return;
|
|
2329
|
+
lastEndedSig = sig2;
|
|
2330
|
+
box.hidden = false;
|
|
2331
|
+
box.className = "ended-block " + (detail.died ? "magenta" : (detail.glyph === "✓" ? "green" : "red"));
|
|
2332
|
+
// text only: the ONE place for [Stop] / [Resume] / [Reuse workflow] is the Run tab's top action row (renderRunActions)
|
|
2333
|
+
box.innerHTML =
|
|
2334
|
+
'<div class="ended-row"><span class="ended-label">ENDED</span><span class="ended-text" data-testid="ended-reason">' + escapeXml(e.reason) + '</span></div>' +
|
|
2335
|
+
'<div class="ended-row"><span class="ended-label">NEXT</span><span class="ended-text" data-testid="ended-next">' + escapeXml(e.next) + '</span></div>';
|
|
2336
|
+
}
|
|
2337
|
+
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2338
|
+
// it ended in a way the engine would accept (the SAME button as the Ended block's, same Job-tab resume mode).
|
|
2339
|
+
// The Ended block (Audit tab) is text only and carries no buttons.
|
|
2340
|
+
var stopAsked = {};
|
|
2341
|
+
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
2342
|
+
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2343
|
+
function reuseWorkflow(runid){
|
|
2344
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid)).then(function(r){
|
|
2345
|
+
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2346
|
+
document.getElementById("tab-chat").click();
|
|
2347
|
+
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2348
|
+
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2349
|
+
}
|
|
2350
|
+
function renderRunActions(detail){
|
|
2351
|
+
var bar = document.getElementById("run-actions");
|
|
2352
|
+
var html = "";
|
|
2353
|
+
var live = !!detail.live && !detail.died && !detail.ended;
|
|
2354
|
+
if(live){
|
|
2355
|
+
if(detail.stopping || stopAsked[detail.runid]){
|
|
2356
|
+
html = '<span class="stopping" data-testid="stopping-note">stopping after this turn…</span>';
|
|
2357
|
+
} else {
|
|
2358
|
+
html = '<button class="btn" type="button" data-testid="btn-stop">Stop</button>';
|
|
2359
|
+
}
|
|
2360
|
+
} else {
|
|
2361
|
+
delete stopAsked[detail.runid];
|
|
2362
|
+
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2363
|
+
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2364
|
+
}
|
|
2365
|
+
// Reuse workflow: on a GREEN run only (the server's Ended block says so) — a new run of the same signed job
|
|
2366
|
+
if(!live && !detail.died && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2367
|
+
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
2368
|
+
}
|
|
2369
|
+
bar.innerHTML = html;
|
|
2370
|
+
bar.hidden = html === "";
|
|
2371
|
+
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2372
|
+
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2373
|
+
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2374
|
+
if(stopBtn){
|
|
2375
|
+
stopBtn.addEventListener("click", function(){
|
|
2376
|
+
stopBtn.disabled = true;
|
|
2377
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/stop", {}).then(function(res){
|
|
2378
|
+
if(res.body && res.body.ok){ stopAsked[detail.runid] = true; renderRunActions(detail); }
|
|
2379
|
+
else { stopBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="stop-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>'); }
|
|
2380
|
+
}).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>'); });
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2384
|
+
if(resBtn) resBtn.addEventListener("click", function(){ openResumeOnJobTab(detail); });
|
|
2385
|
+
}
|
|
2386
|
+
function openResumeOnJobTab(detail){
|
|
2387
|
+
var r = detail.resume || {};
|
|
2388
|
+
resumeMode = {
|
|
2389
|
+
runid: detail.runid, spentUsd: r.spentUsd, spendComplete: r.spendComplete,
|
|
2390
|
+
wallUsedMs: r.wallUsedMs,
|
|
2391
|
+
budget: (r.budgetUsd === null || r.budgetUsd === undefined) ? "" : String(r.budgetUsd),
|
|
2392
|
+
wall: typeof r.maxWallMin === "number" ? String(r.maxWallMin) : null,
|
|
2393
|
+
painted: false,
|
|
2394
|
+
};
|
|
2395
|
+
document.getElementById("tab-details").click();
|
|
2396
|
+
paintResumeMode();
|
|
2397
|
+
}
|
|
2398
|
+
// Turns the Job tab's two cap values into inputs (and shows the Sign box) while a resume is open on this run;
|
|
2399
|
+
// called again after every Job re-render so a re-render never drops the typed numbers.
|
|
2400
|
+
function paintResumeMode(){
|
|
2401
|
+
var box = document.getElementById("resume-job");
|
|
2402
|
+
var moneyLabel = document.getElementById("details-cap-money-label");
|
|
2403
|
+
var timeLabel = document.getElementById("details-cap-time-label");
|
|
2404
|
+
if(!resumeMode || resumeMode.runid !== currentRunid){
|
|
2405
|
+
box.hidden = true; box.innerHTML = "";
|
|
2406
|
+
moneyLabel.textContent = "$ cap"; timeLabel.textContent = "Time cap";
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
var m = resumeMode;
|
|
2410
|
+
// the unit lives in the caption, never as loose text around the input
|
|
2411
|
+
moneyLabel.textContent = "Money cap ($)";
|
|
2412
|
+
timeLabel.textContent = "Time cap (min)";
|
|
2413
|
+
document.getElementById("details-cap-money").innerHTML =
|
|
2414
|
+
'<span class="resume-cap"><input type="number" min="0" step="0.01" id="resume-budget" data-testid="resume-budget" value="' + escapeXml(m.budget) + '"></span>';
|
|
2415
|
+
if(m.wall !== null){
|
|
2416
|
+
document.getElementById("details-cap-time").innerHTML =
|
|
2417
|
+
'<span class="resume-cap"><input type="number" min="1" step="1" id="resume-wall" data-testid="resume-wall" value="' + escapeXml(m.wall) + '"></span>';
|
|
2418
|
+
}
|
|
2419
|
+
var budgetEl = document.getElementById("resume-budget");
|
|
2420
|
+
budgetEl.addEventListener("input", function(){ m.budget = budgetEl.value; });
|
|
2421
|
+
var wallEl = document.getElementById("resume-wall");
|
|
2422
|
+
if(wallEl) wallEl.addEventListener("input", function(){ m.wall = wallEl.value; });
|
|
2423
|
+
box.hidden = false;
|
|
2424
|
+
if(m.painted) return;
|
|
2425
|
+
m.painted = true;
|
|
2426
|
+
var spent = (m.spentUsd === null || m.spentUsd === undefined) ? "unknown" : ((m.spendComplete ? "" : "at least ") + panelMoney(m.spentUsd));
|
|
2427
|
+
box.innerHTML =
|
|
2428
|
+
'<div data-testid="resume-intro">Resume run ' + escapeXml(m.runid) + ' — the same run, not a new one. Only the money cap and the time cap can change; everything else is the signed job.</div>' +
|
|
2429
|
+
'<div class="hint" data-testid="resume-spent">spent so far ' + escapeXml(spent) +
|
|
2430
|
+
(typeof m.wallUsedMs === "number" && isFinite(m.wallUsedMs) ? ' · time used so far ' + Math.ceil(m.wallUsedMs / 60000) + ' min' : '') + '</div>' +
|
|
2431
|
+
'<div class="ended-actions"><button class="btn primary" type="button" data-testid="btn-sign-resume">Sign & resume</button> ' +
|
|
2432
|
+
'<button class="btn" type="button" data-testid="btn-resume-cancel">Cancel</button></div>' +
|
|
2433
|
+
'<div class="resume-err" id="resume-err" data-testid="resume-err" hidden></div>';
|
|
2434
|
+
var resumeSignBtn = box.querySelector('[data-testid="btn-sign-resume"]');
|
|
2435
|
+
box.querySelector('[data-testid="btn-resume-cancel"]').addEventListener("click", function(){
|
|
2436
|
+
resumeMode = null;
|
|
2437
|
+
renderJob(lastJobShown);
|
|
2438
|
+
});
|
|
2439
|
+
resumeSignBtn.addEventListener("click", function(){
|
|
2440
|
+
var err = document.getElementById("resume-err");
|
|
2441
|
+
err.hidden = true;
|
|
2442
|
+
resumeSignBtn.disabled = true;
|
|
2443
|
+
var body = {budgetUsd: document.getElementById("resume-budget").value};
|
|
2444
|
+
var wall = document.getElementById("resume-wall");
|
|
2445
|
+
if(wall){ body.maxWallMin = wall.value; }
|
|
2446
|
+
return authorPost("/api/runs/" + encodeURIComponent(m.runid) + "/resume", body).then(function(res){
|
|
2447
|
+
if(res.body && res.body.ok){
|
|
2448
|
+
// the SAME run goes on: back to its Run tab, which follows it until it is live again
|
|
2449
|
+
resumeMode = null;
|
|
2450
|
+
renderJob(lastJobShown);
|
|
2451
|
+
document.getElementById("tab-run").click();
|
|
2452
|
+
afterResumeRefresh(m.runid, 8);
|
|
2453
|
+
} else {
|
|
2454
|
+
resumeSignBtn.disabled = false;
|
|
2455
|
+
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
2456
|
+
err.hidden = false;
|
|
2457
|
+
}
|
|
2458
|
+
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
// After a resume starts, the open run's own page follows it: re-select once the run reads live again (the spine's
|
|
2462
|
+
// new leg appears a moment after the engine starts), a few tries, never forever.
|
|
2463
|
+
function afterResumeRefresh(runid, tries){
|
|
2464
|
+
refreshRunsList(true);
|
|
2465
|
+
setTimeout(function(){
|
|
2466
|
+
if(currentRunid !== runid) return;
|
|
2467
|
+
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(d){
|
|
2468
|
+
if(currentRunid !== runid) return;
|
|
2469
|
+
if(d.glyph === "▶" && !d.died){ selectRun(runid, null, ".hist-row"); return; }
|
|
2470
|
+
if(tries > 0) afterResumeRefresh(runid, tries - 1);
|
|
2471
|
+
}).catch(function(){ if(tries > 0) afterResumeRefresh(runid, tries - 1); });
|
|
2472
|
+
}, 1500);
|
|
2473
|
+
}
|
|
1866
2474
|
|
|
2475
|
+
function renderRun(detail){
|
|
1867
2476
|
if(detail.fileMissing){
|
|
1868
|
-
|
|
1869
|
-
dot.className = "dot grey";
|
|
1870
|
-
verdict.textContent = "unknown";
|
|
1871
|
-
date.textContent = "file missing";
|
|
2477
|
+
setRunHeader("\u00B7", detail.job + " (" + detail.runid + ")", "file missing");
|
|
1872
2478
|
document.getElementById("run-empty").hidden = false;
|
|
1873
2479
|
document.getElementById("run-empty").textContent = "spine file no longer on disk";
|
|
1874
2480
|
document.getElementById("run-content").hidden = true;
|
|
1875
2481
|
document.getElementById("audit-select-empty").hidden = false;
|
|
2482
|
+
document.getElementById("audit-select-empty").textContent = "select a run first";
|
|
1876
2483
|
document.getElementById("audit-body").hidden = true;
|
|
1877
2484
|
return;
|
|
1878
2485
|
}
|
|
1879
2486
|
|
|
2487
|
+
if(detail.starting){
|
|
2488
|
+
setRunHeader("\u25B6", detail.job + " (" + detail.runid + ")", statusWordHtml(detail.status, "starting\u2026") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
|
|
2489
|
+
document.getElementById("run-empty").hidden = false;
|
|
2490
|
+
document.getElementById("run-empty").textContent = "starting… the run has not written its log yet";
|
|
2491
|
+
document.getElementById("run-content").hidden = true;
|
|
2492
|
+
document.getElementById("audit-select-empty").hidden = false;
|
|
2493
|
+
document.getElementById("audit-select-empty").textContent = "starting… the run has not written its log yet";
|
|
2494
|
+
document.getElementById("audit-body").hidden = true;
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
1880
2497
|
document.getElementById("audit-select-empty").hidden = true;
|
|
1881
2498
|
document.getElementById("audit-body").hidden = false;
|
|
2499
|
+
renderEnded(detail);
|
|
1882
2500
|
|
|
1883
|
-
|
|
1884
|
-
dot.className = "dot " + glyphClass(detail.glyph);
|
|
1885
|
-
verdict.textContent = detail.checkType;
|
|
1886
|
-
if(detail.checkTypeTitle){ verdict.title = detail.checkTypeTitle; } else { verdict.removeAttribute("title"); }
|
|
1887
|
-
date.textContent = detail.date || "unknown";
|
|
2501
|
+
setRunHeader(detail.glyph, detail.job + " (" + detail.runid + ")", runHeaderBody(detail));
|
|
1888
2502
|
|
|
1889
2503
|
document.getElementById("run-empty").hidden = true;
|
|
1890
2504
|
document.getElementById("run-content").hidden = false;
|
|
2505
|
+
renderRunActions(detail);
|
|
1891
2506
|
|
|
1892
2507
|
// died: the two REAL figures the panel has (spentUsd/wallMs, both null —
|
|
1893
2508
|
// no job-end means no final totals were ever recorded) are replaced with
|
|
@@ -1904,11 +2519,12 @@
|
|
|
1904
2519
|
// false and spentUsd is still null) shows the RUNNING floor instead of
|
|
1905
2520
|
// "unknown" — the server now derives the same spendFloorUsd/wallFloorMs
|
|
1906
2521
|
// for a still-running spine that it already derived for a died one.
|
|
1907
|
-
var isLiveNoEnd = !detail.died && detail.spentUsd === null;
|
|
1908
|
-
var spendText, wallPhrase;
|
|
2522
|
+
var isLiveNoEnd = !detail.died && detail.spentUsd === null && detail.fromBridge !== true;
|
|
2523
|
+
var spendText, wallPhrase, wallText;
|
|
1909
2524
|
if(isLiveNoEnd){
|
|
1910
2525
|
spendText = liveSpendText(detail.spendFloorUsd, detail.draftSpentUsd, detail.draftSpendComplete);
|
|
1911
2526
|
wallPhrase = liveWallPhrase(detail.wallFloorMs);
|
|
2527
|
+
wallText = wallPhrase; // the summary headline reads wallText in BOTH branches
|
|
1912
2528
|
} else {
|
|
1913
2529
|
var draftIncomplete = typeof detail.draftSpentUsd === "number" && detail.draftSpentUsd !== null && detail.draftSpendComplete === false;
|
|
1914
2530
|
var spendCoreVal = detail.died ? detail.spendFloorUsd : detail.spentUsd;
|
|
@@ -1921,10 +2537,16 @@
|
|
|
1921
2537
|
// still in progress: a live [▶] run (glyph "▶" and not died).
|
|
1922
2538
|
var isLive = detail.glyph === "▶" && !detail.died;
|
|
1923
2539
|
wallPhrase = isLive ? (wallText + " elapsed") : ("took " + wallText);
|
|
2540
|
+
// an imported job's green run read from its bridge: only what the bridge carries; the rest says so
|
|
2541
|
+
if(detail.fromBridge === true){
|
|
2542
|
+
spendText = typeof detail.spentUsd === "number" ? ((detail.spendComplete === false ? "at least " : "") + panelMoney(detail.spentUsd)) : "not recorded";
|
|
2543
|
+
wallText = typeof detail.wallMs === "number" ? duration(detail.wallMs) : "not recorded";
|
|
2544
|
+
wallPhrase = typeof detail.wallMs === "number" ? ("took " + wallText) : "time not recorded";
|
|
2545
|
+
}
|
|
1924
2546
|
}
|
|
1925
2547
|
|
|
1926
2548
|
document.getElementById("run-counters").innerHTML =
|
|
1927
|
-
spendText + " of " + panelMoney(detail.budgetUsd) +
|
|
2549
|
+
spendText + ((detail.fromBridge === true && typeof detail.budgetUsd !== "number") ? "" : (" of " + panelMoney(detail.budgetUsd))) +
|
|
1928
2550
|
" · " + wallPhrase;
|
|
1929
2551
|
|
|
1930
2552
|
var box = document.getElementById("run-summary");
|
|
@@ -1935,9 +2557,9 @@
|
|
|
1935
2557
|
// "0 of 1 done".
|
|
1936
2558
|
var rSteps = realSteps(detail.steps);
|
|
1937
2559
|
var stepsDone = rSteps.filter(function(s){ return s.state === "done"; }).length;
|
|
1938
|
-
var stepsText = rSteps.length === 0 ? "none started" : ('<span class="num">' + stepsDone + '</span> of <span class="num">' + rSteps.length + '</span> done');
|
|
2560
|
+
var stepsText = rSteps.length === 0 ? (detail.fromBridge === true ? "not recorded" : "none started") : ('<span class="num">' + stepsDone + '</span> of <span class="num">' + rSteps.length + '</span> done');
|
|
1939
2561
|
box.innerHTML =
|
|
1940
|
-
'<div class="summary-headline"><span class="dot ' + glyphClass(detail.glyph) + '"></span
|
|
2562
|
+
'<div class="summary-headline"><span class="dot ' + glyphClass(detail.glyph) + '"></span>' + (detail.status && detail.status.word ? '<b class="st-word" data-testid="summary-status">' + escapeXml(detail.status.word) + '</b> · ' : '') + '<b>' + escapeXml(detail.checkType) + '</b> · ' +
|
|
1941
2563
|
'<span class="num">' + spendText + '</span> · <span class="num">' + wallText + '</span></div>' +
|
|
1942
2564
|
'<div class="summary-row" data-testid="summary-model"><span class="sum-label">model:</span><span class="sum-val">' + escapeXml(modelLine(detail)) + '</span></div>' +
|
|
1943
2565
|
(detail.judgeModel ? '<div class="summary-row" data-testid="summary-judge"><span class="sum-label">judge:</span><span class="sum-val">' + escapeXml(detail.judgeModel) + '</span></div>' : '') +
|
|
@@ -1945,7 +2567,7 @@
|
|
|
1945
2567
|
(detail.replans ? ' · ' + detail.replans + ' replan' + (detail.replans === 1 ? '' : 's') : '') + '</span></div>' +
|
|
1946
2568
|
'<div class="summary-row"><span class="sum-label">why:</span><span class="sum-val">' + escapeXml(detail.stopReason || (detail.glyph === "✓" ? "run ended clean" : "not recorded")) + '</span></div>' +
|
|
1947
2569
|
'<div class="summary-row"><span class="sum-label">close:</span><span class="sum-val">' + escapeXml(detail.close && detail.close.verdict ? detail.close.verdict : "none — the run ended before any close ran") + '</span></div>' +
|
|
1948
|
-
'<div class="summary-row" data-testid="summary-tools"><span class="sum-label">tools:</span><span class="sum-val">' + escapeXml(toolsLine(detail.behaviour)) + '</span></div>' +
|
|
2570
|
+
'<div class="summary-row" data-testid="summary-tools"><span class="sum-label">tools:</span><span class="sum-val">' + escapeXml(detail.fromBridge === true ? bridgeToolsLine(detail.toolsUsed) : toolsLine(detail.behaviour)) + '</span></div>' +
|
|
1949
2571
|
'<div class="summary-row" data-testid="summary-offered" id="summary-offered-row" hidden><span class="sum-label">offered:</span><span class="sum-val" id="summary-offered-val"></span></div>' +
|
|
1950
2572
|
'<div class="summary-row" data-testid="summary-cache"><span class="sum-label">cache:</span><span class="sum-val">' + escapeXml(cacheLine(detail.memoryCache)) + '</span></div>';
|
|
1951
2573
|
|
|
@@ -1968,7 +2590,6 @@
|
|
|
1968
2590
|
listEl.innerHTML = "";
|
|
1969
2591
|
orderedBoxes.forEach(function(box, idx){
|
|
1970
2592
|
var part = box.part;
|
|
1971
|
-
var badgeClass = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
1972
2593
|
var titleText = box.kind === "step" ? ((cardStepNumbers[idx] + 1) + ' · ' + escapeXml(box.title)) : escapeXml(box.title);
|
|
1973
2594
|
var resultGlyph = partResultGlyph(part, box);
|
|
1974
2595
|
// build item 4: ONE owner (partLine1Text, shared with the Audit tab's
|
|
@@ -1983,7 +2604,7 @@
|
|
|
1983
2604
|
card.setAttribute("role", "button");
|
|
1984
2605
|
card.innerHTML =
|
|
1985
2606
|
'<div class="step-head"><h4>' + titleText + '</h4>' +
|
|
1986
|
-
|
|
2607
|
+
stepStateHTML(box, 'part-state-' + idx) +
|
|
1987
2608
|
(box.blocked > 0 ? '<span class="badge red" data-testid="part-blocked-badge">' + box.blocked + ' blocked</span>' : '') +
|
|
1988
2609
|
'</div>' +
|
|
1989
2610
|
'<div class="step-meta" title="a part is one piece of the run (scout, plan, a step, replan, fix, judge); a call is one model round">' + line1Bits.join(" · ") + '</div>' +
|
|
@@ -2002,9 +2623,15 @@
|
|
|
2002
2623
|
// for the map+cards, never a second independently-fetched data source
|
|
2003
2624
|
// (the flat one-row-per-call table still comes from its own `/audit`
|
|
2004
2625
|
// fetch, wired separately in selectRun below).
|
|
2626
|
+
lastLegDividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
2005
2627
|
renderAuditGroups(detail);
|
|
2006
2628
|
}
|
|
2007
2629
|
|
|
2630
|
+
// 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
|
|
2631
|
+
function stepStateHTML(box, testid){
|
|
2632
|
+
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2633
|
+
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(box.state) + '</b></span>';
|
|
2634
|
+
}
|
|
2008
2635
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2009
2636
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
2010
2637
|
function buildAttemptsList(container, partIndex, attempts, boxIsLive){
|
|
@@ -2189,6 +2816,8 @@
|
|
|
2189
2816
|
var latestAttempt = Array.isArray(part.attempts) && part.attempts.length ? part.attempts[part.attempts.length - 1] : null;
|
|
2190
2817
|
var headline = latestAttempt ? attemptChecksLine(latestAttempt) : "";
|
|
2191
2818
|
if(headline) bits.push(headline);
|
|
2819
|
+
// P5-R: a part its leg's halt cut off says why, in words
|
|
2820
|
+
if(part && part.stopReason) bits.push("stopped — " + escapeXml(part.stopReason));
|
|
2192
2821
|
return bits.join(" · ");
|
|
2193
2822
|
}
|
|
2194
2823
|
/**
|
|
@@ -2355,10 +2984,12 @@
|
|
|
2355
2984
|
var isLiveRun = detail.glyph === "▶" && !detail.died;
|
|
2356
2985
|
var boxes = buildOrderedBoxes(parts, isLiveRun);
|
|
2357
2986
|
var numberMap = stepNumberIndices(boxes);
|
|
2987
|
+
// P5-R: a resumed run is ONE run — its legs are told apart by a divider line (code-owned text from the server)
|
|
2988
|
+
var dividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
2358
2989
|
boxes.forEach(function(box, idx){
|
|
2990
|
+
dividers.forEach(function(dv){ if(dv.beforePart === idx) container.appendChild(legDividerEl(dv)); });
|
|
2359
2991
|
var part = box.part;
|
|
2360
2992
|
var titleText = box.kind === "step" ? ((numberMap[idx] + 1) + ' · ' + escapeXml(box.title)) : escapeXml(box.title);
|
|
2361
|
-
var badgeClass = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2362
2993
|
var rowWrap = document.createElement("div");
|
|
2363
2994
|
rowWrap.className = "audit-part";
|
|
2364
2995
|
rowWrap.setAttribute("data-testid", "audit-part-" + idx);
|
|
@@ -2372,7 +3003,7 @@
|
|
|
2372
3003
|
headBtn.setAttribute("data-testid", "audit-part-toggle-" + idx);
|
|
2373
3004
|
var groupResultGlyph = partResultGlyph(part, box);
|
|
2374
3005
|
headBtn.innerHTML =
|
|
2375
|
-
|
|
3006
|
+
stepStateHTML(box, 'audit-part-state-' + idx) + ' ' + titleText + ' · ' + partLine1Text(part, detail) +
|
|
2376
3007
|
(groupResultGlyph !== null ? ' · ' + groupResultGlyph : '') +
|
|
2377
3008
|
(box.blocked > 0 ? ' <span class="badge red" data-testid="audit-part-blocked-' + idx + '">' + box.blocked + ' blocked</span>' : '');
|
|
2378
3009
|
var body = document.createElement("div");
|
|
@@ -2415,8 +3046,17 @@
|
|
|
2415
3046
|
container.appendChild(rowWrap);
|
|
2416
3047
|
if(startExpanded && !built) buildBody(); // a live run's currently-running part opens by default
|
|
2417
3048
|
});
|
|
3049
|
+
// a leg that has not started a part yet (or died before one) closes the list
|
|
3050
|
+
dividers.forEach(function(dv){ if(dv.beforePart >= boxes.length) container.appendChild(legDividerEl(dv)); });
|
|
2418
3051
|
applyAuditFilter(currentAuditFilter);
|
|
2419
3052
|
}
|
|
3053
|
+
function legDividerEl(dv){
|
|
3054
|
+
var d = document.createElement("div");
|
|
3055
|
+
d.className = "audit-leg-divider";
|
|
3056
|
+
d.setAttribute("data-testid", "audit-leg-divider-" + dv.leg);
|
|
3057
|
+
d.textContent = "\u2500\u2500 " + dv.text + " \u2500\u2500";
|
|
3058
|
+
return d;
|
|
3059
|
+
}
|
|
2420
3060
|
// build item B: a Run-tab card (or map box) click switches to the Audit
|
|
2421
3061
|
// tab, that same part's row expanded and scrolled into view — sharing
|
|
2422
3062
|
// partIndex so the two tabs can never disagree about which row a click maps to.
|
|
@@ -2439,6 +3079,9 @@
|
|
|
2439
3079
|
|
|
2440
3080
|
// ---- Audit tab: FLAT view (the old one-row-per-call time-ordered table) ----
|
|
2441
3081
|
var auditRowEls = [];
|
|
3082
|
+
// the open run's leg dividers (set by renderRun); the flat table places them by time
|
|
3083
|
+
var lastLegDividers = [];
|
|
3084
|
+
function dividerAtMs(dv){ var t = Date.parse(dv.at); return Number.isFinite(t) ? t : Infinity; }
|
|
2442
3085
|
function renderAudit(result){
|
|
2443
3086
|
var empty = document.getElementById("audit-empty");
|
|
2444
3087
|
var content = document.getElementById("audit-content");
|
|
@@ -2455,7 +3098,18 @@
|
|
|
2455
3098
|
content.hidden = false;
|
|
2456
3099
|
var tbody = document.getElementById("audit-tbody");
|
|
2457
3100
|
tbody.innerHTML = "";
|
|
3101
|
+
// P5-R: the flat table carries the same leg dividers, placed by time (a divider sits before the first call
|
|
3102
|
+
// made at or after the moment that leg picked up; any that remain close the table)
|
|
3103
|
+
var pendingDividers = Array.isArray(lastLegDividers) ? lastLegDividers.slice() : [];
|
|
3104
|
+
function flatDividerRow(dv){
|
|
3105
|
+
var dividerRow = document.createElement("tr");
|
|
3106
|
+
dividerRow.className = "audit-leg-divider-row";
|
|
3107
|
+
dividerRow.setAttribute("data-testid", "audit-leg-divider-row-" + dv.leg);
|
|
3108
|
+
dividerRow.innerHTML = '<td colspan="6">\u2500\u2500 ' + escapeXml(dv.text) + ' \u2500\u2500</td>';
|
|
3109
|
+
return dividerRow;
|
|
3110
|
+
}
|
|
2458
3111
|
result.rows.forEach(function(r){
|
|
3112
|
+
while(pendingDividers.length && dividerAtMs(pendingDividers[0]) <= Date.parse(r.time)){ tbody.appendChild(flatDividerRow(pendingDividers.shift())); }
|
|
2459
3113
|
var tr = document.createElement("tr");
|
|
2460
3114
|
var rowCls = auditRowClassName(r);
|
|
2461
3115
|
if(rowCls) tr.className = rowCls;
|
|
@@ -2486,8 +3140,9 @@
|
|
|
2486
3140
|
"<td>" + auditTimeCellHtml(r) + "</td>";
|
|
2487
3141
|
tbody.appendChild(tr);
|
|
2488
3142
|
});
|
|
3143
|
+
pendingDividers.forEach(function(dv){ tbody.appendChild(flatDividerRow(dv)); });
|
|
2489
3144
|
document.getElementById("audit-rawlog").textContent = result.raw || "";
|
|
2490
|
-
auditRowEls = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
|
|
3145
|
+
auditRowEls = Array.prototype.slice.call(tbody.querySelectorAll("tr:not(.audit-leg-divider-row)"));
|
|
2491
3146
|
// Scoped to [data-filter] — the audit tab's OWN chips (chip-all/writes/
|
|
2492
3147
|
// blocked). A bare ".chip" here also matched the History filter bar's
|
|
2493
3148
|
// chips (same class, different data- attributes), binding a second
|
|
@@ -2517,6 +3172,7 @@
|
|
|
2517
3172
|
function renderJob(job){
|
|
2518
3173
|
var empty = document.getElementById("job-empty");
|
|
2519
3174
|
var card = document.getElementById("job-card-readonly");
|
|
3175
|
+
lastJobShown = job;
|
|
2520
3176
|
if(!job){ empty.hidden = false; card.hidden = true; return; }
|
|
2521
3177
|
empty.hidden = true;
|
|
2522
3178
|
card.hidden = false;
|
|
@@ -2539,6 +3195,7 @@
|
|
|
2539
3195
|
document.getElementById("job-note").textContent = job.note || "";
|
|
2540
3196
|
lastJobToolsList = Array.isArray(job.toolsList) ? job.toolsList : null;
|
|
2541
3197
|
paintOfferedRow();
|
|
3198
|
+
paintResumeMode();
|
|
2542
3199
|
}
|
|
2543
3200
|
|
|
2544
3201
|
// ---- selection + loading ----
|
|
@@ -2553,10 +3210,13 @@
|
|
|
2553
3210
|
// uses for `isLive` — never re-derived a second way).
|
|
2554
3211
|
var runIsLive = false;
|
|
2555
3212
|
function selectRun(runid, rowEl, siblingSelector){
|
|
3213
|
+
hideImportView();
|
|
2556
3214
|
clearSelection();
|
|
2557
3215
|
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
2558
3216
|
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
2559
3217
|
currentRunid = runid;
|
|
3218
|
+
resumeMode = null;
|
|
3219
|
+
lastLegDividers = [];
|
|
2560
3220
|
lastJobToolsList = null;
|
|
2561
3221
|
lastRunDetailSig = null;
|
|
2562
3222
|
runIsLive = false;
|
|
@@ -2818,7 +3478,7 @@
|
|
|
2818
3478
|
}
|
|
2819
3479
|
function loadProviders(){
|
|
2820
3480
|
authorGet("/api/settings/providers").then(function(r){
|
|
2821
|
-
if(!r || !r.ok){ pvNote.textContent = "could not read providers"; return; }
|
|
3481
|
+
if(!r || !r.ok){ pvNote.textContent = "could not read providers"; document.getElementById("pv-keyfile-path").textContent = "the keys file (path unknown — could not read providers)"; return; }
|
|
2822
3482
|
var kf = r.keysFile;
|
|
2823
3483
|
document.getElementById("pv-keyfile-path").textContent = kf.path;
|
|
2824
3484
|
document.getElementById("pv-keyfile-note").textContent = kf.warning ? ("⚠ " + kf.warning) : "";
|
|
@@ -2910,24 +3570,21 @@
|
|
|
2910
3570
|
|
|
2911
3571
|
// ---- Chat tab: job-card authoring (PANEL-BUILD.md P3) ----
|
|
2912
3572
|
(function(){
|
|
2913
|
-
var
|
|
3573
|
+
var clearBtn = document.getElementById("chat-clear-btn");
|
|
2914
3574
|
var card = document.getElementById("job-card");
|
|
2915
3575
|
var thread = document.getElementById("chat-thread");
|
|
2916
3576
|
var msgInput = document.getElementById("chat-msg");
|
|
2917
|
-
var
|
|
2918
|
-
var
|
|
2919
|
-
var
|
|
2920
|
-
var
|
|
2921
|
-
var
|
|
3577
|
+
var mainBtn = document.getElementById("chat-main-btn");
|
|
3578
|
+
var mainHint = document.getElementById("chat-main-hint");
|
|
3579
|
+
var mainAction = "none";
|
|
3580
|
+
var lastState = null;
|
|
3581
|
+
var startOk = false;
|
|
2922
3582
|
var errEl = document.getElementById("chat-card-error");
|
|
2923
3583
|
var actionErrEl = document.getElementById("chat-action-error");
|
|
2924
|
-
var hashLine = document.getElementById("chat-hash-line");
|
|
2925
3584
|
var moneyCapInput = document.getElementById("jf-cap-money");
|
|
2926
3585
|
var modelSelect = document.getElementById("jf-model");
|
|
2927
3586
|
var modelStatusEl = document.getElementById("jf-model-status");
|
|
2928
3587
|
var progressRow = document.getElementById("chat-progress-row");
|
|
2929
|
-
var progressGlyph = document.getElementById("chat-progress-glyph");
|
|
2930
|
-
var progressLabelEl = document.getElementById("chat-progress-label");
|
|
2931
3588
|
|
|
2932
3589
|
var sessionId = null;
|
|
2933
3590
|
var pollTimer = null;
|
|
@@ -2939,26 +3596,98 @@
|
|
|
2939
3596
|
// src/panel/authorroutes.js, mirrored here so Start never even offers a
|
|
2940
3597
|
// click the server would 409 anyway).
|
|
2941
3598
|
var CLIENT_TERMINAL_PHASES = ["refused", "abandoned", "error", "signed", "signing-failed"];
|
|
2942
|
-
var DONE_PHASES = ["prepared", "signed"];
|
|
2943
3599
|
var sessionLive = false;
|
|
2944
3600
|
// build item 7 — Start also stays disabled while the chosen model's key
|
|
2945
3601
|
// isn't usable; updated by refreshModelStatus() below.
|
|
2946
3602
|
var keyOk = false;
|
|
2947
3603
|
|
|
2948
|
-
|
|
3604
|
+
// Reuse workflow — set when the card was opened by [Reuse workflow]: the run (or imported job) it came from.
|
|
3605
|
+
// Only Source, Destination and the two caps are open on that card; the SERVER refuses any other change.
|
|
3606
|
+
var startFrom = null;
|
|
3607
|
+
var reuseSession = false;
|
|
3608
|
+
var autoSigned = false;
|
|
3609
|
+
var sfOrigin = document.getElementById("chat-startfrom-origin");
|
|
3610
|
+
var sfLine = document.getElementById("chat-startfrom-line");
|
|
3611
|
+
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3612
|
+
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3613
|
+
var LOCKED_IDS = ["jf-name", "jf-goal", "jf-success", "jf-guardrails", "jf-judge"];
|
|
3614
|
+
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3615
|
+
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3616
|
+
var OPEN_IDS = ["jf-source", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3617
|
+
var reuseOn = false;
|
|
3618
|
+
function syncCardLock(){
|
|
3619
|
+
var live = sessionLive;
|
|
3620
|
+
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3621
|
+
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live); });
|
|
3622
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live); });
|
|
3623
|
+
document.getElementById("jf-model").disabled = live;
|
|
3624
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = reuseOn || live; });
|
|
3625
|
+
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3626
|
+
if(!reuseOn && !live){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3627
|
+
}
|
|
3628
|
+
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3629
|
+
function clearStartFrom(){
|
|
3630
|
+
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes
|
|
3631
|
+
if(startFrom){
|
|
3632
|
+
["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"].forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3633
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3634
|
+
}
|
|
3635
|
+
startFrom = null; reuseSession = false; autoSigned = false;
|
|
3636
|
+
sfOrigin.hidden = true; sfLine.hidden = true; sfNote.hidden = true;
|
|
3637
|
+
setReuseLocked(false);
|
|
3638
|
+
renderMain();
|
|
3639
|
+
}
|
|
3640
|
+
function openNewCard(){
|
|
2949
3641
|
card.hidden = false;
|
|
2950
|
-
newBtn.hidden = true;
|
|
2951
3642
|
thread.innerHTML = "";
|
|
2952
3643
|
errEl.textContent = "";
|
|
2953
3644
|
actionErrEl.textContent = "";
|
|
2954
3645
|
sessionId = null;
|
|
2955
3646
|
sessionLive = false;
|
|
2956
3647
|
signClickedOnce = false;
|
|
3648
|
+
reuseSession = false;
|
|
3649
|
+
autoSigned = false;
|
|
2957
3650
|
if(pollTimer){ clearInterval(pollTimer); pollTimer = null; }
|
|
2958
|
-
stopProgressDots();
|
|
2959
3651
|
progressRow.hidden = true;
|
|
2960
|
-
|
|
3652
|
+
progressRow.innerHTML = "";
|
|
3653
|
+
lastState = null;
|
|
2961
3654
|
refreshStartEnabled();
|
|
3655
|
+
}
|
|
3656
|
+
// hamr's ruling 2026-10-05: ONE button, two states. A live, unsigned session -> "Abandon" (the server ends it and
|
|
3657
|
+
// releases the one-at-a-time lock; money already spent stays booked). Anything else (signed and running, terminal,
|
|
3658
|
+
// nothing started) -> "Clear", which empties the card only and never stops a run (Stop lives on the Run tab).
|
|
3659
|
+
function clearButtonFor(live, hasId){
|
|
3660
|
+
return live ? {text: "Abandon", disabled: !hasId} : {text: "Clear", disabled: false};
|
|
3661
|
+
}
|
|
3662
|
+
function refreshClearBtn(){
|
|
3663
|
+
var b = clearButtonFor(sessionLive, !!sessionId);
|
|
3664
|
+
clearBtn.textContent = b.text;
|
|
3665
|
+
clearBtn.disabled = b.disabled;
|
|
3666
|
+
}
|
|
3667
|
+
// back to a fresh empty card: every box, check type deterministic, the default (first) Model, origin/estimate/notes
|
|
3668
|
+
// hidden, progress list and thread cleared, reuse lock lifted
|
|
3669
|
+
function resetCard(){
|
|
3670
|
+
["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"].forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3671
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3672
|
+
document.getElementById("jf-judge").disabled = true;
|
|
3673
|
+
modelSelect.selectedIndex = 0;
|
|
3674
|
+
msgInput.value = "";
|
|
3675
|
+
document.getElementById("jf-cap-note").textContent = "";
|
|
3676
|
+
lastPhase = null;
|
|
3677
|
+
clearStartFrom();
|
|
3678
|
+
openNewCard();
|
|
3679
|
+
refreshModelStatus();
|
|
3680
|
+
}
|
|
3681
|
+
clearBtn.addEventListener("click", function(){
|
|
3682
|
+
if(clearBtn.textContent === "Abandon" && sessionId){
|
|
3683
|
+
authorPost("/api/author/" + sessionId + "/abandon", {}).then(function(r){
|
|
3684
|
+
var o = chatPostOutcome(r);
|
|
3685
|
+
if(!o.ok){ chatActionFailed(o.error); return; }
|
|
3686
|
+
resetCard();
|
|
3687
|
+
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not abandoned"); });
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
resetCard();
|
|
2962
3691
|
});
|
|
2963
3692
|
|
|
2964
3693
|
function currentCard(){
|
|
@@ -2989,7 +3718,10 @@
|
|
|
2989
3718
|
function refreshStartEnabled(){
|
|
2990
3719
|
var v = Number(moneyCapInput.value);
|
|
2991
3720
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
2992
|
-
|
|
3721
|
+
startOk = capOk && !sessionLive && keyOk;
|
|
3722
|
+
syncCardLock();
|
|
3723
|
+
renderMain();
|
|
3724
|
+
refreshClearBtn();
|
|
2993
3725
|
}
|
|
2994
3726
|
moneyCapInput.addEventListener("input", refreshStartEnabled);
|
|
2995
3727
|
// P4a — the monthly-limit note sits right under the cap, as the person types.
|
|
@@ -3080,6 +3812,16 @@
|
|
|
3080
3812
|
});
|
|
3081
3813
|
}
|
|
3082
3814
|
modelSelect.addEventListener("change", refreshModelStatus);
|
|
3815
|
+
// Model is open on a reuse card: the estimate line counts only runs on the worker the box names right now
|
|
3816
|
+
function refreshReuseLine(){
|
|
3817
|
+
if(!startFrom) return;
|
|
3818
|
+
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3819
|
+
"&model=" + encodeURIComponent(modelSelect.value);
|
|
3820
|
+
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3821
|
+
if(startFrom && r && r.ok){ sfLine.textContent = r.line || ""; }
|
|
3822
|
+
}).catch(function(){ /* the line keeps what it had */ });
|
|
3823
|
+
}
|
|
3824
|
+
modelSelect.addEventListener("change", refreshReuseLine);
|
|
3083
3825
|
// P4b — the Model menu IS the Settings rows: every Name, in keys-file order (a blank Name
|
|
3084
3826
|
// is never offered). Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
3085
3827
|
function loadModels(){
|
|
@@ -3119,25 +3861,6 @@
|
|
|
3119
3861
|
thread.scrollTop = thread.scrollHeight;
|
|
3120
3862
|
}
|
|
3121
3863
|
|
|
3122
|
-
// build item 3 — the single live progress line: the LABEL sits first in
|
|
3123
|
-
// the markup, then the animated indicator (never the other way — the
|
|
3124
|
-
// dots must not push the label around as they cycle 1..4).
|
|
3125
|
-
var progressDotTimer = null;
|
|
3126
|
-
var progressDotCount = 1;
|
|
3127
|
-
var reducedMotion = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
3128
|
-
function stopProgressDots(){
|
|
3129
|
-
if(progressDotTimer){ clearInterval(progressDotTimer); progressDotTimer = null; }
|
|
3130
|
-
}
|
|
3131
|
-
function startProgressDots(){
|
|
3132
|
-
if(reducedMotion){ progressGlyph.textContent = "[progress....]"; return; }
|
|
3133
|
-
if(progressDotTimer) return;
|
|
3134
|
-
progressDotCount = 1;
|
|
3135
|
-
progressGlyph.textContent = "[progress.]";
|
|
3136
|
-
progressDotTimer = setInterval(function(){
|
|
3137
|
-
progressDotCount = (progressDotCount % 4) + 1;
|
|
3138
|
-
progressGlyph.textContent = "[progress" + new Array(progressDotCount + 1).join(".") + "]";
|
|
3139
|
-
}, 500);
|
|
3140
|
-
}
|
|
3141
3864
|
// build item 3 — pure: what the label must say for a given state. A
|
|
3142
3865
|
// person waiting on their OWN next action (an ask is pending) must never
|
|
3143
3866
|
// see a stale machine step name (observed: "checking prior behaviour"
|
|
@@ -3150,75 +3873,148 @@
|
|
|
3150
3873
|
if(pending) return "waiting for your answer";
|
|
3151
3874
|
return (state && state.progressLabel) || "working";
|
|
3152
3875
|
}
|
|
3876
|
+
// the ONE progress list: a step per line, in the server's order (STEP_LABELS in src/panel/authorsession.js owns
|
|
3877
|
+
// the ids and words). While a step runs its dots are CSS (still under prefers-reduced-motion); it ends as a check
|
|
3878
|
+
// or a cross, a cross carrying the code-owned reason on the same line. A pending ask for the person replaces the
|
|
3879
|
+
// running step's label with what is being waited on, never an animation.
|
|
3880
|
+
// Returns true when the list's content actually changed (a new line, or the latest line changed) so the caller can
|
|
3881
|
+
// scroll only then; an identical 2s poll tick returns false and leaves the person's scroll position alone.
|
|
3153
3882
|
function renderProgress(state){
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
progressLabelEl.textContent = progressLabelFor(state);
|
|
3168
|
-
startProgressDots();
|
|
3169
|
-
}
|
|
3883
|
+
var steps = (state && state.steps) || [];
|
|
3884
|
+
var before = progressRow.innerHTML;
|
|
3885
|
+
progressRow.hidden = steps.length === 0;
|
|
3886
|
+
progressRow.innerHTML = steps.map(function(st, i){
|
|
3887
|
+
var waiting = st.status === "running" && i === steps.length - 1 && state.pendingAsk;
|
|
3888
|
+
var cls = waiting ? "wait" : (st.status === "running" ? "run" : (st.status === "failed" ? "bad" : "ok"));
|
|
3889
|
+
var sign = cls === "ok" ? "\u2713" : (cls === "bad" ? "\u2717" : "");
|
|
3890
|
+
var label = waiting ? progressLabelFor(state) : st.label;
|
|
3891
|
+
return '<li class="step ' + cls + '" data-step="' + escapeXml(st.id) + '"><div class="step-line"><span class="step-label">' + escapeXml(label) + '</span>' +
|
|
3892
|
+
'<span class="step-sign">' + sign + '</span></div>' +
|
|
3893
|
+
(st.detail ? '<div class="step-detail">' + escapeXml("> " + st.detail) + '</div>' : "") + '</li>';
|
|
3894
|
+
}).join("");
|
|
3895
|
+
return progressRow.innerHTML !== before;
|
|
3170
3896
|
}
|
|
3171
3897
|
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
signBtn.disabled = false;
|
|
3183
|
-
signBtn.textContent = "Sign & run";
|
|
3184
|
-
} else if(state.phase === "prepared" && signClickedOnce){
|
|
3185
|
-
signBtn.disabled = false;
|
|
3186
|
-
signBtn.textContent = "Sign " + (state.specHash || "").slice(0, 8) + " & run";
|
|
3187
|
-
} else {
|
|
3188
|
-
signBtn.disabled = !atMenu;
|
|
3189
|
-
signBtn.textContent = "Sign & run";
|
|
3898
|
+
// hamr's ruling 2026-10-05: ONE wide button below the message box. Its wording, action and enabled state are ONE
|
|
3899
|
+
// pure function of (state, typed-text-empty, reuse); state = {session, startOk, phase, pendingAsk, revisesLeft,
|
|
3900
|
+
// signClicked, specHash}. Actions: start | check-deps | send | revise | sign-prepare | sign | none.
|
|
3901
|
+
function mainButtonFor(st, textEmpty, reuse){
|
|
3902
|
+
var ask = st.pendingAsk;
|
|
3903
|
+
var atMenu = !!ask && ask.kind === "menu";
|
|
3904
|
+
var left = st.revisesLeft || 0;
|
|
3905
|
+
var hint = atMenu ? left + (left === 1 ? " change left" : " changes left") : "";
|
|
3906
|
+
if(!st.session){
|
|
3907
|
+
return {text: reuse ? "Sign & run" : "Start drafting", action: "start", disabled: !st.startOk, hint: ""};
|
|
3190
3908
|
}
|
|
3191
|
-
if(
|
|
3192
|
-
|
|
3193
|
-
hashLine.textContent = "spec hash: " + state.specHash;
|
|
3909
|
+
if(ask && ask.kind === "install-needed"){
|
|
3910
|
+
return {text: "Check again", action: "check-deps", disabled: false, hint: ""};
|
|
3194
3911
|
}
|
|
3195
|
-
if(
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3912
|
+
if(ask && !textEmpty){
|
|
3913
|
+
return atMenu
|
|
3914
|
+
? {text: "Send", action: "revise", disabled: left <= 0, hint: hint}
|
|
3915
|
+
: {text: "Send", action: "send", disabled: false, hint: ""};
|
|
3199
3916
|
}
|
|
3200
|
-
|
|
3917
|
+
if(ask && !atMenu){
|
|
3918
|
+
return {text: "Send", action: "send", disabled: true, hint: ""};
|
|
3919
|
+
}
|
|
3920
|
+
if(atMenu || st.phase === "prepared"){
|
|
3921
|
+
if(st.phase === "prepared" && st.signClicked){
|
|
3922
|
+
return {text: "Sign " + (st.specHash || "").slice(0, 8) + " & run", action: "sign", disabled: false, hint: hint};
|
|
3923
|
+
}
|
|
3924
|
+
return {text: "Sign & run", action: "sign-prepare", disabled: false, hint: hint};
|
|
3925
|
+
}
|
|
3926
|
+
return {text: "Sign & run", action: "none", disabled: true, hint: ""};
|
|
3927
|
+
}
|
|
3928
|
+
// pure: the ask box takes typed text only when the session has a pending ask that accepts it (the plan menu -> a
|
|
3929
|
+
// change request, or any question); not with no session, no ask, or while waiting on an install
|
|
3930
|
+
function askBoxOpenFor(st){
|
|
3931
|
+
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind !== "install-needed";
|
|
3932
|
+
}
|
|
3933
|
+
function renderMain(){
|
|
3934
|
+
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce};
|
|
3935
|
+
if(sessionLive && lastState){
|
|
3936
|
+
st.phase = lastState.phase; st.pendingAsk = lastState.pendingAsk;
|
|
3937
|
+
st.revisesLeft = lastState.revisesLeft; st.specHash = lastState.specHash;
|
|
3938
|
+
}
|
|
3939
|
+
// hamr 2026-10-05: the ask box is open only while something asks for a reply; otherwise dimmed and emptied
|
|
3940
|
+
var open = askBoxOpenFor(st);
|
|
3941
|
+
msgInput.disabled = !open;
|
|
3942
|
+
if(!open) msgInput.value = "";
|
|
3943
|
+
var b = mainButtonFor(st, !msgInput.value.trim(), !!startFrom);
|
|
3944
|
+
mainBtn.textContent = b.text;
|
|
3945
|
+
mainBtn.disabled = b.disabled;
|
|
3946
|
+
mainAction = b.action;
|
|
3947
|
+
mainHint.textContent = b.hint;
|
|
3948
|
+
fitBox(msgInput);
|
|
3949
|
+
fitCardBoxes();
|
|
3950
|
+
}
|
|
3951
|
+
msgInput.addEventListener("input", renderMain);
|
|
3952
|
+
// grow a text box with its text (a few rows, then it scrolls inside); ONE function for the ask box and the card boxes.
|
|
3953
|
+
// A box that is hidden (or not laid out) measures 0 and gets no inline height.
|
|
3954
|
+
function fitBox(el){
|
|
3955
|
+
el.style.height = "auto";
|
|
3956
|
+
if(!el.scrollHeight){ el.style.height = ""; return; }
|
|
3957
|
+
var border = el.offsetHeight - el.clientHeight;
|
|
3958
|
+
el.style.height = Math.min(el.scrollHeight + border, 200) + "px";
|
|
3959
|
+
}
|
|
3960
|
+
function fitCardBoxes(){
|
|
3961
|
+
["jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge"].forEach(function(id){ fitBox(document.getElementById(id)); });
|
|
3962
|
+
}
|
|
3963
|
+
window.addEventListener("resize", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
3964
|
+
document.getElementById("tab-chat").addEventListener("click", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
3965
|
+
// Enter sends through the one main button; Shift+Enter (or IME composition) keeps its newline
|
|
3966
|
+
msgInput.addEventListener("keydown", function(e){
|
|
3967
|
+
if(e.key === "Enter" && !e.shiftKey && !e.isComposing){
|
|
3968
|
+
e.preventDefault();
|
|
3969
|
+
if(mainAction !== "start") mainBtn.click(); // never starts a (paid) draft from a stray Enter
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
function renderActions(state){
|
|
3974
|
+
lastState = state;
|
|
3975
|
+
// a session's stop is shown ONCE, in the progress line (renderProgress); #chat-card-error carries only a
|
|
3976
|
+
// refused START (its own handler writes it) — echoing state.error here made the same refusal appear twice
|
|
3977
|
+
errEl.textContent = "";
|
|
3978
|
+
var progressChanged = renderProgress(state);
|
|
3201
3979
|
// build item 4 — re-enable Start only once the session has settled
|
|
3202
3980
|
// into a terminal phase (a fresh Start becomes possible again).
|
|
3203
3981
|
sessionLive = CLIENT_TERMINAL_PHASES.indexOf(state.phase) === -1;
|
|
3204
3982
|
refreshStartEnabled();
|
|
3983
|
+
// hamr 2026-10-05: while the session is live and the list changed, bring the newest progress line into view.
|
|
3984
|
+
// Never moves keyboard focus, and an unchanged poll tick never scrolls.
|
|
3985
|
+
if(progressChanged && sessionLive && progressRow.lastElementChild){
|
|
3986
|
+
progressRow.lastElementChild.scrollIntoView({block: "nearest"});
|
|
3987
|
+
}
|
|
3205
3988
|
}
|
|
3206
3989
|
|
|
3207
3990
|
function poll(){
|
|
3208
3991
|
if(!sessionId) return;
|
|
3992
|
+
var polledId = sessionId;
|
|
3209
3993
|
authorGet("/api/author/" + sessionId).then(function(j){
|
|
3210
|
-
if(!j || !j.state) return;
|
|
3994
|
+
if(!j || !j.state || polledId !== sessionId) return;
|
|
3211
3995
|
renderMessages(j.state);
|
|
3212
3996
|
renderActions(j.state);
|
|
3997
|
+
if(reuseSession && !autoSigned && j.state.phase === "prepared" && j.state.specHash){
|
|
3998
|
+
autoSigned = true;
|
|
3999
|
+
authorPost("/api/author/" + sessionId + "/sign", {specHash: j.state.specHash}).then(function(r){
|
|
4000
|
+
var o = chatPostOutcome(r);
|
|
4001
|
+
if(o.ok){ chatActionOk(); } else { autoSigned = false; chatActionFailed(o.error); }
|
|
4002
|
+
poll();
|
|
4003
|
+
}).catch(function(){ autoSigned = false; chatActionFailed("network error — could not reach the panel server, not signed"); });
|
|
4004
|
+
}
|
|
3213
4005
|
if(j.state.phase === "signed"){
|
|
3214
4006
|
clearInterval(pollTimer);
|
|
3215
4007
|
document.getElementById("tab-runs").click();
|
|
4008
|
+
// hamr's ruling 2026-10-05: the run has started, so the Chat card goes back to the empty card (the one reset
|
|
4009
|
+
// Clear uses) — it must not keep showing a signed job that invites a rerun. The run itself is untouched.
|
|
4010
|
+
resetCard();
|
|
4011
|
+
return;
|
|
3216
4012
|
}
|
|
3217
4013
|
lastPhase = j.state.phase;
|
|
3218
4014
|
}).catch(function(){ /* transient — next poll tries again */ });
|
|
3219
4015
|
}
|
|
3220
4016
|
|
|
3221
|
-
|
|
4017
|
+
function doStart(){
|
|
3222
4018
|
errEl.textContent = "";
|
|
3223
4019
|
actionErrEl.textContent = "";
|
|
3224
4020
|
// build item 4 — disable the moment it's clicked, not just once the
|
|
@@ -3226,6 +4022,7 @@
|
|
|
3226
4022
|
sessionLive = true;
|
|
3227
4023
|
refreshStartEnabled();
|
|
3228
4024
|
var body = currentCard();
|
|
4025
|
+
if(startFrom) body.startFrom = startFrom.importId ? {importId: startFrom.importId} : startFrom.runid;
|
|
3229
4026
|
authorPost("/api/author/start", body).then(function(r){
|
|
3230
4027
|
if(r.status !== 200 || !r.body.ok){
|
|
3231
4028
|
errEl.textContent = (r.body && r.body.error) || "refused";
|
|
@@ -3234,12 +4031,88 @@
|
|
|
3234
4031
|
return;
|
|
3235
4032
|
}
|
|
3236
4033
|
sessionId = r.body.sessionId;
|
|
3237
|
-
|
|
4034
|
+
// a same-job session has no confirm turn: it goes straight to `prepared`, and THIS click (Sign & run) is the
|
|
4035
|
+
// signature — the page signs the hash the server prepared as soon as it is ready (the server re-checks it)
|
|
4036
|
+
reuseSession = r.body.reuse === true;
|
|
4037
|
+
autoSigned = false;
|
|
4038
|
+
signClickedOnce = reuseSession;
|
|
3238
4039
|
renderMessages(r.body.state);
|
|
3239
4040
|
renderActions(r.body.state);
|
|
3240
4041
|
if(pollTimer) clearInterval(pollTimer);
|
|
3241
4042
|
pollTimer = setInterval(poll, 2000);
|
|
3242
4043
|
});
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// Re-attach after a page load/refresh: the server holds the one live (non-terminal) authoring session, so ask it.
|
|
4047
|
+
// If one exists, this tab takes it over exactly as if it had started it: the card filled from the session's own
|
|
4048
|
+
// copy, the poll loop, progress list, thread, ask box, main button and Abandon. A signed session is terminal, so it
|
|
4049
|
+
// never comes back (the card stays empty). Nothing is stored page-side.
|
|
4050
|
+
function attachSession(j){
|
|
4051
|
+
var st = j.state, c = st.card || {};
|
|
4052
|
+
openNewCard();
|
|
4053
|
+
sessionId = j.sessionId;
|
|
4054
|
+
sessionLive = true;
|
|
4055
|
+
reuseSession = st.reuse === true;
|
|
4056
|
+
autoSigned = false;
|
|
4057
|
+
signClickedOnce = reuseSession;
|
|
4058
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === (c.checkType === "rubric" ? "rubric" : "deterministic")); });
|
|
4059
|
+
if(c.model){
|
|
4060
|
+
if(!Array.prototype.some.call(modelSelect.options, function(o){ return o.value === c.model; })){
|
|
4061
|
+
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
4062
|
+
}
|
|
4063
|
+
modelSelect.value = c.model;
|
|
4064
|
+
}
|
|
4065
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4066
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4067
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4068
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4069
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4070
|
+
if(reuseSession) setReuseLocked(true);
|
|
4071
|
+
renderMessages(st);
|
|
4072
|
+
renderActions(st);
|
|
4073
|
+
if(pollTimer) clearInterval(pollTimer);
|
|
4074
|
+
pollTimer = setInterval(poll, 2000);
|
|
4075
|
+
poll();
|
|
4076
|
+
}
|
|
4077
|
+
function reattachLive(){
|
|
4078
|
+
if(sessionId || sessionLive) return;
|
|
4079
|
+
authorGet("/api/author/live").then(function(j){
|
|
4080
|
+
if(sessionId || sessionLive || !j || !j.ok || !j.sessionId || !j.state) return;
|
|
4081
|
+
attachSession(j);
|
|
4082
|
+
}).catch(function(){ /* nothing to attach to — the empty card stays */ });
|
|
4083
|
+
}
|
|
4084
|
+
document.getElementById("tab-chat").addEventListener("click", reattachLive);
|
|
4085
|
+
reattachLive();
|
|
4086
|
+
|
|
4087
|
+
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4088
|
+
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4089
|
+
document.addEventListener("bareloop-reuse", function(e){
|
|
4090
|
+
var d = e.detail || {};
|
|
4091
|
+
var pre = d.prefill || {};
|
|
4092
|
+
var c = pre.card || {};
|
|
4093
|
+
clearStartFrom();
|
|
4094
|
+
openNewCard();
|
|
4095
|
+
startFrom = d.importId ? {importId: d.importId} : {runid: d.runid};
|
|
4096
|
+
var radios = document.querySelectorAll('input[name="jf-verdict"]');
|
|
4097
|
+
radios.forEach(function(r){ r.checked = (r.value === (c.checkType === "rubric" ? "rubric" : "deterministic")); });
|
|
4098
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4099
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4100
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4101
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4102
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4103
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4104
|
+
setReuseLocked(true);
|
|
4105
|
+
sfOrigin.hidden = false;
|
|
4106
|
+
sfOrigin.textContent = "Reuse: " + ((pre.origin && pre.origin.job) || "job") + (d.runid ? " (run " + d.runid + ")" : " (imported)");
|
|
4107
|
+
sfLine.hidden = false;
|
|
4108
|
+
sfLine.textContent = pre.line || "";
|
|
4109
|
+
sfLine.className = "startfrom-line";
|
|
4110
|
+
sfNote.hidden = !pre.note;
|
|
4111
|
+
sfNote.textContent = pre.note || "";
|
|
4112
|
+
refreshModelStatus();
|
|
4113
|
+
refreshReuseLine();
|
|
4114
|
+
refreshCapNote();
|
|
4115
|
+
refreshStartEnabled();
|
|
3243
4116
|
});
|
|
3244
4117
|
|
|
3245
4118
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
@@ -3275,18 +4148,23 @@
|
|
|
3275
4148
|
function chatActionOk(){
|
|
3276
4149
|
actionErrEl.textContent = "";
|
|
3277
4150
|
}
|
|
4151
|
+
// a dead token (panel restarted): show the one line where Chat actions show errors and stop polling
|
|
4152
|
+
staleTokenHook = function(){
|
|
4153
|
+
actionErrEl.textContent = STALE_TOKEN_LINE;
|
|
4154
|
+
if(pollTimer){ clearInterval(pollTimer); pollTimer = null; }
|
|
4155
|
+
};
|
|
3278
4156
|
|
|
3279
|
-
|
|
4157
|
+
function doCheckDeps(){
|
|
3280
4158
|
if(!sessionId) return;
|
|
3281
|
-
|
|
4159
|
+
mainBtn.disabled = true;
|
|
3282
4160
|
authorPost("/api/author/" + sessionId + "/check-deps", {}).then(function(r){
|
|
3283
4161
|
var o = chatPostOutcome(r);
|
|
3284
4162
|
if(o.ok) chatActionOk(); else chatActionFailed(o.error);
|
|
3285
4163
|
poll();
|
|
3286
4164
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, nothing was sent"); });
|
|
3287
|
-
}
|
|
4165
|
+
}
|
|
3288
4166
|
|
|
3289
|
-
|
|
4167
|
+
function doSend(){
|
|
3290
4168
|
if(!sessionId) return;
|
|
3291
4169
|
var text = msgInput.value;
|
|
3292
4170
|
if(!text.trim()) return;
|
|
@@ -3302,9 +4180,9 @@
|
|
|
3302
4180
|
}
|
|
3303
4181
|
poll();
|
|
3304
4182
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
3305
|
-
}
|
|
4183
|
+
}
|
|
3306
4184
|
|
|
3307
|
-
|
|
4185
|
+
function doRevise(){
|
|
3308
4186
|
if(!sessionId) return;
|
|
3309
4187
|
var text = msgInput.value;
|
|
3310
4188
|
authorPost("/api/author/" + sessionId + "/revise", {text: text}).then(function(r){
|
|
@@ -3317,9 +4195,9 @@
|
|
|
3317
4195
|
}
|
|
3318
4196
|
poll();
|
|
3319
4197
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
3320
|
-
}
|
|
4198
|
+
}
|
|
3321
4199
|
|
|
3322
|
-
|
|
4200
|
+
function doSign(){
|
|
3323
4201
|
if(!sessionId) return;
|
|
3324
4202
|
if(!signClickedOnce){
|
|
3325
4203
|
// click 1 — the confirm turn's own "confirm" pick. Never signs.
|
|
@@ -3342,6 +4220,15 @@
|
|
|
3342
4220
|
poll();
|
|
3343
4221
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not signed"); });
|
|
3344
4222
|
});
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
mainBtn.addEventListener("click", function(){
|
|
4226
|
+
if(mainBtn.disabled) return;
|
|
4227
|
+
if(mainAction === "start") doStart();
|
|
4228
|
+
else if(mainAction === "check-deps") doCheckDeps();
|
|
4229
|
+
else if(mainAction === "send") doSend();
|
|
4230
|
+
else if(mainAction === "revise") doRevise();
|
|
4231
|
+
else if(mainAction === "sign-prepare" || mainAction === "sign") doSign();
|
|
3345
4232
|
});
|
|
3346
4233
|
|
|
3347
4234
|
refreshStartEnabled();
|