@intentius/behold 0.29.6 → 0.29.8
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/AGENTS.md +6 -1
- package/README.md +7 -0
- package/dist/cli.js +1263 -770
- package/package.json +1 -1
- package/web/app.js +320 -0
- package/web/index.html +24 -0
- package/web/kiosk-annotations.js +528 -0
- package/web/kiosk-annotations.test.js +83 -0
- package/web/kiosk-approval.js +241 -0
- package/web/kiosk-approval.test.js +122 -0
- package/web/kiosk-popover.js +235 -0
- package/web/kiosk-popover.test.js +180 -0
- package/web/kiosk-repo.js +25 -0
- package/web/kiosk-repo.test.js +43 -0
- package/web/kiosk-roots.js +199 -0
- package/web/kiosk-roots.test.js +95 -0
- package/web/kiosk-runs.js +365 -23
- package/web/kiosk-runs.test.js +77 -1
- package/web/kiosk-vendor.js +380 -0
- package/web/kiosk-vendor.test.js +265 -0
- package/web/kiosk.html +160 -17
- package/web/kiosk.js +536 -59
- package/web/kiosk.test.js +76 -11
- package/web/theme.js +112 -0
- package/web/theme.test.js +51 -1
- package/web/vendors/choudoufu-128.png +0 -0
- package/web/vendors/choudoufu-64.png +0 -0
- package/web/view-repo.js +44 -0
package/web/kiosk.js
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
// A wave that failed says which waves applied before it and stay applied.
|
|
14
14
|
// Nothing here is all-or-nothing, and nothing says so.
|
|
15
15
|
|
|
16
|
-
import { PICK_HOLD_MS, startRuns } from "./kiosk-runs.js";
|
|
16
|
+
import { PICK_HOLD_MS, annMark, mergeRun, startRuns } from "./kiosk-runs.js";
|
|
17
|
+
import { PANEL_STEPS, PANEL_TITLE, approvalModel, cycleRuns, rowFacts, shortDigest } from "./kiosk-approval.js";
|
|
18
|
+
import { getThemeName, mountThemePicker, onThemeChange } from "./theme.js";
|
|
19
|
+
import { cssString, hoverCss, hoverFromMessage, hoverLinks, hoverMessage, isLightBg, rootSegments, rootText } from "./kiosk-roots.js";
|
|
20
|
+
import { creditLines, filterIndex, keepRun, liveEstimate, liveFor, namesOther, notRunWords, replayAt, replayBanner, replayIndex, rootWord, runAsOf, scrubDemo, servesRepo, vendorOf } from "./kiosk-vendor.js";
|
|
21
|
+
import { repoParam, repoUrl } from "./kiosk-repo.js";
|
|
17
22
|
import { STATE_WORDS, changingRoots, costLine, pinsCommit, planFitsPin, pullPin, readEdges, rootsCount, totalsLine, waveRows, blastRows } from "./terragucci-change.js";
|
|
18
23
|
|
|
19
24
|
/** The scenarios terragucci#1014 numbers. An id not here is shown by the title demo.json carries. */
|
|
@@ -67,7 +72,12 @@ export function scenarioName(demo) {
|
|
|
67
72
|
* place of SCENARIOS' label when it has one; the driver's title alone for an
|
|
68
73
|
* id behold doesn't know.
|
|
69
74
|
*/
|
|
70
|
-
export function scenarioHead(demo) {
|
|
75
|
+
export function scenarioHead(demo, { vendor = null } = {}) {
|
|
76
|
+
// behold#571: a tool's page numbers nothing, so nothing hints at the loop's other scenarios.
|
|
77
|
+
if (vendor) {
|
|
78
|
+
const name = scenarioName(demo);
|
|
79
|
+
return { kicker: name ? `Scenario · ${name}` : "Scenario", title: demo.title || name || demo.scenario };
|
|
80
|
+
}
|
|
71
81
|
const known = Object.prototype.hasOwnProperty.call(SCENARIOS, demo.scenario) ? SCENARIOS[demo.scenario] : null;
|
|
72
82
|
const name = scenarioName(demo);
|
|
73
83
|
if (counted(demo.index)) {
|
|
@@ -218,14 +228,17 @@ export function unreviewedLines(answer) {
|
|
|
218
228
|
|
|
219
229
|
/**
|
|
220
230
|
* The approval the driver gives at a waiting wave, as terragucci's
|
|
221
|
-
* stack/demo.sh gives it: `approve_wave` runs chant's approve
|
|
222
|
-
* the driver
|
|
223
|
-
*
|
|
224
|
-
*
|
|
231
|
+
* stack/demo.sh gives it: `approve_wave` runs chant's approve as the
|
|
232
|
+
* reviewer (terragucci#1052: the driver plays two people, demo-author who
|
|
233
|
+
* opens, merges and comments, and demo-reviewer who approves, since an
|
|
234
|
+
* approval only counts from someone who had no hand in the change), sealed
|
|
235
|
+
* with the reviewer's own signing key; then `resume_pull` posts
|
|
236
|
+
* `/terragucci apply` on the merged pull request. The actor and the key are
|
|
237
|
+
* placeholders: the kiosk never names a path or a secret.
|
|
225
238
|
*/
|
|
226
239
|
export function driverApproveLines(wave, pull) {
|
|
227
240
|
return {
|
|
228
|
-
approve: `chant approve tf-apply wave-${wave} --actor <
|
|
241
|
+
approve: `chant approve tf-apply wave-${wave} --actor <reviewer> --sign <reviewer key>`,
|
|
229
242
|
comment: "/terragucci apply",
|
|
230
243
|
on: pull !== null && pull !== undefined ? `pull request #${pull}` : "the merged pull request",
|
|
231
244
|
};
|
|
@@ -254,12 +267,13 @@ export function pendingApproval(demo, progress) {
|
|
|
254
267
|
|
|
255
268
|
/**
|
|
256
269
|
* behold#571: the approval in plain words, before the command, for a viewer
|
|
257
|
-
* who has never approved a pipeline: who the driver
|
|
270
|
+
* who has never approved a pipeline: who the driver plays, what it signs
|
|
271
|
+
* with (terragucci#1052: a reviewer apart from the change's author). The
|
|
258
272
|
* plan's digest is on the wave's box in the view.
|
|
259
273
|
*/
|
|
260
274
|
export function approveGloss(appr) {
|
|
261
275
|
const roots = appr.roots.length ? ` (${appr.roots.join(", ")})` : "";
|
|
262
|
-
return `The driver
|
|
276
|
+
return `The driver plays a reviewer who had no hand in the change, and approves wave ${appr.wave}${roots} with that reviewer's own signing key.`;
|
|
263
277
|
}
|
|
264
278
|
|
|
265
279
|
/**
|
|
@@ -331,8 +345,10 @@ export function runPin(demo) {
|
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
/** The framed view's address: behold's own page, bare, at a zoom (the resources zoom unless told), told who its host is. */
|
|
334
|
-
export function frameSrc(origin, zoom = "resources") {
|
|
348
|
+
export function frameSrc(origin, zoom = "resources", repo = null, theme = null) {
|
|
335
349
|
const q = new URLSearchParams({ embed: "1", bare: "1", zoom, host: origin });
|
|
350
|
+
if (repo) q.set("repo", repo); // behold#571: the checkout the frame draws, on a behold that serves several
|
|
351
|
+
if (theme) q.set("theme", theme); // behold#571: the kiosk's theme, which the frame follows
|
|
336
352
|
return `/?${q.toString()}`;
|
|
337
353
|
}
|
|
338
354
|
|
|
@@ -394,11 +410,12 @@ export function kioskView(demo, { components = false, pick = null, now = Date.no
|
|
|
394
410
|
}
|
|
395
411
|
|
|
396
412
|
/** The line under the view saying how to read it, for the view shown. */
|
|
397
|
-
export function legendWords(view) {
|
|
413
|
+
export function legendWords(view, tool = null) {
|
|
414
|
+
const root = rootWord(tool);
|
|
398
415
|
if (view === "components") {
|
|
399
|
-
return
|
|
416
|
+
return `Each card is ${root} with its own state, in a box per wave titled with the pipeline job that applies it; an arrow is one root reading another's outputs, dashed while it reads planned values. A wave applies its roots together, after the waves it reads.`;
|
|
400
417
|
}
|
|
401
|
-
return
|
|
418
|
+
return `Each box is ${root} with its own state; an arrow is one root reading another's outputs, dashed while it reads planned values. A wave applies its roots together, after the waves it reads.`;
|
|
402
419
|
}
|
|
403
420
|
|
|
404
421
|
/** The behold zoom for a view. */
|
|
@@ -457,76 +474,232 @@ const el = (tag, cls, text) => {
|
|
|
457
474
|
return n;
|
|
458
475
|
};
|
|
459
476
|
|
|
477
|
+
/** behold#571: an element whose text puts a dot in its root's colour by each whole-word root name. */
|
|
478
|
+
const elRoots = (tag, cls, text, s) => {
|
|
479
|
+
const roots = (s && (s.rootNames || s.roots)) || [];
|
|
480
|
+
if (text === undefined || text === null || !rootSegments(String(text), roots).some((x) => x.root)) return el(tag, cls, text);
|
|
481
|
+
const n = el(tag, cls);
|
|
482
|
+
n.appendChild(rootText(document, String(text), roots, { light: !!(s && s.light) }));
|
|
483
|
+
return n;
|
|
484
|
+
};
|
|
485
|
+
|
|
460
486
|
/** Draw the bar and the foot from what the kiosk holds. Pure over its arguments, but for the DOM. */
|
|
461
487
|
export function renderKiosk(bar, s, now = new Date(), foot = null) {
|
|
462
488
|
bar.replaceChildren();
|
|
463
489
|
if (foot) foot.replaceChildren();
|
|
490
|
+
drawBar(bar, s, now, foot);
|
|
491
|
+
// behold#571: a tool's page ends with who makes the tool, and that they don't endorse the demo.
|
|
492
|
+
if (foot && vendorOf(s.vendor)) drawCredit(foot, s.vendor);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function drawBar(bar, s, now, foot) {
|
|
496
|
+
const vendor = vendorOf(s.vendor);
|
|
497
|
+
if (vendor && s.mode === "none") {
|
|
498
|
+
// behold#571: the tool's scenario has no cycle yet: its name, its link, and a calm line.
|
|
499
|
+
const words = notRunWords(vendor);
|
|
500
|
+
const box = el("div", "k-empty");
|
|
501
|
+
const title = el("h1", "k-title");
|
|
502
|
+
title.append(brandEl(vendor), el("span", "k-title-text", words.title));
|
|
503
|
+
box.append(title, el("p", "k-muted", words.text));
|
|
504
|
+
bar.appendChild(box);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
464
507
|
if (!s.demo || !s.demo.demo) {
|
|
465
508
|
const box = el("div", "k-empty");
|
|
466
|
-
|
|
509
|
+
const title = el("h1", "k-title");
|
|
510
|
+
if (vendor) title.appendChild(brandEl(vendor));
|
|
511
|
+
title.appendChild(el("span", "k-title-text", "Waiting for the demo"));
|
|
512
|
+
box.appendChild(title);
|
|
467
513
|
box.appendChild(el("p", "k-muted", s.demo ? s.demo.error || s.demo.absent : "Reading demo.json…"));
|
|
468
514
|
bar.appendChild(box);
|
|
469
515
|
return;
|
|
470
516
|
}
|
|
471
517
|
const demo = s.demo.demo;
|
|
472
|
-
const head = scenarioHead(demo);
|
|
518
|
+
const head = scenarioHead(demo, { vendor: s.vendor || null });
|
|
473
519
|
|
|
520
|
+
// behold#571: a tool's page says plainly when it replays an earlier run, and when it can't draw the repo's graph.
|
|
521
|
+
if (s.replay && s.replay.banner) bar.appendChild(el("p", "k-replay", s.replay.banner));
|
|
522
|
+
if (vendor && s.noReports && (s.mode === "live" || s.mode === "replay")) bar.appendChild(el("p", "k-nograph-note", NO_GRAPH_WORDS));
|
|
474
523
|
const intro = introText(demo);
|
|
475
|
-
if (intro) bar.appendChild(el("p", "k-intro", intro));
|
|
476
524
|
|
|
477
|
-
// behold#571: the
|
|
525
|
+
// behold#571: the left column in fixed boxes: the title, the card (the intro between scenarios), the step.
|
|
478
526
|
const left = el("div", "k-left");
|
|
527
|
+
const titleBox = box("title", "k-box-title");
|
|
479
528
|
const title = el("h1", "k-title");
|
|
529
|
+
if (vendor) title.appendChild(brandEl(vendor));
|
|
480
530
|
title.append(el("span", "k-kicker", head.kicker), el("span", "k-title-text", head.title));
|
|
481
|
-
|
|
531
|
+
if (title.setAttribute) title.setAttribute("title", `${head.kicker} · ${head.title || ""}`);
|
|
532
|
+
titleBox.appendChild(title);
|
|
533
|
+
left.appendChild(titleBox);
|
|
534
|
+
const cardBox = box("card");
|
|
482
535
|
const rows = aboutRows(demo);
|
|
483
|
-
if (
|
|
536
|
+
if (demo.step === "reset" && intro) {
|
|
537
|
+
// Between scenarios nothing else happens: the card's box says what the demo is about.
|
|
538
|
+
const p = el("p", "k-intro");
|
|
539
|
+
p.append(el("span", "k-intro-head", INTRO_HEAD), intro);
|
|
540
|
+
cardBox.appendChild(p);
|
|
541
|
+
} else if (rows.length) {
|
|
484
542
|
const card = el("dl", "k-about");
|
|
485
|
-
for (const r of rows) card.append(el("dt", "k-about-label", r.label),
|
|
486
|
-
|
|
543
|
+
for (const r of rows) card.append(el("dt", "k-about-label", r.label), elRoots("dd", "k-about-text", r.text, s));
|
|
544
|
+
cardBox.appendChild(card);
|
|
487
545
|
}
|
|
546
|
+
left.appendChild(cardBox);
|
|
488
547
|
const said = el("p", `k-step k-step-${demo.step}`);
|
|
489
|
-
said.append(el("span", "k-step-word", STEP_WORDS[demo.step] || demo.step),
|
|
490
|
-
|
|
548
|
+
said.append(el("span", "k-step-word", STEP_WORDS[demo.step] || demo.step), elRoots("span", "k-label", demo.label, s));
|
|
549
|
+
const stepBox = box("step");
|
|
550
|
+
stepBox.appendChild(said);
|
|
551
|
+
left.appendChild(stepBox);
|
|
491
552
|
bar.appendChild(left);
|
|
492
553
|
|
|
493
554
|
const right = el("div", "k-right");
|
|
494
555
|
const strip = el("ol", "k-strip");
|
|
495
556
|
for (const stop of stripStops(demo.step)) strip.appendChild(el("li", `k-stop k-${stop.state}`, stop.words));
|
|
496
557
|
const where = el("div", "k-where");
|
|
497
|
-
where.
|
|
558
|
+
where.appendChild(strip);
|
|
498
559
|
right.appendChild(where);
|
|
499
560
|
|
|
500
561
|
const nx = nextWords(demo, now);
|
|
501
562
|
const next = el("div", "k-next");
|
|
502
563
|
next.appendChild(el("span", "k-next-word", "next, by the driver"));
|
|
503
|
-
next.appendChild(
|
|
564
|
+
next.appendChild(elRoots("span", "k-action", nx.action, s));
|
|
504
565
|
next.appendChild(el("span", "k-when", nx.when));
|
|
566
|
+
next.appendChild(el("span", "k-cycle", cycleWords(demo, now)));
|
|
505
567
|
right.appendChild(next);
|
|
506
568
|
|
|
569
|
+
// behold#571: the hold's box: how the hold ends, from the records; else the driver's approval, or the run's note.
|
|
570
|
+
const holdBox = box("hold");
|
|
507
571
|
const appr = pendingApproval(demo, s.progress);
|
|
508
|
-
if (
|
|
572
|
+
if (s.approval) {
|
|
573
|
+
holdBox.appendChild(approvalPanel(s.approval, s));
|
|
574
|
+
} else if (appr) {
|
|
509
575
|
const a = el("div", "k-approve");
|
|
510
|
-
a.appendChild(
|
|
576
|
+
a.appendChild(elRoots("p", "k-approve-gloss", approveGloss(appr), s));
|
|
511
577
|
a.appendChild(el("code", "k-command", appr.approve));
|
|
512
578
|
const then = el("p", "k-approve-then");
|
|
513
579
|
then.append(`then comments `, el("code", "k-inline", appr.comment), ` on ${appr.on}, and the waves run again.`);
|
|
514
580
|
a.appendChild(then);
|
|
515
|
-
|
|
516
|
-
} else {
|
|
581
|
+
holdBox.appendChild(a);
|
|
582
|
+
} else if (!s.noReports) {
|
|
517
583
|
const panel = runPanel(demo, s.answer, s.progress);
|
|
518
|
-
if (panel.note)
|
|
519
|
-
else if (panel.stage === "pull-request") for (const u of unreviewedLines(s.answer))
|
|
584
|
+
if (panel.note) holdBox.appendChild(elRoots("p", "k-note", panel.note, s));
|
|
585
|
+
else if (panel.stage === "pull-request") for (const u of unreviewedLines(s.answer)) holdBox.appendChild(elRoots("p", "k-note k-unknown", u, s));
|
|
520
586
|
}
|
|
587
|
+
right.appendChild(holdBox);
|
|
521
588
|
bar.appendChild(right);
|
|
522
589
|
|
|
523
590
|
if (foot) {
|
|
524
|
-
|
|
591
|
+
// The legend reads the graph; with no graph shown there is nothing for it to explain.
|
|
592
|
+
if (!s.noReports) foot.appendChild(el("span", "k-legend", legendWords(s.view || "resources", s.vendor || null)));
|
|
525
593
|
// behold#571: no line on where the narration was read from: a viewer new to the demo reads the facts.
|
|
526
|
-
for (const f of factLines(demo, s.answer)) foot.appendChild(
|
|
594
|
+
if (!s.noReports) for (const f of factLines(demo, s.answer)) foot.appendChild(elRoots("span", "k-fact", f, s));
|
|
527
595
|
}
|
|
528
596
|
}
|
|
529
597
|
|
|
598
|
+
/** behold#571: the head over the intro, in the card's box between scenarios, and the title of "What is this?". */
|
|
599
|
+
export const INTRO_HEAD = "What this demo is about";
|
|
600
|
+
|
|
601
|
+
/** A fixed box of the header: its text scrolls inside it, and the page keeps its scroll across redraws by `data-box`. */
|
|
602
|
+
function box(name, cls = "") {
|
|
603
|
+
const b = el("div", `k-box${cls ? ` ${cls}` : ""}`);
|
|
604
|
+
if (b.dataset) b.dataset.box = name;
|
|
605
|
+
return b;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* behold#571: "How this hold ends", drawn from approvalModel's rows. A row's
|
|
610
|
+
* chip names the log line it rests on and shows it in the runs pane
|
|
611
|
+
* (`s.showLine`).
|
|
612
|
+
*/
|
|
613
|
+
export function approvalPanel(model, s = {}) {
|
|
614
|
+
const wrap = el("div", "k-ap");
|
|
615
|
+
const head = el("p", "k-ap-head", `${PANEL_TITLE} · wave ${model.wave} `);
|
|
616
|
+
if (model.digest) head.appendChild(el("span", "k-ap-plan", `plan ${shortDigest(model.digest)}`));
|
|
617
|
+
head.append(" · times in UTC");
|
|
618
|
+
wrap.appendChild(head);
|
|
619
|
+
const list = el("ol", "k-ap-rows");
|
|
620
|
+
for (const r of model.rows) {
|
|
621
|
+
const li = el("li", `k-ap-row k-ap-${r.state}`);
|
|
622
|
+
const mark = el("span", "k-ap-mark");
|
|
623
|
+
if (mark.setAttribute) mark.setAttribute("aria-label", r.state === "lit" ? "done" : r.state === "going" ? "under way" : "not yet");
|
|
624
|
+
li.append(mark, el("span", "k-ap-label", r.label));
|
|
625
|
+
const text = elRoots("span", "k-ap-text", r.text, s);
|
|
626
|
+
const facts = rowFacts(r);
|
|
627
|
+
if (facts.length) text.appendChild(el("span", `k-ap-facts${r.seal && r.seal.ok === false ? " k-ap-bad" : ""}`, facts.join(" · ")));
|
|
628
|
+
if (r.line && r.line.a) {
|
|
629
|
+
const chip = annMark(r.line.a, { first: true });
|
|
630
|
+
chip.dataset.annJump = "1";
|
|
631
|
+
chip.title = `${r.line.a.note} Shows run #${r.line.run}, ${r.line.job}, line ${r.line.n}.`;
|
|
632
|
+
const line = r.line;
|
|
633
|
+
chip.addEventListener("click", () => s.showLine && s.showLine(line));
|
|
634
|
+
text.append(" ", chip);
|
|
635
|
+
}
|
|
636
|
+
li.appendChild(text);
|
|
637
|
+
if (r.command) li.appendChild(el("code", "k-command", r.command));
|
|
638
|
+
list.appendChild(li);
|
|
639
|
+
}
|
|
640
|
+
wrap.appendChild(list);
|
|
641
|
+
return wrap;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** behold#571: the header's line when this behold serves another repo than the page's. */
|
|
645
|
+
export const NO_GRAPH_WORDS = "This page shows the runs and their logs; the graph of this repo isn't served here.";
|
|
646
|
+
|
|
647
|
+
/** behold#571: a tool's name, and its logo where its owner allows one, linking to its official site. */
|
|
648
|
+
function brandEl(vendor) {
|
|
649
|
+
const a = el("a", "k-brand");
|
|
650
|
+
a.href = vendor.url;
|
|
651
|
+
a.target = "_blank";
|
|
652
|
+
a.rel = "noopener";
|
|
653
|
+
if (vendor.logo) {
|
|
654
|
+
const img = el("img", "k-brand-logo");
|
|
655
|
+
img.src = vendor.logo.src;
|
|
656
|
+
if (vendor.logo.src2x) img.srcset = `${vendor.logo.src2x} 2x`;
|
|
657
|
+
img.alt = "";
|
|
658
|
+
a.appendChild(img);
|
|
659
|
+
}
|
|
660
|
+
a.appendChild(el("span", "k-brand-name", vendor.name));
|
|
661
|
+
return a;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** behold#571: the footer's credit on a tool's page, its names linked to their official sites. */
|
|
665
|
+
function drawCredit(foot, tool) {
|
|
666
|
+
for (const line of creditLines(tool)) {
|
|
667
|
+
const p = el("span", "k-credit");
|
|
668
|
+
for (const seg of line) {
|
|
669
|
+
if (!seg.url) {
|
|
670
|
+
p.append(seg.text);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const a = el("a", "k-credit-link", seg.text);
|
|
674
|
+
a.href = seg.url;
|
|
675
|
+
a.target = "_blank";
|
|
676
|
+
a.rel = "noopener";
|
|
677
|
+
p.appendChild(a);
|
|
678
|
+
}
|
|
679
|
+
foot.appendChild(p);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** behold#571: how a hovered root's name and wave's job row light. A background and a shadow: nothing moves. */
|
|
684
|
+
export const HOVER_ROOT_RULE = "background: color-mix(in srgb, var(--focus) 32%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus) 32%, transparent); border-radius: 3px;";
|
|
685
|
+
export const HOVER_JOB_RULE = "background: color-mix(in srgb, var(--focus) 24%, transparent); box-shadow: inset 4px 0 0 var(--focus);";
|
|
686
|
+
|
|
687
|
+
/** behold#571: how often the approval panel reads the cycle's runs, and the gate ledger (which the server caches 30 s). */
|
|
688
|
+
export const APPROVAL_POLL_MS = 4_000;
|
|
689
|
+
export const LEDGER_POLL_MS = 10_000;
|
|
690
|
+
|
|
691
|
+
/** behold#571: the approval panel's words for the approval the driver gives next, over its command. */
|
|
692
|
+
export const DRIVER_SIGNS = "Next the driver plays a reviewer with no hand in the change and signs this plan with that reviewer's key:";
|
|
693
|
+
|
|
694
|
+
/** behold#571: "What is this?" before demo.json says. */
|
|
695
|
+
export const WHAT_FALLBACK = "A demo that runs a real infrastructure change through its pipeline in a loop, and shows each step as it happens.";
|
|
696
|
+
|
|
697
|
+
/** behold#571: a fade at a box's edge where its text goes on past it. */
|
|
698
|
+
function fade(b) {
|
|
699
|
+
b.classList.toggle("k-more-below", b.scrollTop + b.clientHeight < b.scrollHeight - 1);
|
|
700
|
+
b.classList.toggle("k-more-above", b.scrollTop > 1);
|
|
701
|
+
}
|
|
702
|
+
|
|
530
703
|
/** How often the kiosk asks for demo.json. The server shares one read between kiosks. */
|
|
531
704
|
export const DEMO_POLL_MS = 3_000;
|
|
532
705
|
|
|
@@ -537,8 +710,37 @@ export const DEMO_POLL_MS = 3_000;
|
|
|
537
710
|
* countdown. When the roots the step is about change, move the framed view
|
|
538
711
|
* with a `behold:view` message; the frame takes them only from this origin.
|
|
539
712
|
*/
|
|
540
|
-
export function startKiosk({ bar, frame, foot, side, main, views }, { fetchJson = defaultFetch, now = () => new Date(), origin = location.origin } = {}) {
|
|
541
|
-
const s = { demo: null, answer: null, progress: null, components: false, pick: null, view: "resources" };
|
|
713
|
+
export function startKiosk({ bar, frame, foot, side, main, views, tools = null }, { fetchJson = defaultFetch, now = () => new Date(), origin = location.origin, tool = null } = {}) {
|
|
714
|
+
const s = { demo: null, answer: null, progress: null, components: false, pick: null, view: "resources", roots: [], waves: [], light: false, hover: null, frameHover: null };
|
|
715
|
+
// behold#571: on a tool's page, `live` while its scenario runs, `replay` of its latest cycle otherwise, `none` before it has one.
|
|
716
|
+
const vendor = vendorOf(tool);
|
|
717
|
+
s.vendor = vendor ? tool : null;
|
|
718
|
+
s.mode = vendor ? "loading" : "live";
|
|
719
|
+
s.replay = null;
|
|
720
|
+
s.repo = null;
|
|
721
|
+
s.live = null;
|
|
722
|
+
if (vendor && typeof document !== "undefined") document.title = `${vendor.name} · terragucci demo`;
|
|
723
|
+
/** The time the page shows: the replay's, while it replays. */
|
|
724
|
+
const shownNow = () => (s.mode === "replay" && s.replay && s.replay.at ? s.replay.at : now());
|
|
725
|
+
/** behold#571: the frame and the plan's facts are the served repo's, so a tool's page shows them only for its own repo. */
|
|
726
|
+
const frameOk = () => !vendor || servesRepo(s.answer && !s.answer.error ? s.answer.project : null, s.repo);
|
|
727
|
+
// behold#571: a behold serving several repos (`serve --repos`) answers the
|
|
728
|
+
// graph's reads from the checkout of the repo the page is about, asked by
|
|
729
|
+
// name. `viewRepos` is what it serves, `aimed` the repo the reads are for.
|
|
730
|
+
s.viewRepos = null;
|
|
731
|
+
s.aimed = null;
|
|
732
|
+
const wantRepo = () => repoParam(s.repo, s.viewRepos);
|
|
733
|
+
const fetchServed = fetchJson;
|
|
734
|
+
fetchJson = (url) => fetchServed(repoUrl(url, wantRepo()));
|
|
735
|
+
/** When the page moves to another served repo, read its project, reports and run view again, and point the frame at it. */
|
|
736
|
+
const followRepo = () => {
|
|
737
|
+
const want = wantRepo();
|
|
738
|
+
if (!want || want === s.aimed) return;
|
|
739
|
+
s.aimed = want;
|
|
740
|
+
void readProject();
|
|
741
|
+
void reports(true);
|
|
742
|
+
void progress(true);
|
|
743
|
+
};
|
|
542
744
|
let lastKey = "";
|
|
543
745
|
let lastFocus;
|
|
544
746
|
const shownView = () => kioskView(s.demo && s.demo.demo, { components: s.components, pick: s.pick, now: now().getTime() });
|
|
@@ -558,32 +760,280 @@ export function startKiosk({ bar, frame, foot, side, main, views }, { fetchJson
|
|
|
558
760
|
s.pick = { view, at: now().getTime() };
|
|
559
761
|
draw();
|
|
560
762
|
};
|
|
763
|
+
// behold#571: the roots' colours and the hover links between the panes and the frame.
|
|
764
|
+
const rootList = () => (s.roots.length ? s.roots : s.answer && Array.isArray(s.answer.roots) ? s.answer.roots.map((r) => r.path) : []);
|
|
765
|
+
const glow = () => {
|
|
766
|
+
let style = document.getElementById("k-hover-style");
|
|
767
|
+
if (!style) {
|
|
768
|
+
style = document.createElement("style");
|
|
769
|
+
style.id = "k-hover-style";
|
|
770
|
+
document.head.appendChild(style);
|
|
771
|
+
}
|
|
772
|
+
const a = hoverLinks(s.hover, s.waves);
|
|
773
|
+
const b = hoverLinks(s.frameHover, s.waves);
|
|
774
|
+
const links = { roots: [...new Set([...a.roots, ...b.roots])], waves: [...new Set([...a.waves, ...b.waves])] };
|
|
775
|
+
style.textContent = [
|
|
776
|
+
hoverCss({ roots: links.roots, waves: [] }, { root: (r) => `.k-root[data-k-root=${cssString(r)}]` }, HOVER_ROOT_RULE),
|
|
777
|
+
hoverCss({ roots: [], waves: links.waves }, { wave: (w) => `.r-job[data-k-wave="${w}"]` }, HOVER_JOB_RULE),
|
|
778
|
+
].join("\n");
|
|
779
|
+
};
|
|
780
|
+
const tellHover = (h) => {
|
|
781
|
+
if (frame && frame.contentWindow) frame.contentWindow.postMessage(hoverMessage(h), origin);
|
|
782
|
+
};
|
|
783
|
+
const hoverAt = (target) => {
|
|
784
|
+
const t = target && target.closest ? target.closest("[data-k-root],[data-k-wave]") : null;
|
|
785
|
+
if (!t) return null;
|
|
786
|
+
if (t.dataset.kRoot) return { root: t.dataset.kRoot };
|
|
787
|
+
const w = Number(t.dataset.kWave);
|
|
788
|
+
return Number.isInteger(w) ? { wave: w } : null;
|
|
789
|
+
};
|
|
790
|
+
const setHover = (h) => {
|
|
791
|
+
if ((h && h.root) === (s.hover && s.hover.root) && (h && h.wave) === (s.hover && s.hover.wave)) return;
|
|
792
|
+
s.hover = h;
|
|
793
|
+
glow();
|
|
794
|
+
tellHover(h);
|
|
795
|
+
};
|
|
796
|
+
if (typeof document !== "undefined" && document.addEventListener) {
|
|
797
|
+
document.addEventListener("pointerover", (e) => setHover(hoverAt(e.target)));
|
|
798
|
+
document.documentElement.addEventListener("pointerleave", () => setHover(null));
|
|
799
|
+
// A click or Escape anywhere here is a click elsewhere for the frame's pinned popover.
|
|
800
|
+
const unpin = () => frame && frame.contentWindow && frame.contentWindow.postMessage({ type: "behold:unpin" }, origin);
|
|
801
|
+
document.addEventListener("pointerdown", unpin);
|
|
802
|
+
document.addEventListener("keydown", (e) => {
|
|
803
|
+
if (e.key === "Escape") unpin();
|
|
804
|
+
});
|
|
805
|
+
window.addEventListener("message", (e) => {
|
|
806
|
+
if (e.origin !== origin || !frame || e.source !== frame.contentWindow) return;
|
|
807
|
+
const h = hoverFromMessage(e.data);
|
|
808
|
+
if (h === undefined) return;
|
|
809
|
+
s.frameHover = h;
|
|
810
|
+
glow();
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// behold#571: the titles and names demo.json has carried, by scenario, for the runs' subtitles.
|
|
815
|
+
const titles = {};
|
|
816
|
+
const names = {};
|
|
817
|
+
/** Take one narration (demo.json, or a replayed step), and ask for the run it is about when the step moved. */
|
|
818
|
+
const setDemo = (answer) => {
|
|
819
|
+
s.demo = answer;
|
|
820
|
+
const d = answer && answer.demo;
|
|
821
|
+
if (d && d.scenario && typeof d.title === "string" && d.title) titles[d.scenario] = d.title;
|
|
822
|
+
if (d && d.scenario && scenarioName(d)) names[d.scenario] = scenarioName(d);
|
|
823
|
+
const key = d ? `${s.mode}|${d.cycle}|${d.step}|${d.commit}|${d.pull}` : "";
|
|
824
|
+
if (key !== lastKey) {
|
|
825
|
+
lastKey = key;
|
|
826
|
+
// The step moved: the run it is about may be newer than what behold read.
|
|
827
|
+
const stage = d ? demoStage(d) : "none";
|
|
828
|
+
if (stage === "pull-request" || stage === "either") void reports(true);
|
|
829
|
+
if (stage === "apply" || stage === "either") void progress(true);
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
// behold#571: the replay's clock, read on every draw.
|
|
833
|
+
const replayTick = () => {
|
|
834
|
+
if (s.mode !== "replay" || !s.replay) return;
|
|
835
|
+
const steps = s.replay.cycle.steps;
|
|
836
|
+
const r = replayAt(steps, now().getTime() - s.replay.start);
|
|
837
|
+
s.replay.at = r.at;
|
|
838
|
+
s.replay.banner = replayBanner(s.replay.cycle, liveEstimate(s.live && s.live.demo, s.replay.cycle, now()));
|
|
839
|
+
setDemo({ source: "replay", read: steps[r.index].at, demo: scrubDemo(steps[r.index], tool) });
|
|
840
|
+
};
|
|
841
|
+
|
|
561
842
|
const draw = () => {
|
|
843
|
+
followRepo();
|
|
844
|
+
replayTick();
|
|
845
|
+
s.light = typeof document !== "undefined" && document.documentElement ? isLightBg(getComputedStyle(document.documentElement).getPropertyValue("--bg")) : false;
|
|
846
|
+
s.rootNames = rootList();
|
|
562
847
|
const shown = shownView();
|
|
563
848
|
s.view = shown.view;
|
|
564
|
-
|
|
565
|
-
|
|
849
|
+
const ok = frameOk();
|
|
850
|
+
s.noReports = !ok;
|
|
851
|
+
s.approval = approvalNow();
|
|
852
|
+
// behold#571: the header's boxes redraw every second; each keeps where a viewer scrolled it.
|
|
853
|
+
const scrolled = Object.fromEntries([...bar.querySelectorAll(".k-box")].map((b) => [b.dataset.box, b.scrollTop]));
|
|
854
|
+
renderKiosk(bar, s, shownNow(), foot);
|
|
855
|
+
for (const b of bar.querySelectorAll(".k-box")) {
|
|
856
|
+
if (scrolled[b.dataset.box]) b.scrollTop = scrolled[b.dataset.box];
|
|
857
|
+
fade(b);
|
|
858
|
+
b.addEventListener("scroll", () => fade(b), { passive: true });
|
|
859
|
+
}
|
|
860
|
+
whatText();
|
|
861
|
+
renderViewToggle(views, shown, { components: s.components && ok, onPick: pickView });
|
|
862
|
+
// behold#571: with no graph to show, the graph pane folds away and the runs and the log take its width.
|
|
863
|
+
if (typeof document !== "undefined" && document.body && document.body.classList) document.body.classList.toggle("k-nograph", !!vendor && !ok);
|
|
566
864
|
tellFrame(false);
|
|
567
865
|
};
|
|
568
|
-
|
|
569
|
-
// behold#571: the
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
866
|
+
|
|
867
|
+
// behold#571: on a tool's page the runs pane shows the tool's repo alone; in the replay, the cycle's runs as they stood.
|
|
868
|
+
const runDocs = new Map();
|
|
869
|
+
const runDoc = (id) => {
|
|
870
|
+
if (!runDocs.has(id)) runDocs.set(id, fetchJson(`/api/terragucci/runs/${id}`).then((a) => (a && a.doc ? a.doc : null)));
|
|
871
|
+
return runDocs.get(id);
|
|
872
|
+
};
|
|
873
|
+
const replayRuns = async () => {
|
|
874
|
+
const r = s.replay;
|
|
875
|
+
if (r.runs) return r.runs;
|
|
876
|
+
const idx = await fetchJson("/api/terragucci/runs");
|
|
877
|
+
const listed = new Map((idx && idx.doc && Array.isArray(idx.doc.runs) ? idx.doc.runs : []).map((x) => [x.id, x]));
|
|
878
|
+
const runs = [];
|
|
879
|
+
for (const id of r.cycle.runs) {
|
|
880
|
+
const e = listed.get(id) || (await runDoc(id));
|
|
881
|
+
if (e) runs.push(e);
|
|
882
|
+
}
|
|
883
|
+
r.runs = runs.filter((x) => keepRun(x, { tool, repo: r.cycle.repo }));
|
|
884
|
+
return r.runs;
|
|
885
|
+
};
|
|
886
|
+
const runsSource = () => {
|
|
887
|
+
if (!vendor || s.mode === "live") return null;
|
|
888
|
+
if (s.mode !== "replay" || !s.replay) return { index: async () => ({ doc: { schema: "demo.runs/v1", runs: [] } }), run: async () => ({ doc: null, absent: "No run to show yet." }) };
|
|
889
|
+
return {
|
|
890
|
+
index: async () => replayIndex(await replayRuns(), shownNow().getTime()),
|
|
891
|
+
run: async (id) => {
|
|
892
|
+
const doc = await runDoc(id);
|
|
893
|
+
return doc ? { doc: runAsOf(doc, shownNow().getTime()) } : { doc: null, absent: `Run #${id} is no longer kept in the reports.` };
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
};
|
|
897
|
+
// behold#571: the approval panel's records: the cycle's runs, the gate ledger's history, and the lane.
|
|
898
|
+
const ap = { runs: new Map(), list: [], lifecycle: null, history: null, at: 0, ledgerAt: 0, busy: false };
|
|
899
|
+
const readApproval = async () => {
|
|
900
|
+
const d = s.demo && s.demo.demo;
|
|
901
|
+
if (!d || !PANEL_STEPS.includes(d.step) || ap.busy) return;
|
|
902
|
+
const t = now().getTime();
|
|
903
|
+
if (t - ap.at < APPROVAL_POLL_MS) return;
|
|
904
|
+
ap.busy = true;
|
|
905
|
+
ap.at = t;
|
|
906
|
+
try {
|
|
907
|
+
const src = runsSource();
|
|
908
|
+
const raw = src ? await src.index() : await fetchJson("/api/terragucci/runs");
|
|
909
|
+
const idx = vendor ? filterIndex(raw, { tool, repo: s.repo }) : raw;
|
|
910
|
+
const mine = cycleRuns(idx && idx.doc && Array.isArray(idx.doc.runs) ? idx.doc.runs : [], d);
|
|
911
|
+
for (const e of mine) {
|
|
912
|
+
const key = `${s.mode}|${e.status}|${e.ended_at}|${src ? Math.floor(shownNow().getTime() / 5000) : ""}`;
|
|
913
|
+
const had = ap.runs.get(e.id);
|
|
914
|
+
if (had && had.key === key) continue;
|
|
915
|
+
const r = src ? await src.run(e.id) : await fetchJson(`/api/terragucci/runs/${e.id}`);
|
|
916
|
+
if (r && r.doc) ap.runs.set(e.id, { key, doc: mergeRun(e, r.doc) });
|
|
917
|
+
}
|
|
918
|
+
ap.list = mine.map((e) => ap.runs.get(e.id)).filter(Boolean).map((x) => x.doc);
|
|
919
|
+
if (t - ap.ledgerAt >= LEDGER_POLL_MS) {
|
|
920
|
+
ap.ledgerAt = t;
|
|
921
|
+
const [lifecycle, history] = await Promise.all([fetchJson("/api/terragucci/lifecycle"), fetchJson("/api/lifecycle/history?limit=200")]);
|
|
922
|
+
ap.lifecycle = lifecycle && !lifecycle.error ? lifecycle : null;
|
|
923
|
+
ap.history = history && !history.error ? history : null;
|
|
924
|
+
}
|
|
925
|
+
} finally {
|
|
926
|
+
ap.busy = false;
|
|
927
|
+
}
|
|
928
|
+
draw();
|
|
929
|
+
};
|
|
930
|
+
const approvalNow = () => {
|
|
931
|
+
const d = s.demo && s.demo.demo;
|
|
932
|
+
if (!d || !PANEL_STEPS.includes(d.step)) return null;
|
|
933
|
+
const appr = pendingApproval(d, s.progress);
|
|
934
|
+
const driver = appr && s.mode !== "replay" ? { gloss: DRIVER_SIGNS, approve: appr.approve, comment: appr.comment, on: appr.on } : null;
|
|
935
|
+
return approvalModel({ demo: d, runs: ap.list, history: ap.history, lifecycle: ap.lifecycle, fallback: appr, now: shownNow().getTime(), driver });
|
|
936
|
+
};
|
|
937
|
+
// behold#571: a chip in the panel shows its log line in the runs pane (opened, on a phone).
|
|
938
|
+
let runsApi = null;
|
|
939
|
+
s.showLine = (line) => {
|
|
940
|
+
if (!runsApi) return;
|
|
941
|
+
if (tools && tools.runsToggle) setRunsOpen(true);
|
|
942
|
+
runsApi.show(line.run, line.job, line.step, line.n);
|
|
943
|
+
};
|
|
944
|
+
const setRunsOpen = (open) => {
|
|
945
|
+
document.body.classList.toggle("k-runs-open", open);
|
|
946
|
+
if (tools && tools.runsToggle) tools.runsToggle.setAttribute("aria-expanded", String(open));
|
|
947
|
+
};
|
|
948
|
+
if (tools && tools.runsToggle) tools.runsToggle.addEventListener("click", () => setRunsOpen(!document.body.classList.contains("k-runs-open")));
|
|
949
|
+
|
|
950
|
+
// behold#571: "What is this?" opens the intro over the page (no layout moves); a click elsewhere or Escape closes it.
|
|
951
|
+
const whatText = () => {
|
|
952
|
+
if (!tools || !tools.pop) return;
|
|
953
|
+
const d = s.demo && s.demo.demo;
|
|
954
|
+
const text = (d && introText(d)) || WHAT_FALLBACK;
|
|
955
|
+
const p = tools.pop.firstElementChild;
|
|
956
|
+
if (p && p.textContent === text) return;
|
|
957
|
+
tools.pop.replaceChildren(el("p", null, text));
|
|
958
|
+
};
|
|
959
|
+
if (tools && tools.what && tools.pop) {
|
|
960
|
+
const setWhat = (open) => {
|
|
961
|
+
tools.pop.hidden = !open;
|
|
962
|
+
tools.what.setAttribute("aria-expanded", String(open));
|
|
963
|
+
};
|
|
964
|
+
tools.what.addEventListener("click", (e) => {
|
|
965
|
+
e.stopPropagation();
|
|
966
|
+
whatText();
|
|
967
|
+
setWhat(tools.pop.hidden);
|
|
968
|
+
});
|
|
969
|
+
document.addEventListener("click", (e) => {
|
|
970
|
+
if (!tools.pop.hidden && !tools.pop.contains(e.target)) setWhat(false);
|
|
971
|
+
});
|
|
972
|
+
document.addEventListener("keydown", (e) => {
|
|
973
|
+
if (e.key === "Escape") setWhat(false);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
// behold#571: the theme picker, persisted per viewer as the app does; the frame follows without a reload.
|
|
977
|
+
if (tools && tools.theme) mountThemePicker(tools.theme);
|
|
978
|
+
onThemeChange((th) => {
|
|
979
|
+
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ type: "behold:theme", name: th.name }, origin);
|
|
980
|
+
draw();
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
if (side && main) {
|
|
984
|
+
runsApi = startRuns(
|
|
985
|
+
{ side, main },
|
|
986
|
+
{
|
|
987
|
+
fetchJson,
|
|
988
|
+
now,
|
|
989
|
+
shown: shownNow,
|
|
990
|
+
scenarios: SCENARIOS,
|
|
991
|
+
titles: () => titles,
|
|
992
|
+
names: () => names,
|
|
993
|
+
roots: rootList,
|
|
994
|
+
waves: () => s.waves,
|
|
995
|
+
light: () => s.light,
|
|
996
|
+
source: runsSource,
|
|
997
|
+
filter: vendor ? (index) => filterIndex(index, { tool, repo: s.repo }) : (index) => index,
|
|
998
|
+
replay: () => s.mode === "replay",
|
|
999
|
+
hideLine: vendor ? (line) => namesOther(line, tool) : null,
|
|
1000
|
+
},
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
573
1003
|
// #571: whether the repo offers the components zoom (a terragucci.components.json), read before the frame opens so it opens on the step's view.
|
|
1004
|
+
let frameSrcNow = null;
|
|
574
1005
|
const openFrame = () => {
|
|
575
1006
|
if (!frame) return;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1007
|
+
const src = frameSrc(origin, viewZoom(shownView().view), wantRepo(), getThemeName());
|
|
1008
|
+
if (src === frameSrcNow) return;
|
|
1009
|
+
if (frameSrcNow === null) {
|
|
1010
|
+
// The frame reads its graph after load; tell it again once it has had the time.
|
|
1011
|
+
frame.addEventListener("load", () => {
|
|
1012
|
+
for (const ms of [1500, 4000, 8000]) setTimeout(() => tellFrame(true), ms);
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
frameSrcNow = src;
|
|
1016
|
+
frame.src = src;
|
|
581
1017
|
};
|
|
582
|
-
|
|
1018
|
+
const readProject = () => fetchJson("/api/project").then((p) => {
|
|
1019
|
+
// behold#571: the repos this behold serves, and which one this answer is.
|
|
1020
|
+
if (p && !p.error && Array.isArray(p.viewRepos)) {
|
|
1021
|
+
s.viewRepos = p.viewRepos;
|
|
1022
|
+
if (s.aimed === null && p.viewRepo) s.aimed = p.viewRepo;
|
|
1023
|
+
}
|
|
583
1024
|
s.components = !!(p && !p.error && p.terragucciComponents);
|
|
1025
|
+
// behold#571: the declared roots (their colours' order) and the wave jobs that apply them.
|
|
1026
|
+
if (p && p.terragucciRoots) {
|
|
1027
|
+
s.roots = Array.isArray(p.terragucciRoots.roots) ? p.terragucciRoots.roots : [];
|
|
1028
|
+
s.waves = Array.isArray(p.terragucciRoots.waves) ? p.terragucciRoots.waves : [];
|
|
1029
|
+
} else if (s.aimed) {
|
|
1030
|
+
s.roots = [];
|
|
1031
|
+
s.waves = [];
|
|
1032
|
+
}
|
|
584
1033
|
openFrame();
|
|
585
1034
|
draw();
|
|
586
1035
|
});
|
|
1036
|
+
void readProject();
|
|
587
1037
|
|
|
588
1038
|
const reports = async (fresh) => {
|
|
589
1039
|
s.answer = await fetchJson(`/api/terragucci${fresh ? "?fresh=1" : ""}`);
|
|
@@ -604,19 +1054,45 @@ export function startKiosk({ bar, frame, foot, side, main, views }, { fetchJson
|
|
|
604
1054
|
draw();
|
|
605
1055
|
};
|
|
606
1056
|
|
|
1057
|
+
// behold#571: a tool's latest cycle, read when its scenario stops being live, and while it has none yet.
|
|
1058
|
+
let cycleRead = 0;
|
|
1059
|
+
const readCycle = async () => {
|
|
1060
|
+
cycleRead = now().getTime();
|
|
1061
|
+
const a = await fetchJson(`/api/terragucci/cycles/${tool}`);
|
|
1062
|
+
if (s.mode === "live") return; // its scenario started while the read was out
|
|
1063
|
+
if (a && a.cycle) {
|
|
1064
|
+
if (!s.replay || s.replay.cycle.cycle !== a.cycle.cycle) s.replay = { cycle: a.cycle, start: now().getTime(), at: null, banner: null, runs: null };
|
|
1065
|
+
s.mode = "replay";
|
|
1066
|
+
s.repo = a.cycle.repo;
|
|
1067
|
+
} else if (a && a.absent) {
|
|
1068
|
+
s.mode = "none";
|
|
1069
|
+
s.replay = null;
|
|
1070
|
+
} else {
|
|
1071
|
+
s.mode = "error";
|
|
1072
|
+
s.replay = null;
|
|
1073
|
+
s.demo = { demo: null, error: a && a.error ? a.error : "The tool's latest cycle could not be read." };
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const CYCLE_REREAD_MS = 60_000;
|
|
1077
|
+
|
|
607
1078
|
const demo = async () => {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1079
|
+
const live = await fetchJson("/api/terragucci/demo");
|
|
1080
|
+
s.live = live;
|
|
1081
|
+
if (!vendor) {
|
|
1082
|
+
s.repo = live && live.demo ? live.demo.repo : null;
|
|
1083
|
+
setDemo(live);
|
|
1084
|
+
return draw();
|
|
1085
|
+
}
|
|
1086
|
+
const d = live && live.demo;
|
|
1087
|
+
if (liveFor(d, tool, SCENARIOS)) {
|
|
1088
|
+
s.mode = "live";
|
|
1089
|
+
s.replay = null;
|
|
1090
|
+
s.repo = d.repo || s.repo;
|
|
1091
|
+
setDemo({ ...live, demo: scrubDemo(d, tool) });
|
|
1092
|
+
return draw();
|
|
619
1093
|
}
|
|
1094
|
+
// Not this tool's scenario: replay its latest cycle, read again when it went live and ended, or now and then.
|
|
1095
|
+
if (s.mode === "live" || s.mode === "loading" || now().getTime() - cycleRead > CYCLE_REREAD_MS) await readCycle();
|
|
620
1096
|
draw();
|
|
621
1097
|
};
|
|
622
1098
|
|
|
@@ -624,6 +1100,7 @@ export function startKiosk({ bar, frame, foot, side, main, views }, { fetchJson
|
|
|
624
1100
|
void reports(false);
|
|
625
1101
|
void progress(false);
|
|
626
1102
|
setInterval(() => void demo(), DEMO_POLL_MS);
|
|
1103
|
+
setInterval(() => void readApproval(), 1000);
|
|
627
1104
|
setInterval(draw, 1000);
|
|
628
1105
|
|
|
629
1106
|
if (typeof EventSource !== "undefined") {
|