@intentius/behold 0.28.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 +570 -262
- package/package.json +1 -1
- package/web/app.js +141 -19
- 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.",
|
package/web/app.js
CHANGED
|
@@ -18,6 +18,7 @@ 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";
|
|
@@ -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}`);
|
|
@@ -4659,6 +4768,7 @@ function render(ir, svg, m) {
|
|
|
4659
4768
|
markTerragucciCards(); // #490: and what terragucci's reports found, dated
|
|
4660
4769
|
markYodelCards(); // #526: and SQL Yodeler's drift, dated
|
|
4661
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
|
|
4662
4772
|
renderDial();
|
|
4663
4773
|
}
|
|
4664
4774
|
|
|
@@ -5472,7 +5582,7 @@ async function pullServerLayout(lens) {
|
|
|
5472
5582
|
* read-only project, older behold) simply never pushes, and a failed push is
|
|
5473
5583
|
* not reported — the localStorage tier already has it. */
|
|
5474
5584
|
const pushServerLayout = debounce((lens, deltas) => {
|
|
5475
|
-
if (staticMode || !layoutServer.writable || layoutServer.lens !== lens) return;
|
|
5585
|
+
if (staticMode || publicMode || !layoutServer.writable || layoutServer.lens !== lens) return;
|
|
5476
5586
|
postServerLayout((url, init) => fetch(url, init), lens, deltas);
|
|
5477
5587
|
}, 600);
|
|
5478
5588
|
// A reload (or a tab closing) within the debounce window would otherwise drop
|
|
@@ -5980,6 +6090,7 @@ events.addEventListener("changed", () => {
|
|
|
5980
6090
|
// status dot + name + state, with the bring-up/reset/pipeline actions inline
|
|
5981
6091
|
// again (#73 moved those into ⌘K; they remain there too, via lastSubstrates).
|
|
5982
6092
|
async function loadSubstrates() {
|
|
6093
|
+
if (publicMode) return; // #569: /api/substrates is refused in a public view
|
|
5983
6094
|
try {
|
|
5984
6095
|
const { substrates } = await apiFetch("/api/substrates").then((r) => r.json());
|
|
5985
6096
|
renderSubstrates(substrates || []);
|
|
@@ -6022,7 +6133,7 @@ function renderSubstrates(subs) {
|
|
|
6022
6133
|
row.append(dot, name, status);
|
|
6023
6134
|
// Writes — none in a static export; the GitHub dispatch also respects the
|
|
6024
6135
|
// preview lock, mirroring paletteCommands()'s gating exactly.
|
|
6025
|
-
if (!staticMode) {
|
|
6136
|
+
if (!staticMode && !publicMode) {
|
|
6026
6137
|
if (s.bringUp) row.appendChild(actButton("bring up", () => bringUpSubstrate(s)));
|
|
6027
6138
|
if (s.name === "floci" && s.status === "up")
|
|
6028
6139
|
row.appendChild(actButton("reset", () => resetLocal(), "Reset the local emulator — wipes every stack, reboots, redeploys clean"));
|
|
@@ -6142,7 +6253,7 @@ function hideLoading() {
|
|
|
6142
6253
|
// ask. Without this guard the bundle logs a 404 on every page load and relies on
|
|
6143
6254
|
// the fetch failing to stay silent, which is an outcome rather than a rule.
|
|
6144
6255
|
async function refreshReadCost() {
|
|
6145
|
-
if (staticMode) return;
|
|
6256
|
+
if (staticMode || publicMode) return; // #569: /api/doctor is refused in a public view
|
|
6146
6257
|
try {
|
|
6147
6258
|
const res = await fetch("/api/doctor?reads=1");
|
|
6148
6259
|
if (!res.ok) return;
|
|
@@ -6263,7 +6374,7 @@ function runStatusTarget() {
|
|
|
6263
6374
|
return opsApply && opsApply.gate ? opsApply.name : null;
|
|
6264
6375
|
}
|
|
6265
6376
|
function pollRunStatus() {
|
|
6266
|
-
if (staticMode || runStatusInFlight) return;
|
|
6377
|
+
if (staticMode || publicMode || runStatusInFlight) return; // #569: a public serve refuses the op tools
|
|
6267
6378
|
const name = runStatusTarget();
|
|
6268
6379
|
if (!name) return;
|
|
6269
6380
|
// Only a durable run has gate state at all. A local run's playhead is
|
|
@@ -6292,7 +6403,7 @@ function pollRunStatus() {
|
|
|
6292
6403
|
* strip standing in for "behold couldn't ask".
|
|
6293
6404
|
*/
|
|
6294
6405
|
function pollOperatorStatus() {
|
|
6295
|
-
if (staticMode || operatorInFlight) return;
|
|
6406
|
+
if (staticMode || publicMode || operatorInFlight) return;
|
|
6296
6407
|
operatorInFlight = true;
|
|
6297
6408
|
apiFetch("/api/operator/status")
|
|
6298
6409
|
.then((r) => r.json())
|
|
@@ -6332,7 +6443,7 @@ function toggleOperatorHistory() {
|
|
|
6332
6443
|
* empty timeline must never stand in for "behold couldn't ask".
|
|
6333
6444
|
*/
|
|
6334
6445
|
function loadOperatorHistory() {
|
|
6335
|
-
if (staticMode || operatorHistory.loading) return;
|
|
6446
|
+
if (staticMode || publicMode || operatorHistory.loading) return;
|
|
6336
6447
|
operatorHistory = { ...operatorHistory, loading: true, error: null };
|
|
6337
6448
|
apiFetch("/api/operator/log")
|
|
6338
6449
|
.then((r) => r.json())
|
|
@@ -6506,7 +6617,7 @@ async function initActions() {
|
|
|
6506
6617
|
// #165: which envs deploy through a forge, and whether Deploy can dispatch
|
|
6507
6618
|
// them right now (`/api/project`'s executor block).
|
|
6508
6619
|
executorInfo = project.executor || {};
|
|
6509
|
-
previewMode = staticMode || !!project.previewMode; // static ⇒ read-only, no writes at all
|
|
6620
|
+
previewMode = staticMode || publicMode || !!project.previewMode; // static ⇒ read-only, no writes at all
|
|
6510
6621
|
// #517: a SQL Yodeler repo is guarded the same way.
|
|
6511
6622
|
const guarded = project.terragucci || project.sqlYodeler;
|
|
6512
6623
|
if (guarded) {
|
|
@@ -6519,6 +6630,16 @@ async function initActions() {
|
|
|
6519
6630
|
bar.appendChild(pill);
|
|
6520
6631
|
return; // no Deploy, no Run, no Approve: the pipeline is the only way to apply
|
|
6521
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
|
+
}
|
|
6522
6643
|
const { ops, adoptLexicons, autoSync, local, applyProgress: apInit, runState: runInit, operatorState: opInit } = await apiFetch("/api/ops")
|
|
6523
6644
|
.then((r) => r.json())
|
|
6524
6645
|
.catch(() => ({ ops: [], adoptLexicons: [] }));
|
|
@@ -6762,12 +6883,13 @@ function paletteCommands() {
|
|
|
6762
6883
|
const c = [];
|
|
6763
6884
|
|
|
6764
6885
|
// Reads — always available, even in a static export or the preview lock.
|
|
6765
|
-
|
|
6886
|
+
// #569: not in a public view — Refresh is a POST.
|
|
6887
|
+
if (!staticMode && !publicMode) c.push(["Re-check live (refresh drift)", () => refresh()]);
|
|
6766
6888
|
// #404: the expensive sibling, listed beside it and named for what it costs.
|
|
6767
6889
|
// Offered only with an env picked — there is no overlay to plan against
|
|
6768
6890
|
// without one — and disabled-with-its-reason otherwise, the convention the
|
|
6769
6891
|
// colour-mode and carve rows below already follow.
|
|
6770
|
-
if (!staticMode) {
|
|
6892
|
+
if (!staticMode && !publicMode) {
|
|
6771
6893
|
c.push(
|
|
6772
6894
|
view.env
|
|
6773
6895
|
? ["Re-check live with plan (attribute drift) — a full read of the account", () => refreshWithPlan()]
|
|
@@ -6864,7 +6986,7 @@ function paletteCommands() {
|
|
|
6864
6986
|
c.push([`target: ${t.endpoint}` + (view.target === t.endpoint ? " ✓" : ""), () => { view.target = t.endpoint; resetDialCaches(); load(); }]);
|
|
6865
6987
|
}
|
|
6866
6988
|
|
|
6867
|
-
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
|
|
6868
6990
|
|
|
6869
6991
|
// Deploy / write actions — previewMode hides exactly the git/PR write
|
|
6870
6992
|
// affordances the toolbar hid (Rollback, Sync, Run <op>); Apply all, Reset,
|
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>
|