Make swarm dashboard iPad kiosk safe
This commit is contained in:
parent
8288b457da
commit
b50d6510a5
2 changed files with 194 additions and 47 deletions
|
|
@ -666,6 +666,7 @@ function renderHtml(snapshot) {
|
|||
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #app { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
||||
html { height: -webkit-fill-available; }
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at 72% 12%, rgba(255, 174, 88, 0.14), transparent 24%),
|
||||
|
|
@ -699,21 +700,38 @@ function renderHtml(snapshot) {
|
|||
.shell {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
height: var(--app-height, 100vh);
|
||||
min-height: -webkit-fill-available;
|
||||
background: rgba(3, 7, 13, 0.72);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hud-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ctb-rail {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--rail);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
padding: 10px 274px 10px 10px;
|
||||
border-color: var(--line);
|
||||
background: rgba(3, 7, 13, 0.86);
|
||||
backdrop-filter: blur(18px);
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(142, 223, 176, 0.58) rgba(3, 7, 13, 0.32);
|
||||
}
|
||||
|
|
@ -762,27 +780,22 @@ function renderHtml(snapshot) {
|
|||
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
.workspace {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: var(--rail);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
grid-template-columns: minmax(230px, 0.82fr) minmax(360px, 1.15fr) minmax(420px, 1.38fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"inspector tree memory";
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
position: sticky;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
flex: 0 0 250px;
|
||||
z-index: 4;
|
||||
width: 250px;
|
||||
height: 96px;
|
||||
margin-left: auto;
|
||||
align-self: center;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 5px;
|
||||
|
|
@ -866,6 +879,7 @@ function renderHtml(snapshot) {
|
|||
.hud-fill.warn { background: linear-gradient(90deg, var(--amber), var(--coral)); }
|
||||
|
||||
.pane {
|
||||
position: absolute;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
|
|
@ -878,10 +892,28 @@ function renderHtml(snapshot) {
|
|||
overflow: hidden;
|
||||
}
|
||||
.pane-scroll { overflow: auto; }
|
||||
.inspector-pane { grid-area: inspector; grid-template-rows: auto auto auto minmax(0, 1fr); }
|
||||
.tree-pane { grid-area: tree; grid-template-rows: auto minmax(0, 1fr); }
|
||||
.inspector-pane {
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
width: calc(24% - 15px);
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr);
|
||||
}
|
||||
.tree-pane {
|
||||
left: calc(24% + 6px);
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
width: calc(34% - 18px);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
.tree-pane .mono { overflow-wrap: anywhere; }
|
||||
.memory-pane { grid-area: memory; grid-template-rows: auto minmax(0, 1fr); }
|
||||
.memory-pane {
|
||||
left: calc(58% + 6px);
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.kicker { color: var(--green); font-size: 0.74rem; text-transform: uppercase; }
|
||||
h1 { font-size: clamp(1.55rem, 3.4vw, 3rem); line-height: 0.98; }
|
||||
|
|
@ -923,6 +955,7 @@ function renderHtml(snapshot) {
|
|||
.inspector-hero h1, .inspector-hero p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.inspector-lore {
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
color: var(--muted);
|
||||
font-size: 0.83rem;
|
||||
}
|
||||
|
|
@ -966,6 +999,7 @@ function renderHtml(snapshot) {
|
|||
.channel-list, .state-tree, .detail-body {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
|
|
@ -987,7 +1021,7 @@ function renderHtml(snapshot) {
|
|||
border: 1px solid rgba(142, 223, 176, 0.12);
|
||||
border-radius: 8px;
|
||||
background: rgba(142, 223, 176, 0.045);
|
||||
overflow: clip;
|
||||
overflow: hidden;
|
||||
}
|
||||
details.state-node[open] { border-color: rgba(105, 226, 239, 0.22); }
|
||||
details.state-node > summary {
|
||||
|
|
@ -1049,6 +1083,7 @@ function renderHtml(snapshot) {
|
|||
min-height: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
color: rgba(239, 252, 248, 0.88);
|
||||
|
|
@ -1073,53 +1108,73 @@ function renderHtml(snapshot) {
|
|||
.badge.repo-face { background: var(--violet); }
|
||||
|
||||
@media (orientation: landscape) {
|
||||
.shell { grid-template-rows: var(--rail) minmax(0, 1fr); }
|
||||
.ctb-rail {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.workspace { grid-row: 2; grid-column: 1; }
|
||||
.turn-card { width: 76px; height: 96px; }
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
:root { --rail: 94px; }
|
||||
.shell { grid-template-columns: var(--rail) minmax(0, 1fr); }
|
||||
.ctb-rail {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: auto;
|
||||
bottom: 0;
|
||||
width: var(--rail);
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
.workspace {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"inspector memory"
|
||||
"tree memory";
|
||||
top: 0;
|
||||
left: var(--rail);
|
||||
}
|
||||
.inspector-pane {
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
bottom: auto;
|
||||
width: calc(45% - 16px);
|
||||
height: calc(43% - 15px);
|
||||
}
|
||||
.tree-pane {
|
||||
left: 10px;
|
||||
top: calc(43% + 5px);
|
||||
bottom: 10px;
|
||||
width: calc(45% - 16px);
|
||||
}
|
||||
.memory-pane {
|
||||
left: calc(45% + 4px);
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
.facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.turn-card { width: 72px; height: 96px; }
|
||||
.status-panel { flex-basis: 230px; width: 230px; height: 84px; }
|
||||
.status-panel {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
width: 230px;
|
||||
height: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.workspace { padding: 10px; gap: 10px; }
|
||||
.pane { padding: 12px; }
|
||||
.inspector-lore { display: none; }
|
||||
.status-panel { flex-basis: 210px; width: 210px; }
|
||||
.status-panel { width: 210px; }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) and (orientation: landscape) {
|
||||
.workspace {
|
||||
grid-template-columns: minmax(190px, 0.8fr) minmax(300px, 1.1fr) minmax(340px, 1.28fr);
|
||||
}
|
||||
.inspector-pane { width: 25%; }
|
||||
.tree-pane { left: calc(25% + 18px); width: calc(34% - 18px); }
|
||||
.memory-pane { left: calc(59% + 12px); }
|
||||
.inspector-lore { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1154,6 +1209,7 @@ function renderHtml(snapshot) {
|
|||
const badge = (value, label = value) => "<span class=\\"badge " + tone(value) + "\\">" + esc(String(label).replace(/_/g, " ")) + "</span>";
|
||||
|
||||
function render(snapshot) {
|
||||
setAppHeight();
|
||||
const participants = sortedParticipants(snapshot);
|
||||
if (!selectedIdentity || !participants.some((agent) => agent.identityId === selectedIdentity)) {
|
||||
selectedIdentity = (snapshot.upcomingTurns || [])[0]?.identityId || participants[0]?.identityId || null;
|
||||
|
|
@ -1164,7 +1220,9 @@ function renderHtml(snapshot) {
|
|||
if (selectedLeaf) selectedStatePath = selectedLeaf.path;
|
||||
app.innerHTML = [
|
||||
"<div class=\\"shell\\">",
|
||||
renderCtb(snapshot.upcomingTurns || [], snapshot, selected),
|
||||
"<canvas id=\\"hud-canvas\\" class=\\"hud-canvas\\"></canvas>",
|
||||
renderCtb(snapshot.upcomingTurns || []),
|
||||
renderStatusPanel(snapshot, selected),
|
||||
"<section class=\\"workspace\\" aria-label=\\"VoidBot swarm control\\">",
|
||||
renderInspector(snapshot, participants, selected),
|
||||
renderStateTree(selected),
|
||||
|
|
@ -1175,6 +1233,11 @@ function renderHtml(snapshot) {
|
|||
attachControls();
|
||||
attachSelection();
|
||||
attachStateSelection();
|
||||
requestAnimationFrame(drawHudCanvas);
|
||||
}
|
||||
|
||||
function setAppHeight() {
|
||||
document.documentElement.style.setProperty("--app-height", window.innerHeight + "px");
|
||||
}
|
||||
|
||||
function sortedParticipants(snapshot) {
|
||||
|
|
@ -1187,11 +1250,11 @@ function renderHtml(snapshot) {
|
|||
});
|
||||
}
|
||||
|
||||
function renderCtb(turns, snapshot, selected) {
|
||||
function renderCtb(turns) {
|
||||
return "<nav class=\\"ctb-rail\\" aria-label=\\"Upcoming CTB turns\\">" + turns.map((turn) => {
|
||||
const klass = ["turn-card", turn.identityId === selectedIdentity ? "selected" : "", turn.activeJobId ? "active-turn" : "", turn.pendingMentionCount > 0 ? "mention-turn" : ""].filter(Boolean).join(" ");
|
||||
return "<button class=\\"" + klass + "\\" type=\\"button\\" data-select=\\"" + esc(turn.identityId) + "\\" title=\\"" + esc(turn.displayName + " / " + minutes(turn.nextTurnInMinutes)) + "\\">" + avatarHtml(turn) + "<strong>" + esc(turn.displayName) + "</strong><span>" + esc(minutes(turn.nextTurnInMinutes)) + "</span>" + (turn.shuffleReason ? "<span class=\\"turn-reason\\">" + esc(turn.shuffleReason) + "</span>" : "") + "</button>";
|
||||
}).join("") + renderStatusPanel(snapshot, selected) + "</nav>";
|
||||
}).join("") + "</nav>";
|
||||
}
|
||||
|
||||
function renderInspector(snapshot, participants, agent) {
|
||||
|
|
@ -1383,6 +1446,86 @@ function renderHtml(snapshot) {
|
|||
: "<span class=\\"avatar\\">" + esc(initials(agent.displayName)) + "</span>";
|
||||
}
|
||||
|
||||
function drawHudCanvas() {
|
||||
const canvas = document.getElementById("hud-canvas");
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const ratio = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
|
||||
const width = Math.max(1, Math.floor(rect.width * ratio));
|
||||
const height = Math.max(1, Math.floor(rect.height * ratio));
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
|
||||
const bg = ctx.createLinearGradient(0, 0, rect.width, rect.height);
|
||||
bg.addColorStop(0, "#03070d");
|
||||
bg.addColorStop(0.55, "#07110f");
|
||||
bg.addColorStop(1, "#091712");
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, rect.width, rect.height);
|
||||
|
||||
ctx.globalAlpha = 0.26;
|
||||
ctx.strokeStyle = "rgba(105,226,239,0.16)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < rect.width; x += 32) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 0.5, 0);
|
||||
ctx.lineTo(x + 0.5, rect.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < rect.height; y += 24) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y + 0.5);
|
||||
ctx.lineTo(rect.width, y + 0.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
document.querySelectorAll(".pane, .status-panel").forEach((panel) => {
|
||||
const r = panel.getBoundingClientRect();
|
||||
drawPanelChrome(ctx, r.left, r.top, r.width, r.height);
|
||||
});
|
||||
}
|
||||
|
||||
function drawPanelChrome(ctx, x, y, width, height) {
|
||||
const radius = 8;
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(0,0,0,0.45)";
|
||||
ctx.shadowBlur = 22;
|
||||
ctx.shadowOffsetY = 10;
|
||||
roundedRect(ctx, x, y, width, height, radius);
|
||||
ctx.fillStyle = "rgba(6,15,19,0.34)";
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = "rgba(105,226,239,0.2)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(255,174,88,0.16)";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 10, y + 1.5);
|
||||
ctx.lineTo(x + Math.min(width - 10, 84), y + 1.5);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function roundedRect(ctx, x, y, width, height, radius) {
|
||||
const r = Math.min(radius, width / 2, height / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + width - r, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
||||
ctx.lineTo(x + width, y + height - r);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
||||
ctx.lineTo(x + r, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||
}
|
||||
|
||||
function attachSelection() {
|
||||
document.querySelectorAll("[data-select]").forEach((element) => {
|
||||
element.addEventListener("click", () => {
|
||||
|
|
@ -1452,8 +1595,11 @@ function renderHtml(snapshot) {
|
|||
|
||||
render(initialSnapshot);
|
||||
setInterval(refresh, 10000);
|
||||
window.addEventListener("orientationchange", () => setTimeout(refresh, 120));
|
||||
window.addEventListener("resize", () => window.requestAnimationFrame(() => refresh()));
|
||||
window.addEventListener("orientationchange", () => setTimeout(refresh, 180));
|
||||
window.addEventListener("resize", () => window.requestAnimationFrame(() => {
|
||||
setAppHeight();
|
||||
drawHudCanvas();
|
||||
}));
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -334,3 +334,4 @@
|
|||
{"ts":"2026-05-24T23:05:48Z","type":"implementation","status":"ok","note":"Operator absence now drives Face sleep projection: when idle cooling reaches nap mode, repo Face sleep cycles are forced/extended into napping with operator-absence-dreaming and memory-distillation themes before speaking pressure, CTB eligibility, and nap memory maintenance are computed."}
|
||||
{"ts":"2026-05-24T23:06:38Z","type":"implementation","status":"ok","note":"Rebuilt the local swarm dashboard into a CTB-owned four-column SPA: Face inspector, typed Face-state tree, selected memory/detail viewer, and command/watchdog column. The page now reads registered Face .cc state through the typed loader for display-only memory browsing while cadence/manual-turn controls remain the only writers."}
|
||||
{"ts":"2026-05-24T23:19:32Z","type":"implementation","status":"ok","note":"Adjusted the local swarm dashboard layout from iPad screenshot evidence: watchdog HUD is now a sticky module in the CTB rail, controls moved to the top of the Face inspector, agent facts became game-style stat bars, and the Face state tree gets the freed column width with readable wrapping plus selected-path expansion."}
|
||||
{"ts":"2026-05-25T00:18:11Z","type":"implementation","status":"ok","note":"Rebuilt the local swarm dashboard into an iPad Safari kiosk renderer: fixed absolute panel geometry replaces grid ownership, a canvas chrome layer paints the HUD surface, JS-owned viewport height avoids 100vh Safari drift, and CTB/HUD/inspector/tree/detail panels stay in viewport under browser verification."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue