@intentius/behold 0.27.0 → 0.29.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/README.md +27 -0
- package/dist/cli.js +702 -285
- package/package.json +4 -4
- package/web/app.js +158 -34
- package/web/edge-kind.js +31 -0
- package/web/edge-kind.test.js +31 -1
- package/web/kiosk.html +67 -0
- package/web/kiosk.js +385 -0
- package/web/operator.js +1 -0
- package/web/run-playhead.js +1 -0
- package/web/terragucci-change.js +423 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/behold",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@hono/node-server": "^2.0.5",
|
|
44
|
-
"@intentius/chant": "^0.124.
|
|
45
|
-
"@intentius/chant-k8s-client": "^0.124.
|
|
46
|
-
"@intentius/pinhole": "^0.
|
|
44
|
+
"@intentius/chant": "^0.124.24",
|
|
45
|
+
"@intentius/chant-k8s-client": "^0.124.24",
|
|
46
|
+
"@intentius/pinhole": "^0.5.0",
|
|
47
47
|
"ajv": "^8.20.0",
|
|
48
48
|
"hono": "^4.12.34"
|
|
49
49
|
},
|
package/web/app.js
CHANGED
|
@@ -18,10 +18,11 @@ import { renderLane } from "./terragucci-lifecycle.js";
|
|
|
18
18
|
import { renderReleases } from "./releases.js";
|
|
19
19
|
import { renderYodelLane } from "./sql-yodeler-lane.js";
|
|
20
20
|
import { initTerragucciEstate } from "./terragucci-estate.js";
|
|
21
|
+
import { changeSection as tgChangeSection, readCardIds as tgReadCardIds, readEdges as tgReadEdges, readLine as tgReadLine, readTag as tgReadTag } from "./terragucci-change.js";
|
|
21
22
|
import { auditLines as yrAuditLines, cornerText as yrCorner, datedWords as yrDated, environmentLines as yrEnvLines, joinMarks as yrJoin, legend as yrLegend, markLine as yrMarkLine } from "./yodel-report.js";
|
|
22
23
|
import { cardProgress as tgCardProgress, progressLines as tgProgressLines, progressSection as tgProgressSection, stampProgress as tgStampProgress } from "./terragucci-progress.js";
|
|
23
24
|
import { seconds, whyQuery, whyView } from "./why.js";
|
|
24
|
-
import { edgeLabel, edgeLegend, edgeTitle, isCiKind } from "./edge-kind.js";
|
|
25
|
+
import { edgeKey, edgeLabel, edgeLegend, edgeTitle, isCiKind, viaIndex } from "./edge-kind.js";
|
|
25
26
|
import { readCostLine } from "./read-cost.js";
|
|
26
27
|
import { mountHistoryPicker } from "./history-picker.js";
|
|
27
28
|
import { applyMemberFrame, pendingLine, stillPending } from "./pending.js";
|
|
@@ -305,6 +306,10 @@ function markStatusChanges(ir, statusVar) {
|
|
|
305
306
|
// replay every read from `snapshots/` — the graph, zoom dial, radial, inspect,
|
|
306
307
|
// and env/tier pickers all work; live observe + all writes are off.
|
|
307
308
|
const staticMode = !!window.__BEHOLD_STATIC__;
|
|
309
|
+
// #569: `behold serve --public` injects this flag into the page it serves. The
|
|
310
|
+
// reads stay live (the SSE stream, the polls); every control that writes is
|
|
311
|
+
// hidden, and the server refuses every method but GET, HEAD and OPTIONS anyway.
|
|
312
|
+
const publicMode = !!window.__BEHOLD_PUBLIC__;
|
|
308
313
|
let manifest = null;
|
|
309
314
|
if (staticMode) {
|
|
310
315
|
try {
|
|
@@ -620,6 +625,7 @@ function stopWhyRead() {
|
|
|
620
625
|
}
|
|
621
626
|
|
|
622
627
|
function renderWhySection(panel, ask) {
|
|
628
|
+
if (publicMode) return; // #569: /api/workspace/why is refused in a public view
|
|
623
629
|
stopWhyRead();
|
|
624
630
|
if (staticMode) return;
|
|
625
631
|
const memberName = ask.member || nodeMember(ask.node || "");
|
|
@@ -941,6 +947,21 @@ function inspect(node) {
|
|
|
941
947
|
panel.appendChild(p);
|
|
942
948
|
}
|
|
943
949
|
}
|
|
950
|
+
// #568: the reads this card's root makes or answers, from the pull
|
|
951
|
+
// request's plan or the run view, dated by its run.
|
|
952
|
+
const tgReadRoot = tgAnswer && !tgAnswer.error && tgRootOfCard(tgAnswer, node.id);
|
|
953
|
+
const tgReads = tgReadRoot ? tgReadEdges(tgAnswer, tgProgress).filter((e) => e.from === tgReadRoot || e.to === tgReadRoot) : [];
|
|
954
|
+
if (tgReads.length) {
|
|
955
|
+
const h = document.createElement("h3");
|
|
956
|
+
h.textContent = "terragucci reads between roots (not live)";
|
|
957
|
+
panel.appendChild(h);
|
|
958
|
+
for (const e of tgReads) {
|
|
959
|
+
const p = document.createElement("p");
|
|
960
|
+
p.className = "tg-inspect";
|
|
961
|
+
p.textContent = tgReadLine(e);
|
|
962
|
+
panel.appendChild(p);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
944
965
|
// #506: the audit record's entries for this card's root (the record names
|
|
945
966
|
// roots, never a resource), newest first.
|
|
946
967
|
const tgRoot = tgTimeline && tgRootOfCard(tgAnswer, node.id);
|
|
@@ -1280,6 +1301,7 @@ let bulkDiffCache = null; // { env, nodes: { <id>: { observed, diff, health } }
|
|
|
1280
1301
|
// without anyone paying for a second full pass over the account.
|
|
1281
1302
|
let planDriftOnce = false;
|
|
1282
1303
|
async function loadNodeDiff(id) {
|
|
1304
|
+
if (publicMode) return null; // #569: /api/diff is refused in a public view
|
|
1283
1305
|
if (!bulkDiffCache || bulkDiffCache.env !== view.env) {
|
|
1284
1306
|
bulkDiffCache = null;
|
|
1285
1307
|
try {
|
|
@@ -1585,7 +1607,7 @@ function bootZoom(memberKinds) {
|
|
|
1585
1607
|
// v0.1.0 preview lock (set from /api/project in initActions): hides the git/PR
|
|
1586
1608
|
// write ops (Rollback, Sync, Adopt, Run ▾) — the server also 403s them. Local
|
|
1587
1609
|
// deploy (Apply all / dial), Reset, Bring up, Approve, and reads stay on.
|
|
1588
|
-
let previewMode =
|
|
1610
|
+
let previewMode = publicMode; // #569: a public view hides what preview hides, and more
|
|
1589
1611
|
// #489: set when /api/project says the repo is a terragucci repo — applied by
|
|
1590
1612
|
// its pipeline only, so the page offers no write at all.
|
|
1591
1613
|
let terragucciRepo = null;
|
|
@@ -1957,7 +1979,7 @@ function copyToClipboard(text, el) {
|
|
|
1957
1979
|
* `carve.json`.
|
|
1958
1980
|
*/
|
|
1959
1981
|
function renderChoudoufuMoves(host, info) {
|
|
1960
|
-
if (!info || !info.plans || !info.plans.length || staticMode) return;
|
|
1982
|
+
if (!info || !info.plans || !info.plans.length || staticMode || publicMode) return;
|
|
1961
1983
|
host.appendChild(panelHeading("moves (choudoufu)"));
|
|
1962
1984
|
for (const plan of info.plans) {
|
|
1963
1985
|
const head = document.createElement("div");
|
|
@@ -2112,7 +2134,7 @@ function renderPanelScope() {
|
|
|
2112
2134
|
name.textContent = pathBasename(dir) + (estateDirs.length > 1 && dir === info.projectDir ? " · primary" : "");
|
|
2113
2135
|
name.title = dir;
|
|
2114
2136
|
row.appendChild(name);
|
|
2115
|
-
if (!staticMode) row.appendChild(actButton("⌖ reveal", () => revealProject(dir), "Open this project's folder in your file manager"));
|
|
2137
|
+
if (!staticMode && !publicMode) row.appendChild(actButton("⌖ reveal", () => revealProject(dir), "Open this project's folder in your file manager"));
|
|
2116
2138
|
host.appendChild(row);
|
|
2117
2139
|
host.appendChild(panelMuted(dir));
|
|
2118
2140
|
}
|
|
@@ -2195,7 +2217,7 @@ function renderPanelScope() {
|
|
|
2195
2217
|
// #234's last remainder — "is prod running what staging tested". Only when
|
|
2196
2218
|
// an env is picked and there is another to compare against; ledger-only, so
|
|
2197
2219
|
// it never adds a cloud read to opening the panel (fetch happens on pick).
|
|
2198
|
-
if (view.env && environments.length > 1 && !staticMode) {
|
|
2220
|
+
if (view.env && environments.length > 1 && !staticMode && !publicMode) {
|
|
2199
2221
|
if (view.compareTo === view.env) view.compareTo = null; // env re-picked onto the compared one
|
|
2200
2222
|
host.appendChild(panelHeading("compare releases"));
|
|
2201
2223
|
const sel = document.createElement("select");
|
|
@@ -2517,7 +2539,7 @@ function gatesEnv() {
|
|
|
2517
2539
|
return view.gates || view.env || "";
|
|
2518
2540
|
}
|
|
2519
2541
|
function loadWorkspaceGates({ force = false } = {}) {
|
|
2520
|
-
if (staticMode) return;
|
|
2542
|
+
if (staticMode || publicMode) return;
|
|
2521
2543
|
const env = gatesEnv();
|
|
2522
2544
|
if (!force && wsGates.env === env) return paintWorkspaceGates();
|
|
2523
2545
|
// Every read is numbered; only the newest one paints, so an answer for an
|
|
@@ -3388,8 +3410,89 @@ async function loadTgProgress({ fresh = false } = {}) {
|
|
|
3388
3410
|
}
|
|
3389
3411
|
function paintTgProgress() {
|
|
3390
3412
|
tgStampProgress(document.querySelector("#graph svg"), tgProgress, new Date());
|
|
3413
|
+
drawTgReads(); // #568: the run view's reads, once the change merged
|
|
3391
3414
|
renderTerragucciPanel();
|
|
3392
3415
|
}
|
|
3416
|
+
// #568: the reads edges between roots of one repo, drawn over the card view:
|
|
3417
|
+
// a dashed line from the reader (its data card when the checkout draws one,
|
|
3418
|
+
// else its root's cards) to the root it reads, behind the cards, plus a tag on
|
|
3419
|
+
// the reader's data card ("reads network · planned · 1 unknown"). From the
|
|
3420
|
+
// pull request's plan, or from the run view once merged. Not live; redrawn on
|
|
3421
|
+
// each render, so a card dragged since moves its line on the next one.
|
|
3422
|
+
function drawTgReads() {
|
|
3423
|
+
const svg = document.querySelector("#graph svg");
|
|
3424
|
+
if (!svg) return;
|
|
3425
|
+
for (const old of svg.querySelectorAll("[data-tg-reads]")) old.remove();
|
|
3426
|
+
if (!tgAnswer || tgAnswer.error || !tgAnswer.roots) return;
|
|
3427
|
+
const edges = tgReadEdges(tgAnswer, tgProgress);
|
|
3428
|
+
if (!edges.length) return;
|
|
3429
|
+
const toSvg = svg.getScreenCTM && svg.getScreenCTM();
|
|
3430
|
+
if (!toSvg) return;
|
|
3431
|
+
const inv = toSvg.inverse();
|
|
3432
|
+
// A card's box in the svg's own coordinates, whatever transform a layout put on it.
|
|
3433
|
+
const boxOf = (g) => {
|
|
3434
|
+
let b;
|
|
3435
|
+
try {
|
|
3436
|
+
b = g.getBBox();
|
|
3437
|
+
} catch {
|
|
3438
|
+
return null;
|
|
3439
|
+
}
|
|
3440
|
+
const ctm = g.getScreenCTM && g.getScreenCTM();
|
|
3441
|
+
if (!ctm || !b.width) return null;
|
|
3442
|
+
const m = inv.multiply(ctm);
|
|
3443
|
+
const pts = [[b.x, b.y], [b.x + b.width, b.y + b.height]].map(([x, y]) => new DOMPoint(x, y).matrixTransform(m));
|
|
3444
|
+
return { x: Math.min(pts[0].x, pts[1].x), y: Math.min(pts[0].y, pts[1].y), x2: Math.max(pts[0].x, pts[1].x), y2: Math.max(pts[0].y, pts[1].y) };
|
|
3445
|
+
};
|
|
3446
|
+
const union = (boxes) => (boxes.length ? { x: Math.min(...boxes.map((b) => b.x)), y: Math.min(...boxes.map((b) => b.y)), x2: Math.max(...boxes.map((b) => b.x2)), y2: Math.max(...boxes.map((b) => b.y2)) } : null);
|
|
3447
|
+
const rootBox = (path) => {
|
|
3448
|
+
const r = tgAnswer.roots.find((x) => x.path === path);
|
|
3449
|
+
if (!r) return null;
|
|
3450
|
+
const prefix = `${r.member}/${r.name}/`;
|
|
3451
|
+
return union([...svg.querySelectorAll("[data-node-id]")].filter((g) => (g.getAttribute("data-node-id") || "").startsWith(prefix)).map(boxOf).filter(Boolean));
|
|
3452
|
+
};
|
|
3453
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
3454
|
+
const layer = document.createElementNS(NS, "g");
|
|
3455
|
+
layer.setAttribute("data-tg-reads", "1");
|
|
3456
|
+
layer.setAttribute("pointer-events", "none");
|
|
3457
|
+
for (const e of edges) {
|
|
3458
|
+
const card = tgReadCardIds(tgAnswer, e).map((id) => svg.querySelector('[data-node-id="' + CSS.escape(id) + '"]')).find(Boolean);
|
|
3459
|
+
const from = (card && boxOf(card)) || rootBox(e.from);
|
|
3460
|
+
const to = rootBox(e.to);
|
|
3461
|
+
if (card) {
|
|
3462
|
+
const tag = document.createElementNS(NS, "text");
|
|
3463
|
+
tag.setAttribute("data-tg-reads", "tag");
|
|
3464
|
+
const rect = card.querySelector("rect");
|
|
3465
|
+
const local = rect && rect.getBBox ? rect.getBBox() : null;
|
|
3466
|
+
tag.setAttribute("x", String((local ? local.x : 0) + 10));
|
|
3467
|
+
tag.setAttribute("y", String((local ? local.y + local.height : 60) - 9));
|
|
3468
|
+
tag.setAttribute("font-size", "11");
|
|
3469
|
+
tag.setAttribute("font-weight", "600");
|
|
3470
|
+
tag.setAttribute("fill", e.unknown.length ? "var(--pending)" : "var(--muted)");
|
|
3471
|
+
tag.textContent = tgReadTag(e);
|
|
3472
|
+
const t = document.createElementNS(NS, "title");
|
|
3473
|
+
t.setAttribute("data-tg-reads", "title");
|
|
3474
|
+
t.textContent = tgReadLine(e) + "\n(from terragucci reports, not live)";
|
|
3475
|
+
card.append(t, tag);
|
|
3476
|
+
}
|
|
3477
|
+
if (!from || !to) continue;
|
|
3478
|
+
const line = document.createElementNS(NS, "line");
|
|
3479
|
+
line.setAttribute("x1", String((from.x + from.x2) / 2));
|
|
3480
|
+
line.setAttribute("y1", String((from.y + from.y2) / 2));
|
|
3481
|
+
line.setAttribute("x2", String((to.x + to.x2) / 2));
|
|
3482
|
+
line.setAttribute("y2", String((to.y + to.y2) / 2));
|
|
3483
|
+
line.setAttribute("stroke", e.unknown.length ? "var(--pending)" : "var(--muted)");
|
|
3484
|
+
line.setAttribute("stroke-width", "2");
|
|
3485
|
+
line.setAttribute("stroke-dasharray", e.outputs === "planned" ? "6 4" : "2 4");
|
|
3486
|
+
line.setAttribute("data-tg-read-from", e.from);
|
|
3487
|
+
line.setAttribute("data-tg-read-to", e.to);
|
|
3488
|
+
const t = document.createElementNS(NS, "title");
|
|
3489
|
+
t.textContent = tgReadLine(e);
|
|
3490
|
+
line.appendChild(t);
|
|
3491
|
+
layer.appendChild(line);
|
|
3492
|
+
}
|
|
3493
|
+
// Behind every card: first in the svg.
|
|
3494
|
+
svg.insertBefore(layer, svg.firstChild);
|
|
3495
|
+
}
|
|
3393
3496
|
function markTerragucciCards() {
|
|
3394
3497
|
const svg = document.querySelector("#graph svg");
|
|
3395
3498
|
tgStampProgress(svg, tgProgress, new Date()); // #511
|
|
@@ -3419,6 +3522,7 @@ function markTerragucciCards() {
|
|
|
3419
3522
|
title.textContent = marks.map((m) => markLine(m, now)).join("\n") + "\n(from terragucci reports, not live: the Terragucci tab links each run)";
|
|
3420
3523
|
g.append(title, tag);
|
|
3421
3524
|
}
|
|
3525
|
+
drawTgReads(); // #568
|
|
3422
3526
|
}
|
|
3423
3527
|
function markAge(marks) {
|
|
3424
3528
|
const newest = marks.map((m) => m.run.finished).sort().pop();
|
|
@@ -3470,6 +3574,7 @@ function renderTerragucciPanel() {
|
|
|
3470
3574
|
}
|
|
3471
3575
|
renderTerragucciLane(host, now);
|
|
3472
3576
|
if (tgProgress) host.appendChild(tgProgressSection(tgProgress, now, tgLink)); // #511
|
|
3577
|
+
host.appendChild(tgChangeSection(a, tgProgress, now, { link: tgLink, button, copy: copyToClipboard })); // #568
|
|
3473
3578
|
for (const w of waveCards(a, now)) {
|
|
3474
3579
|
const box = document.createElement("div");
|
|
3475
3580
|
box.className = "ws-gate tg-wave";
|
|
@@ -3708,6 +3813,7 @@ initYodelReport();
|
|
|
3708
3813
|
// the gate ledgers' history (#522). Read-only and dated; never live.
|
|
3709
3814
|
let ylHost = null;
|
|
3710
3815
|
async function initYodelLane() {
|
|
3816
|
+
if (publicMode) return; // #569: /api/sql-yodeler/lifecycle is refused in a public view
|
|
3711
3817
|
// #537: a static export carries the lane as a snapshot (src/export.ts), so it draws here too.
|
|
3712
3818
|
const project = await apiFetch("/api/project").then((r) => r.json()).catch(() => ({}));
|
|
3713
3819
|
if (!project.sqlYodeler) return;
|
|
@@ -3872,6 +3978,7 @@ let ciByComponent = new Map();
|
|
|
3872
3978
|
let ciForge = null; // the forge the pipeline was generated for (#164) — heads the inspect section
|
|
3873
3979
|
|
|
3874
3980
|
async function loadCi() {
|
|
3981
|
+
if (publicMode) return; // #569: /api/ci is refused in a public view
|
|
3875
3982
|
try {
|
|
3876
3983
|
const q = lensParams(new URLSearchParams(view.env ? { env: view.env } : {}));
|
|
3877
3984
|
const res = await apiFetch(`/api/ci?${q}`);
|
|
@@ -3896,6 +4003,7 @@ async function loadCi() {
|
|
|
3896
4003
|
let resourcesByComponent = {};
|
|
3897
4004
|
|
|
3898
4005
|
async function loadResources() {
|
|
4006
|
+
if (publicMode) return; // #569: /api/resources is refused in a public view
|
|
3899
4007
|
try {
|
|
3900
4008
|
const q = lensParams(new URLSearchParams(view.env ? { env: view.env } : {}));
|
|
3901
4009
|
const res = await apiFetch(`/api/resources?${q}`);
|
|
@@ -4116,7 +4224,7 @@ function renderDial() {
|
|
|
4116
4224
|
// Apply is the write step — omitted in a static export (observe + reconcile
|
|
4117
4225
|
// above are reads and stay).
|
|
4118
4226
|
const applying = applyProgress && applyProgress.status === "running";
|
|
4119
|
-
if (!staticMode) {
|
|
4227
|
+
if (!staticMode && !publicMode) {
|
|
4120
4228
|
track.appendChild(dialArrow());
|
|
4121
4229
|
const applyBtn = button(
|
|
4122
4230
|
applying ? "apply · running…" : "apply",
|
|
@@ -4134,7 +4242,7 @@ function renderDial() {
|
|
|
4134
4242
|
|
|
4135
4243
|
host.appendChild(track);
|
|
4136
4244
|
if (reconcileCache) host.appendChild(renderReconcileDetail(reconcileCache));
|
|
4137
|
-
if (applyPicker && !applying && !staticMode) host.appendChild(renderApplyPicker());
|
|
4245
|
+
if (applyPicker && !applying && !staticMode && !publicMode) host.appendChild(renderApplyPicker());
|
|
4138
4246
|
if (applyProgress && applyProgress.waves.length) host.appendChild(renderApplyProgress(applyProgress));
|
|
4139
4247
|
mountOperator(host);
|
|
4140
4248
|
mountPlayhead(host);
|
|
@@ -4154,7 +4262,7 @@ function mountOperator(host) {
|
|
|
4154
4262
|
{
|
|
4155
4263
|
approve: embedOpts.embed || previewMode || staticMode || terragucciRepo ? null : (op, gate) => approveConvergeGate(op, gate),
|
|
4156
4264
|
approver: projectInfo && projectInfo.approver,
|
|
4157
|
-
why: embedOpts.embed ? "host" : previewMode ? "preview" : "static",
|
|
4265
|
+
why: embedOpts.embed ? "host" : publicMode ? "public" : previewMode ? "preview" : "static",
|
|
4158
4266
|
// A static export has no server to ask, so it gets no history affordance
|
|
4159
4267
|
// at all rather than a button that can only fail.
|
|
4160
4268
|
...(staticMode ? {} : { history: () => toggleOperatorHistory() }),
|
|
@@ -4172,7 +4280,7 @@ function mountPlayhead(host) {
|
|
|
4172
4280
|
if (!hasPlayhead(runState)) return;
|
|
4173
4281
|
const box = document.createElement("div");
|
|
4174
4282
|
box.className = "run-playhead";
|
|
4175
|
-
renderPlayhead(box, runState, gateCard, { approve: embedOpts.embed || previewMode || staticMode || terragucciRepo ? null : (op, gate) => signal(op, gate), approver: projectInfo && projectInfo.approver, why: embedOpts.embed ? "host" : previewMode ? "preview" : "static" });
|
|
4283
|
+
renderPlayhead(box, runState, gateCard, { approve: embedOpts.embed || previewMode || staticMode || terragucciRepo ? null : (op, gate) => signal(op, gate), approver: projectInfo && projectInfo.approver, why: embedOpts.embed ? "host" : publicMode ? "public" : previewMode ? "preview" : "static" });
|
|
4176
4284
|
host.appendChild(box);
|
|
4177
4285
|
}
|
|
4178
4286
|
|
|
@@ -4409,6 +4517,7 @@ function renderReconcileDetail(r) {
|
|
|
4409
4517
|
}
|
|
4410
4518
|
|
|
4411
4519
|
async function loadReconcile() {
|
|
4520
|
+
if (publicMode) return; // #569: /api/reconcile is refused in a public view
|
|
4412
4521
|
try {
|
|
4413
4522
|
const q = lensParams(new URLSearchParams({ env: view.env }));
|
|
4414
4523
|
const res = await apiFetch(`/api/reconcile?${q}`);
|
|
@@ -4457,21 +4566,16 @@ function relabelEdgeChip(chip, kind, via) {
|
|
|
4457
4566
|
function wireEdgeHighlight(svgEl, ir) {
|
|
4458
4567
|
const edges = [...svgEl.querySelectorAll("g[data-edge-from]")];
|
|
4459
4568
|
if (!edges.length) return;
|
|
4460
|
-
// Reason per edge, keyed by from
|
|
4461
|
-
//
|
|
4462
|
-
//
|
|
4463
|
-
const viaOf =
|
|
4464
|
-
for (const e of ir.edges || []) {
|
|
4465
|
-
if (!e.viaAttr) continue;
|
|
4466
|
-
viaOf.set(`${e.from}|${e.to}`, e.viaAttr);
|
|
4467
|
-
viaOf.set(`${e.to}|${e.from}`, e.viaAttr);
|
|
4468
|
-
}
|
|
4569
|
+
// Reason per edge, keyed by from, to and kind (pinhole's edgeKey, #564), so
|
|
4570
|
+
// two kinds on one pair keep their own reason. pinhole stamps data-edge-via
|
|
4571
|
+
// itself; this fills it in where it did not.
|
|
4572
|
+
const viaOf = viaIndex(ir.edges || []);
|
|
4469
4573
|
for (const g of edges) {
|
|
4470
4574
|
const from = g.getAttribute("data-edge-from");
|
|
4471
4575
|
const to = g.getAttribute("data-edge-to");
|
|
4472
|
-
const via = g.getAttribute("data-edge-via") || viaOf.get(`${from}|${to}`);
|
|
4473
|
-
if (via) g.setAttribute("data-edge-via", via); // drives the dashed style
|
|
4474
4576
|
const kind = g.getAttribute("data-edge-kind") || "ref";
|
|
4577
|
+
const via = g.getAttribute("data-edge-via") || viaOf.get(edgeKey({ from, to, kind }));
|
|
4578
|
+
if (via) g.setAttribute("data-edge-via", via); // drives the dashed style
|
|
4475
4579
|
const cross = g.getAttribute("data-edge-cross") === "true";
|
|
4476
4580
|
if (!g.querySelector("path")) relabelEdgeChip(g, kind, via);
|
|
4477
4581
|
const title = document.createElementNS(SVGNS, "title");
|
|
@@ -4664,6 +4768,7 @@ function render(ir, svg, m) {
|
|
|
4664
4768
|
markTerragucciCards(); // #490: and what terragucci's reports found, dated
|
|
4665
4769
|
markYodelCards(); // #526: and SQL Yodeler's drift, dated
|
|
4666
4770
|
applyLayout(); // #228: last, so the hand-placed deltas ride on top of every other pass
|
|
4771
|
+
drawTgReads(); // #568: after the layout, so the reads lines meet the cards where they now sit
|
|
4667
4772
|
renderDial();
|
|
4668
4773
|
}
|
|
4669
4774
|
|
|
@@ -5280,17 +5385,24 @@ function userRect(el, toUser) {
|
|
|
5280
5385
|
*/
|
|
5281
5386
|
function edgeLabelOf(g) {
|
|
5282
5387
|
if (!(g.getAttribute("data-edge-via") || "").trim()) return null;
|
|
5283
|
-
|
|
5284
|
-
|
|
5388
|
+
// The pair and the kind (pinhole#128, #564): two kinds on one pair draw two
|
|
5389
|
+
// lines with a chip each, and each line keeps its own chip.
|
|
5390
|
+
const key = edgeKeyOf(g);
|
|
5285
5391
|
for (const cand of g.parentNode ? g.parentNode.children : []) {
|
|
5286
5392
|
if (cand === g || cand.tagName.toLowerCase() !== "g") continue;
|
|
5287
|
-
if (cand.
|
|
5393
|
+
if (!cand.hasAttribute("data-edge-from") || edgeKeyOf(cand) !== key) continue;
|
|
5288
5394
|
if (cand.hasAttribute("data-node-id") || cand.querySelector("path")) continue;
|
|
5289
5395
|
return cand;
|
|
5290
5396
|
}
|
|
5291
5397
|
return null;
|
|
5292
5398
|
}
|
|
5293
5399
|
|
|
5400
|
+
/** A drawn edge group's identity, pinhole's `edgeKey` over its stamped
|
|
5401
|
+
* attributes (no `data-edge-kind` is a reference). */
|
|
5402
|
+
function edgeKeyOf(g) {
|
|
5403
|
+
return edgeKey({ from: g.getAttribute("data-edge-from"), to: g.getAttribute("data-edge-to"), kind: g.getAttribute("data-edge-kind") || "ref" });
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5294
5406
|
/** Index the freshly rendered SVG: node groups, containment boxes, edge paths. */
|
|
5295
5407
|
function indexLayout(svgEl) {
|
|
5296
5408
|
const toUser = userSpaceMapper(svgEl);
|
|
@@ -5470,7 +5582,7 @@ async function pullServerLayout(lens) {
|
|
|
5470
5582
|
* read-only project, older behold) simply never pushes, and a failed push is
|
|
5471
5583
|
* not reported — the localStorage tier already has it. */
|
|
5472
5584
|
const pushServerLayout = debounce((lens, deltas) => {
|
|
5473
|
-
if (staticMode || !layoutServer.writable || layoutServer.lens !== lens) return;
|
|
5585
|
+
if (staticMode || publicMode || !layoutServer.writable || layoutServer.lens !== lens) return;
|
|
5474
5586
|
postServerLayout((url, init) => fetch(url, init), lens, deltas);
|
|
5475
5587
|
}, 600);
|
|
5476
5588
|
// A reload (or a tab closing) within the debounce window would otherwise drop
|
|
@@ -5978,6 +6090,7 @@ events.addEventListener("changed", () => {
|
|
|
5978
6090
|
// status dot + name + state, with the bring-up/reset/pipeline actions inline
|
|
5979
6091
|
// again (#73 moved those into ⌘K; they remain there too, via lastSubstrates).
|
|
5980
6092
|
async function loadSubstrates() {
|
|
6093
|
+
if (publicMode) return; // #569: /api/substrates is refused in a public view
|
|
5981
6094
|
try {
|
|
5982
6095
|
const { substrates } = await apiFetch("/api/substrates").then((r) => r.json());
|
|
5983
6096
|
renderSubstrates(substrates || []);
|
|
@@ -6020,7 +6133,7 @@ function renderSubstrates(subs) {
|
|
|
6020
6133
|
row.append(dot, name, status);
|
|
6021
6134
|
// Writes — none in a static export; the GitHub dispatch also respects the
|
|
6022
6135
|
// preview lock, mirroring paletteCommands()'s gating exactly.
|
|
6023
|
-
if (!staticMode) {
|
|
6136
|
+
if (!staticMode && !publicMode) {
|
|
6024
6137
|
if (s.bringUp) row.appendChild(actButton("bring up", () => bringUpSubstrate(s)));
|
|
6025
6138
|
if (s.name === "floci" && s.status === "up")
|
|
6026
6139
|
row.appendChild(actButton("reset", () => resetLocal(), "Reset the local emulator — wipes every stack, reboots, redeploys clean"));
|
|
@@ -6140,7 +6253,7 @@ function hideLoading() {
|
|
|
6140
6253
|
// ask. Without this guard the bundle logs a 404 on every page load and relies on
|
|
6141
6254
|
// the fetch failing to stay silent, which is an outcome rather than a rule.
|
|
6142
6255
|
async function refreshReadCost() {
|
|
6143
|
-
if (staticMode) return;
|
|
6256
|
+
if (staticMode || publicMode) return; // #569: /api/doctor is refused in a public view
|
|
6144
6257
|
try {
|
|
6145
6258
|
const res = await fetch("/api/doctor?reads=1");
|
|
6146
6259
|
if (!res.ok) return;
|
|
@@ -6261,7 +6374,7 @@ function runStatusTarget() {
|
|
|
6261
6374
|
return opsApply && opsApply.gate ? opsApply.name : null;
|
|
6262
6375
|
}
|
|
6263
6376
|
function pollRunStatus() {
|
|
6264
|
-
if (staticMode || runStatusInFlight) return;
|
|
6377
|
+
if (staticMode || publicMode || runStatusInFlight) return; // #569: a public serve refuses the op tools
|
|
6265
6378
|
const name = runStatusTarget();
|
|
6266
6379
|
if (!name) return;
|
|
6267
6380
|
// Only a durable run has gate state at all. A local run's playhead is
|
|
@@ -6290,7 +6403,7 @@ function pollRunStatus() {
|
|
|
6290
6403
|
* strip standing in for "behold couldn't ask".
|
|
6291
6404
|
*/
|
|
6292
6405
|
function pollOperatorStatus() {
|
|
6293
|
-
if (staticMode || operatorInFlight) return;
|
|
6406
|
+
if (staticMode || publicMode || operatorInFlight) return;
|
|
6294
6407
|
operatorInFlight = true;
|
|
6295
6408
|
apiFetch("/api/operator/status")
|
|
6296
6409
|
.then((r) => r.json())
|
|
@@ -6330,7 +6443,7 @@ function toggleOperatorHistory() {
|
|
|
6330
6443
|
* empty timeline must never stand in for "behold couldn't ask".
|
|
6331
6444
|
*/
|
|
6332
6445
|
function loadOperatorHistory() {
|
|
6333
|
-
if (staticMode || operatorHistory.loading) return;
|
|
6446
|
+
if (staticMode || publicMode || operatorHistory.loading) return;
|
|
6334
6447
|
operatorHistory = { ...operatorHistory, loading: true, error: null };
|
|
6335
6448
|
apiFetch("/api/operator/log")
|
|
6336
6449
|
.then((r) => r.json())
|
|
@@ -6504,7 +6617,7 @@ async function initActions() {
|
|
|
6504
6617
|
// #165: which envs deploy through a forge, and whether Deploy can dispatch
|
|
6505
6618
|
// them right now (`/api/project`'s executor block).
|
|
6506
6619
|
executorInfo = project.executor || {};
|
|
6507
|
-
previewMode = staticMode || !!project.previewMode; // static ⇒ read-only, no writes at all
|
|
6620
|
+
previewMode = staticMode || publicMode || !!project.previewMode; // static ⇒ read-only, no writes at all
|
|
6508
6621
|
// #517: a SQL Yodeler repo is guarded the same way.
|
|
6509
6622
|
const guarded = project.terragucci || project.sqlYodeler;
|
|
6510
6623
|
if (guarded) {
|
|
@@ -6517,6 +6630,16 @@ async function initActions() {
|
|
|
6517
6630
|
bar.appendChild(pill);
|
|
6518
6631
|
return; // no Deploy, no Run, no Approve: the pipeline is the only way to apply
|
|
6519
6632
|
}
|
|
6633
|
+
// #569: a public view. Nothing in the bar writes; the pill says why.
|
|
6634
|
+
if (publicMode || project.publicMode) {
|
|
6635
|
+
previewMode = true;
|
|
6636
|
+
const pill = document.createElement("span");
|
|
6637
|
+
pill.textContent = "● public view, read-only";
|
|
6638
|
+
pill.title = "behold serve --public: this page reads and follows; it starts nothing and approves nothing.";
|
|
6639
|
+
pill.style.cssText = "align-self:center;font:var(--t-caption)/1.4 var(--font-mono);color:var(--muted);border:1px solid var(--line);border-radius:var(--r-ctl);padding:2px 8px";
|
|
6640
|
+
bar.appendChild(pill);
|
|
6641
|
+
return;
|
|
6642
|
+
}
|
|
6520
6643
|
const { ops, adoptLexicons, autoSync, local, applyProgress: apInit, runState: runInit, operatorState: opInit } = await apiFetch("/api/ops")
|
|
6521
6644
|
.then((r) => r.json())
|
|
6522
6645
|
.catch(() => ({ ops: [], adoptLexicons: [] }));
|
|
@@ -6760,12 +6883,13 @@ function paletteCommands() {
|
|
|
6760
6883
|
const c = [];
|
|
6761
6884
|
|
|
6762
6885
|
// Reads — always available, even in a static export or the preview lock.
|
|
6763
|
-
|
|
6886
|
+
// #569: not in a public view — Refresh is a POST.
|
|
6887
|
+
if (!staticMode && !publicMode) c.push(["Re-check live (refresh drift)", () => refresh()]);
|
|
6764
6888
|
// #404: the expensive sibling, listed beside it and named for what it costs.
|
|
6765
6889
|
// Offered only with an env picked — there is no overlay to plan against
|
|
6766
6890
|
// without one — and disabled-with-its-reason otherwise, the convention the
|
|
6767
6891
|
// colour-mode and carve rows below already follow.
|
|
6768
|
-
if (!staticMode) {
|
|
6892
|
+
if (!staticMode && !publicMode) {
|
|
6769
6893
|
c.push(
|
|
6770
6894
|
view.env
|
|
6771
6895
|
? ["Re-check live with plan (attribute drift) — a full read of the account", () => refreshWithPlan()]
|
|
@@ -6862,7 +6986,7 @@ function paletteCommands() {
|
|
|
6862
6986
|
c.push([`target: ${t.endpoint}` + (view.target === t.endpoint ? " ✓" : ""), () => { view.target = t.endpoint; resetDialCaches(); load(); }]);
|
|
6863
6987
|
}
|
|
6864
6988
|
|
|
6865
|
-
if (staticMode || terragucciRepo) return c.map(([label, run]) => ({ label, run })); // no writes at all in a static export or a terragucci repo
|
|
6989
|
+
if (staticMode || publicMode || terragucciRepo) return c.map(([label, run]) => ({ label, run })); // no writes at all in a static export, a public view (#569) or a terragucci repo
|
|
6866
6990
|
|
|
6867
6991
|
// Deploy / write actions — previewMode hides exactly the git/PR write
|
|
6868
6992
|
// affordances the toolbar hid (Rollback, Sync, Run <op>); Apply all, Reset,
|
package/web/edge-kind.js
CHANGED
|
@@ -32,6 +32,37 @@ export function isCiKind(kind) {
|
|
|
32
32
|
return !!kind && kind !== "ref";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* An edge's identity, the same key pinhole 0.5.0's `edgeKey` gives
|
|
37
|
+
* (pinhole#128): `from>to` for a reference, `from>to>kind` otherwise. Two
|
|
38
|
+
* kinds on one pair (a GitLab job that both `needs` and `extends` another)
|
|
39
|
+
* are two edges, so anything the SPA looks up per edge goes through this
|
|
40
|
+
* (#564). Read off a drawn edge, pass `data-edge-kind`, which pinhole leaves
|
|
41
|
+
* off for "ref".
|
|
42
|
+
*/
|
|
43
|
+
export function edgeKey({ from, to, kind } = {}) {
|
|
44
|
+
return `${from}>${to}${isCiKind(kind) ? `>${kind}` : ""}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The `viaAttr` per edge key, for the drawn edges pinhole did not stamp
|
|
49
|
+
* `data-edge-via` on. The reverse direction is filled in as well, where
|
|
50
|
+
* nothing already holds it, since pinhole may paint an edge either way round
|
|
51
|
+
* relative to the IR.
|
|
52
|
+
*/
|
|
53
|
+
export function viaIndex(edges = []) {
|
|
54
|
+
const via = new Map();
|
|
55
|
+
for (const e of edges) {
|
|
56
|
+
if (!e.viaAttr) continue;
|
|
57
|
+
via.set(edgeKey(e), e.viaAttr);
|
|
58
|
+
}
|
|
59
|
+
for (const e of edges) {
|
|
60
|
+
const back = edgeKey({ from: e.to, to: e.from, kind: e.kind });
|
|
61
|
+
if (e.viaAttr && !via.has(back)) via.set(back, e.viaAttr);
|
|
62
|
+
}
|
|
63
|
+
return via;
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
/**
|
|
36
67
|
* The chip text, as chant's Mermaid and DOT writers label the edge
|
|
37
68
|
* (graph-mermaid.ts `edgeLabel`): a CI edge by its kind, plus the attribute
|
package/web/edge-kind.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { edgeLabel, edgeLegend, edgeTitle } from "./edge-kind.js";
|
|
2
|
+
import { edgeKey, edgeLabel, edgeLegend, edgeTitle, viaIndex } from "./edge-kind.js";
|
|
3
3
|
|
|
4
4
|
// The edges chant 0.124.20's GitHub lexicon writes (lexicons/github/src/
|
|
5
5
|
// graph-edges.ts) and one from its GitLab lexicon.
|
|
@@ -68,3 +68,33 @@ describe("edgeLegend (#553)", () => {
|
|
|
68
68
|
expect(edgeLegend([ref, { from: "a", to: "b", kind: "ref" }])).toEqual([]);
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
// #564: pinhole 0.5.0 keys an edge on from, to and kind (pinhole#128), so two
|
|
73
|
+
// kinds on one pair are two edges, each with its own chip and tooltip.
|
|
74
|
+
describe("edgeKey and viaIndex (#564)", () => {
|
|
75
|
+
it("gives the key pinhole's edgeKey gives: a reference keeps from>to", () => {
|
|
76
|
+
expect(edgeKey({ from: "api", to: "db", kind: "ref" })).toBe("api>db");
|
|
77
|
+
expect(edgeKey({ from: "api", to: "db" })).toBe("api>db");
|
|
78
|
+
expect(edgeKey({ from: "test", to: "build", kind: "needs" })).toBe("test>build>needs");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps each kind's reason on a pair joined twice", () => {
|
|
82
|
+
const via = viaIndex([
|
|
83
|
+
{ from: "test", to: "build", kind: "needs", viaAttr: "stage" },
|
|
84
|
+
{ from: "test", to: "build", kind: "extends", viaAttr: "extends" },
|
|
85
|
+
]);
|
|
86
|
+
expect(via.get(edgeKey({ from: "test", to: "build", kind: "needs" }))).toBe("stage");
|
|
87
|
+
expect(via.get(edgeKey({ from: "test", to: "build", kind: "extends" }))).toBe("extends");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("fills in the reverse direction without overwriting an edge drawn that way", () => {
|
|
91
|
+
const via = viaIndex([
|
|
92
|
+
{ from: "a", to: "b", kind: "ref", viaAttr: "role" },
|
|
93
|
+
{ from: "b", to: "a", kind: "ref", viaAttr: "policy_arn" },
|
|
94
|
+
{ from: "c", to: "d", kind: "ref", viaAttr: "zone_id" },
|
|
95
|
+
]);
|
|
96
|
+
expect(via.get("a>b")).toBe("role");
|
|
97
|
+
expect(via.get("b>a")).toBe("policy_arn");
|
|
98
|
+
expect(via.get("d>c")).toBe("zone_id");
|
|
99
|
+
});
|
|
100
|
+
});
|
package/web/kiosk.html
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>behold kiosk</title>
|
|
7
|
+
<link
|
|
8
|
+
rel="icon"
|
|
9
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='none' stroke='%23a6adc8' stroke-width='1.6'/%3E%3Ccircle cx='8' cy='8' r='2.2' fill='%23a6adc8'/%3E%3C/svg%3E"
|
|
10
|
+
/>
|
|
11
|
+
<style>
|
|
12
|
+
/* #569 step 2. The pre-paint fallback is index.html's: the default
|
|
13
|
+
theme's tokens, which initTheme() replaces with the viewer's theme. */
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #1e1e2e; --panel: #2e2f40; --line: #3b3c4a; --fg: #cdd6f4; --muted: #777a8c;
|
|
16
|
+
--edge: #56596d; --managed: #a6e3a1; --foreign: #f9e2af; --pending: #89b4fa;
|
|
17
|
+
--degraded: #f38ba8; --neutral: #6d7187; --runtime: #94e2d5;
|
|
18
|
+
--rule: #4e4f64; --focus: #a3b4d0; --active: #3d4660; --well: #11101f; --shadow: #050507;
|
|
19
|
+
--font-ui: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
20
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
html, body { margin: 0; background: var(--bg); color: var(--fg); font: 18px/1.45 var(--font-ui); }
|
|
24
|
+
#kiosk { max-width: 1100px; margin: 0 auto; padding: 2rem 1rem 3rem; display: grid; gap: 1.25rem; }
|
|
25
|
+
.k-kicker { font: 600 0.8rem var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
|
|
26
|
+
.k-title { margin: 0.2rem 0; font-size: 2rem; line-height: 1.2; }
|
|
27
|
+
.k-muted { color: var(--muted); margin: 0.25rem 0; }
|
|
28
|
+
section { background: var(--panel); border: 1px solid var(--rule); border-radius: 10px; padding: 1rem 1.25rem; }
|
|
29
|
+
.k-strip { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0; margin: 0; }
|
|
30
|
+
.k-stop { font: 0.85rem var(--font-mono); padding: 0.3rem 0.7rem; border-radius: 999px; border: 1px solid var(--rule); color: var(--muted); }
|
|
31
|
+
.k-done { color: var(--fg); border-color: var(--managed); }
|
|
32
|
+
.k-current { color: var(--bg); background: var(--pending); border-color: var(--pending); font-weight: 600; }
|
|
33
|
+
.k-failed { color: var(--bg); background: var(--degraded); border-color: var(--degraded); font-weight: 600; }
|
|
34
|
+
.k-label { font-size: 1.35rem; margin: 0.4rem 0 0; }
|
|
35
|
+
.k-step-waiting { border-color: var(--foreign); }
|
|
36
|
+
.k-step-failed, .k-step-stalled { border-color: var(--degraded); }
|
|
37
|
+
.k-step-applied { border-color: var(--managed); }
|
|
38
|
+
.k-next-line { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: baseline; margin: 0.4rem 0 0; font-size: 1.2rem; }
|
|
39
|
+
.k-action { font-family: var(--font-mono); }
|
|
40
|
+
.k-when { color: var(--foreign); font-family: var(--font-mono); }
|
|
41
|
+
.k-approve { margin-top: 0.75rem; }
|
|
42
|
+
.k-command { display: block; padding: 0.6rem 0.8rem; background: var(--well); border-radius: 6px; font: 0.95rem var(--font-mono); overflow-wrap: anywhere; user-select: none; }
|
|
43
|
+
.k-read { font-family: var(--font-mono); font-size: 0.95rem; margin: 0.3rem 0; }
|
|
44
|
+
.k-unknown { color: var(--foreign); }
|
|
45
|
+
.k-unreviewed { margin: 0.4rem 0; padding-left: 0.6rem; border-left: 3px solid var(--foreign); }
|
|
46
|
+
.k-waves { list-style: none; padding: 0; margin: 0.6rem 0; display: grid; gap: 0.5rem; }
|
|
47
|
+
.k-wave { border: 1px solid var(--rule); border-left-width: 4px; border-radius: 6px; padding: 0.5rem 0.8rem; background: var(--bg); }
|
|
48
|
+
.k-wave-title { font-family: var(--font-mono); }
|
|
49
|
+
.k-wave-fact { color: var(--muted); font-size: 0.9rem; }
|
|
50
|
+
.k-tone-ok { border-left-color: var(--managed); }
|
|
51
|
+
.k-tone-wave { border-left-color: var(--foreign); }
|
|
52
|
+
.k-tone-failed { border-left-color: var(--degraded); }
|
|
53
|
+
.k-tone-plan { border-left-color: var(--pending); }
|
|
54
|
+
.k-tone-none { border-left-color: var(--neutral); }
|
|
55
|
+
.k-foot { font-size: 0.8rem; }
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<main id="kiosk" aria-live="polite"></main>
|
|
60
|
+
<script type="module">
|
|
61
|
+
import { initTheme } from "/theme.js";
|
|
62
|
+
import { startKiosk } from "/kiosk.js";
|
|
63
|
+
initTheme();
|
|
64
|
+
startKiosk(document.getElementById("kiosk"));
|
|
65
|
+
</script>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|