Files
turnstone/scripts/livepass.py
T
Patrick Buckley f8f7152d63 feat(ui): split view returns to the L-shell — PaneManager layout tree
Revives the split-pane feature retired with ui/static (step 6), rebuilt
on PaneManager: an optional binary layout tree (null = the one-pane-per-
tab behaviour, unchanged) renders visible panes as %-inset cells — no
reparenting, so live stream DOM, scroll state and media survive layout
changes. Tabs stay global: the active tab is the focused cell, a
backgrounded tab swaps into it, clicking inside a visible pane focuses
its cell, .shown marks visible-unfocused tabs. Separators resize by
pointer-capture drag and arrow keys (role=separator + aria-value*); the
tree persists in the working-set blob and rehydrate prunes leaves whose
pane did not restore. Limits: 6 cells, 200x150 cell minimums, denials
toast the manager's reason.

Affordance: Split right / Split down / Unsplit buttons in the tab-bar
tail replace the redundant [+] (the permanent Dashboard tab is the
launcher) — deliberately no contextmenu override this time. The dead
TS_APP.focusLauncher seam goes with it.

Measured chrome: the focused cell wears a 2px accent top bar (no thin
tinted ring clears 3:1 in both themes) plus a 55%-mix inset ring;
separators rest at --ink-4 with solid-accent hover/drag/focus; .shown
tabs carry an accent underline; the tail cluster is fenced and lifted
to --ink-3.

scripts/livepass.py grows a third surface: shell/livepass.html boots
the real shell.js + pane.js and drives ?split=right|down|three|none
(+ &theme=light), stamping SPLIT-READY-<cells> / SPLIT-FAILED-<reason>.
2026-06-12 00:11:08 -07:00

640 lines
28 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Build the livepass harnesses — render real hatch dialogs/shelves headlessly.
The livepass is how converted modal surfaces get verified without booting a
server: a minimal page that symlinks the REAL stylesheets and scripts, embeds
the REAL markup (extracted fresh from the index files at build time), stubs
``window.authFetch`` with canned fixtures, and drives surfaces via ``?open=``
query params — including click-driving submits so dead buttons can't hide
(the model-Save bug class).
Usage:
python3 scripts/livepass.py # build into /tmp/livepass/
python3 scripts/livepass.py --out DIR # build elsewhere
python3 scripts/livepass.py --serve 8950 # build + serve (Ctrl+C stops)
Then screenshot states (file:// blocks ES modules — always serve over http;
the reduced-motion flag is REQUIRED, entrance animations race the capture):
google-chrome --headless --disable-gpu --hide-scrollbars \\
--force-prefers-reduced-motion --window-size=1440,900 \\
--virtual-time-budget=9000 --screenshot=out.png \\
"http://localhost:8950/ui/livepass.html?open=new-ws&theme=light"
UI harness (?open=): new-ws · new-ws-fork · edit-title · delete-ws ·
revoke-mcp · ws-delete · ws-delete-results (+ &theme=light, &busy=1)
Console harness (?open=): schedule-create · schedule-edit · model-create ·
model-edit · model-save (drives a Save click; document.title becomes
PUT-OK-<n> on success) · policy · confirm · token
Plus &tall=1 (90-row users panel — the .admin-content scroll state; the
synthetic rows wrap to two lines, so judge overflow geometry, not row
cadence) · &scrolled=1 lands mid-list, &scrolled=bottom shows the 24px
scroll tail · &focuslast=1 focuses the last shelf-body control (the
displaced-dock regression probe: only .sh-body may scroll; head/foot stay
pinned). All combinable with ?open=. The console page wraps the fragment
in the REAL L-shell chain — pane-pinned height, interior scroller — so
scroll/dock geometry matches production; keep it that way. Body-level
dialogs (confirm/install/coord-delete) are injected as riders; a driven
?open= that ends with no open dialog stamps OPEN-FAILED-<state> into the
title instead of passing silently.
Governance surfaces (roles/HR/OGP/memory/skill) need fixtures that are not
canned yet — add a fixture + driver branch below when you need one.
Shell harness (?split=): right (default) · down · three · none — boots the
REAL shell.js + pane.js split-view engine over stubbed seams (two demo
conversational panes; ?split=three adds the Dashboard cell). + &theme=light.
document.title stamps SPLIT-READY-<visible cells> on success and
SPLIT-FAILED-<reason> when a driven split was denied — judge the focused
cell's top accent bar, the separators, and the .shown tab marker.
Rebuild after ANY markup change: the dialog blocks are embedded at build
time. Assets are symlinked, so CSS/JS edits are live on refresh.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI_INDEX = ROOT / "turnstone/ui/static/index.html"
CONSOLE_INDEX = ROOT / "turnstone/console/static/index.html"
def extract_dialogs(index: Path, only_id: str | None = None) -> list[str]:
"""Every <dialog class="hatch ..."> block, verbatim from the tree."""
html = index.read_text(encoding="utf-8")
blocks = []
for m in re.finditer(r"[ \t]*<dialog\s[^>]*class=\"[^\"]*\bhatch\b[^\"]*\"", html):
end = html.index("</dialog>", m.start()) + len("</dialog>")
block = html[m.start() : end]
if only_id and f'id="{only_id}"' not in block:
continue
blocks.append(block)
if not blocks:
raise SystemExit(f"no dialog.hatch blocks found in {index}")
return blocks
def extract_admin_fragment() -> str:
"""The console admin pane — the hatch-host all shelves live inside."""
html = CONSOLE_INDEX.read_text(encoding="utf-8")
start = html.index('<div id="admin-layout"')
end = html.index("<!-- /admin-layout -->") + len("<!-- /admin-layout -->")
return html[start:end]
def inject(template: str, marker: str, payload: str) -> str:
begin = template.index(f"<!-- {marker}:BEGIN -->") + len(f"<!-- {marker}:BEGIN -->")
end = template.index(f"<!-- {marker}:END -->")
return template[:begin] + "\n" + payload + "\n" + template[end:]
def symlink(link: Path, target: Path) -> None:
if link.is_symlink() or link.exists():
link.unlink()
link.symlink_to(target)
# --------------------------------------------------------------------------
# UI harness — the standalone app's dialog tier. Drives the REAL cards.js
# controller for the batch surfaces so the production code path renders.
# --------------------------------------------------------------------------
UI_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ui livepass</title>
<link rel="stylesheet" href="shared/base.css" />
<link rel="stylesheet" href="shared/ui-base.css" />
<link rel="stylesheet" href="shared/chat.css" />
<link rel="stylesheet" href="shared/conversation.css" />
<link rel="stylesheet" href="shared/cards.css" />
<link rel="stylesheet" href="static/style.css" />
<link rel="stylesheet" href="shared/shell.css" />
<link rel="stylesheet" href="shared/interactive.css" />
<link rel="stylesheet" href="shared/hatch.css" />
</head>
<body>
<!-- DIALOGS:BEGIN -->
<!-- DIALOGS:END -->
<div id="toast" role="status" aria-live="polite"></div>
<script>
window.authFetch = function (url) {
// One canned failure so the results view shows the mixed state.
var fail = url && url.indexOf("c3d4e5f6a1b2") !== -1;
return Promise.resolve({
ok: !fail,
status: fail ? 409 : 200,
headers: { get: function () { return "application/json"; } },
json: function () { return Promise.resolve({}); },
text: function () {
return Promise.resolve(
fail ? '{"error": "workstream is still running"}' : "",
);
},
});
};
window.showToast = function (msg) { console.log("toast:", msg); };
</script>
<script type="module">
import { openDialog, setBusy } from "./shared/hatch.js";
const q = new URLSearchParams(location.search);
if (q.get("theme") === "light")
document.documentElement.dataset.theme = "light";
const open = q.get("open") || "";
function fill(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
if (open === "new-ws" || open === "new-ws-fork") {
const dlg = document.getElementById("new-ws-dialog");
const canned = {
"new-ws-model": ["sonnet-4-6", "gpt-5-2", "qwen3-32b"],
"new-ws-judge-model": ["sonnet-4-6", "qwen3-32b"],
"new-ws-skill": ["code-review (default)", "deep-research"],
};
for (const id in canned) {
const s = document.getElementById(id);
for (const n of canned[id]) {
const o = document.createElement("option");
o.value = n;
o.textContent = n;
s.appendChild(o);
}
}
if (open === "new-ws-fork") {
fill("new-ws-title", "Fork workstream");
fill("new-ws-tag", "WS-FORK");
document.getElementById("new-ws-submit").textContent = "Fork";
const skillLabel = document.querySelector('label[for="new-ws-skill"]');
if (skillLabel) skillLabel.hidden = true;
document.getElementById("new-ws-skill").hidden = true;
document.getElementById("new-ws-attach-row").hidden = true;
}
openDialog(dlg);
} else if (open === "edit-title") {
document.getElementById("edit-title-input").value =
"lshell renovation pass 3";
openDialog(document.getElementById("edit-title-dialog"));
} else if (open === "delete-ws") {
fill(
"delete-ws-message",
'Delete "lshell renovation pass 3"? This cannot be undone.',
);
openDialog(document.getElementById("delete-ws-dialog"));
} else if (open === "revoke-mcp") {
fill(
"revoke-mcp-message",
"Revoke the connection to github? Tools that need this server will require re-consent.",
);
openDialog(document.getElementById("revoke-mcp-dialog"));
} else if (open === "ws-delete" || open === "ws-delete-results") {
// Drive the REAL shared controller so the dialog renders through
// the production code path (cards.js confirmSelection/confirm).
const mod = await import("./shared/cards.js");
const c = mod.createSavedCardsController({
idPrefix: "ws-delete",
buttonId: "ws-delete-btn",
noun: "workstream",
activateLabel: (s) => "Resume: " + (s.title || s.ws_id),
render: () => {},
buildDeleteRequest: (wsId) => ({
url: "/v1/api/workstreams/" + encodeURIComponent(wsId) + "/delete",
options: { method: "POST" },
}),
});
c.setItems([
{ ws_id: "a1b2c3d4e5f6", title: "lshell renovation pass 3" },
{ ws_id: "b2c3d4e5f6a1", title: "canonical trajectory spike" },
{
ws_id: "c3d4e5f6a1b2",
title:
"a very long workstream title that should wrap " +
"rather than punch out of the dialog box entirely",
},
]);
c.toggleAll();
c.confirmSelection();
if (open === "ws-delete-results") c.confirm();
}
if (q.get("busy")) {
const d = document.querySelector("dialog[open]");
if (d) setBusy(d, true);
}
</script>
</body>
</html>
"""
# --------------------------------------------------------------------------
# Console harness — the admin pane fragment hosts the shelves (token-created
# included); dialog-tier markup outside the fragment (confirm/install/
# coord-delete) is injected via the RIDERS marker in build().
# model-save click-drives the submit: document.title flips to PUT-OK-<n>.
# --------------------------------------------------------------------------
CONSOLE_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>console livepass</title>
<link rel="stylesheet" href="shared/base.css" />
<link rel="stylesheet" href="shared/ui-base.css" />
<link rel="stylesheet" href="console-static/style.css" />
<link rel="stylesheet" href="shared/shell.css" />
<link rel="stylesheet" href="shared/hatch.css" />
</head>
<body>
<!-- The REAL L-shell chain (shell.js buildShell + pane.js DOM, verbatim
class names) so the harness inherits production scroll geometry:
.pane-body > #view-admin > .admin-layout height-pin the hatch-host
and .admin-content is the pane's interior scroller. Never replace
this with bespoke height overrides — the clipped-pane / displaced-
shelf regressions were invisible to the harness precisely because
it used to pin #admin-layout with its own CSS. -->
<div class="app">
<aside class="rail" id="shell-rail">
<div class="rail-brand">
<button class="brand-home" type="button">
<div class="brand-mark"></div>
<span class="brand-name">turnstone</span>
<span class="brand-sub">console</span>
</button>
</div>
</aside>
<main class="content">
<div class="tabbar"></div>
<div class="panes">
<section class="pane">
<!-- no .pane-head: PaneManager._mount builds section.pane >
div.pane-body only -->
<div class="pane-body">
<div id="view-admin">
<!-- FRAGMENT:BEGIN -->
<!-- FRAGMENT:END -->
</div>
</div>
</section>
</div>
</main>
</div>
<!-- Body-level dialog tier (confirm / install / coord-delete): their
markup sits OUTSIDE #admin-layout in index.html, so the fragment
extraction misses them — build() injects every hatch dialog the
fragment does not already contain. -->
<!-- RIDERS:BEGIN -->
<!-- RIDERS:END -->
<div id="toast" role="status" aria-live="polite"></div>
<script>
(function () {
function reply(data) {
return Promise.resolve({
ok: true,
status: 200,
headers: { get: function () { return "application/json"; } },
json: function () { return Promise.resolve(data); },
text: function () { return Promise.resolve(JSON.stringify(data)); },
});
}
var SCHED = {
task_id: "t1", name: "nightly-digest", description: "Morning digest",
schedule_type: "cron", cron_expr: "0 6 * * 1,3,5", at_time: "",
target_mode: "auto", model: "fable-5", skill: "daily-digest",
initial_message: "Summarize overnight cluster activity.",
auto_approve: false, enabled: true,
notify_targets: [{ channel_type: "discord", channel_id: "8675309" }],
next_run: "2026-06-10T06:00:00",
};
var MODEL = {
definition_id: "def1", alias: "fable-5", model: "claude-fable-5",
provider: "anthropic", base_url: "", context_window: 200000,
capabilities: JSON.stringify({ supports_vision: true }),
enabled: true, temperature: null, max_tokens: null,
reasoning_effort: null, surface_persisted_reasoning: true,
replay_reasoning_to_model: false,
};
window.__putCount = 0;
window.authFetch = function (url, opts) {
var method = (opts && opts.method) || "GET";
if (method === "PUT" && url.indexOf("/model-definitions/def1") >= 0) {
window.__putCount++;
document.title = "PUT-OK-" + window.__putCount;
return reply({ ok: true });
}
if (url.indexOf("/schedules/preview") >= 0)
return reply({
valid: true, error: "",
next: [
"2026-06-10T06:00:00+00:00",
"2026-06-12T06:00:00+00:00",
"2026-06-15T06:00:00+00:00",
],
});
if (url.indexOf("/schedules/t1") >= 0) return reply(SCHED);
if (url.indexOf("/schedules") >= 0) return reply({ schedules: [SCHED] });
if (url.indexOf("/model-capabilities/known") >= 0)
return reply({ models: ["claude-fable-5", "claude-opus-4-8"] });
if (url.indexOf("/model-capabilities?") >= 0)
return reply({
known: true,
capabilities: {
context_window: 200000, supports_tools: true,
supports_streaming: true, supports_vision: true,
supports_web_search: true, supports_temperature: true,
supports_effort: true,
},
});
if (url.indexOf("/model-definitions/def1") >= 0) return reply(MODEL);
if (url.indexOf("/model-definitions") >= 0) return reply({ models: [] });
if (url.indexOf("/api/models") >= 0)
return reply({ models: [
{ alias: "fable-5", model: "claude-fable-5" },
{ alias: "gpt-5.2", model: "gpt-5.2" },
] });
if (url.indexOf("/skills") >= 0)
return reply({ skills: [{ name: "daily-digest" }, { name: "ops-runbook" }] });
if (url.indexOf("/policies") >= 0)
return reply({ policies: [
{ policy_id: "p1", name: "deny-rm", tool_pattern: "bash*rm*",
action: "deny", priority: 900, enabled: true },
{ policy_id: "p2", name: "default-ask", tool_pattern: "*",
action: "ask", priority: 0, enabled: true },
] });
return reply({});
};
window.showToast = function (m) {
console.log("toast:", m);
var t = document.getElementById("toast");
t.textContent = m;
t.classList.add("show");
};
})();
</script>
<script type="module" src="shared/utils.js"></script>
<script type="module" src="shared/hatch.js"></script>
<script src="console-static/admin.js"></script>
<script src="console-static/governance.js"></script>
<script>
window.addEventListener("load", function () {
var q = new URLSearchParams(location.search);
if (q.get("theme") === "light")
document.documentElement.dataset.theme = "light";
var open = q.get("open") || "";
// ?tall=1 — the scroll state: one panel visible with enough rows to
// overflow the pane, so a screenshot shows .admin-content scrolling
// (and a shelf staying docked above it). Mirrors switchAdminTab's
// one-panel-visible invariant without booting the tab loaders.
if (q.get("tall")) {
var panels = document.querySelectorAll(".admin-panel");
for (var i = 0; i < panels.length; i++)
panels[i].style.display =
panels[i].id === "admin-users" ? "" : "none";
// No fallback: a fragment rename must fail loudly, not misplace rows.
var rowHost = document.querySelector("#admin-users [role=list]");
rowHost.textContent = ""; // drop the static "Loading users…" stub
for (var r = 0; r < 90; r++) {
var row = document.createElement("div");
row.className = "admin-row"; // real row chrome — geometry tracks production
row.textContent =
"user-" + String(r).padStart(3, "0") + " \\u00b7 synthetic row";
rowHost.appendChild(row);
}
var content = document.getElementById("admin-content");
if (content && q.get("scrolled"))
content.scrollTop =
q.get("scrolled") === "bottom"
? content.scrollHeight // the 24px scroll-tail state
: content.scrollHeight / 2; // land mid-list
}
setTimeout(function () {
if (open === "schedule-create") showCreateScheduleModal();
else if (open === "schedule-edit") showEditScheduleModal("t1");
else if (open === "model-create") showCreateModelModal();
else if (open === "model-edit" || open === "model-save")
showEditModelModal("def1");
else if (open === "policy") {
window._govPolicies && _govPolicies.length === 0 &&
loadGovPolicies && loadGovPolicies();
showCreatePolicyModal();
} else if (open === "confirm")
showConfirmModal(
"Delete schedule",
"Delete nightly-digest? Its run history is removed with it. This cannot be undone.",
"Delete",
function () {},
);
else if (open === "token")
showTokenCreatedModal(
"tsk_9f2e41c7a8b35d60e1f4a2b89c7d3e5f6a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d",
);
if (open === "model-save")
setTimeout(function () {
document.getElementById("model-create-submit").click();
}, 900);
if (q.get("busy"))
setTimeout(function () {
var d = document.querySelector("dialog[open]");
if (d) window.TurnstoneHatch.setBusy(d, true);
}, 400);
// A driven state that ends with nothing open must fail LOUDLY in
// the screenshot pipeline, not render a quietly dialog-less page.
setTimeout(function () {
var top = document.querySelector("dialog[open]");
if (open && !top) document.title = "OPEN-FAILED-" + open;
// &focuslast=1 — the displaced-dock regression probe: focus the
// last form control in the shelf BODY (the visually-hidden
// toggle/radio inputs live there). Only .sh-body may scroll;
// the head/foot strips must stay pinned in the screenshot.
if (top && q.get("focuslast")) {
var els = top.querySelectorAll(
".sh-body input, .sh-body select, .sh-body textarea",
);
if (els.length) els[els.length - 1].focus();
}
}, 600);
}, 150);
});
</script>
</body>
</html>
"""
# --------------------------------------------------------------------------
# Shell harness — the SPLIT-VIEW surface. Unlike the ui/console pages (which
# embed extracted markup), this one boots the REAL shell.js + pane.js over
# stubbed classic seams and drives the split engine via ?split=. Two demo
# conversational panes give the cells plausible content; the Dashboard pane
# (registered by the shell itself) fills the third cell in ?split=three.
# Loud-failure rule: the title stamps SPLIT-READY-<cells> only when the built
# state matches the request — a denied/failed split stamps SPLIT-FAILED-<why>.
# --------------------------------------------------------------------------
SHELL_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>shell livepass</title>
<link rel="stylesheet" href="shared/base.css" />
<link rel="stylesheet" href="shared/ui-base.css" />
<link rel="stylesheet" href="shared/chat.css" />
<link rel="stylesheet" href="shared/conversation.css" />
<link rel="stylesheet" href="shared/cards.css" />
<link rel="stylesheet" href="static/style.css" />
<link rel="stylesheet" href="shared/shell.css" />
<link rel="stylesheet" href="shared/interactive.css" />
</head>
<body>
<div id="header"><div id="status-bar"></div><button id="theme-toggle">☾</button></div>
<div id="breadcrumb"></div>
<div id="main" style="padding: 18px">
<h2 style="margin: 0 0 8px">Dashboard</h2>
<p style="color: var(--ink-3)">
Launcher + workstreams table live here (livepass stub).
</p>
</div>
<div id="view-admin" style="display: none"></div>
<script>
window.TURNSTONE_SHELL_CAPS = { cluster: false, brandSub: "console" };
window.TS_APP = {
boot() {},
getClusterState() { return { nodes: {} }; },
onRender() {},
};
window.TS_ADMIN = {};
var q = new URLSearchParams(location.search);
if (q.get("theme") === "light")
document.documentElement.dataset.theme = "light";
</script>
<script type="module" src="shared/shell.js"></script>
<script type="module">
const q = new URLSearchParams(location.search);
for (let i = 0; i < 100 && !window.TS_SHELL; i++)
await new Promise((r) => setTimeout(r, 20));
if (!window.TS_SHELL) {
document.title = "SPLIT-FAILED-no-shell";
} else {
sessionStorage.clear();
const pm = window.TS_SHELL.panes;
const { ShellPane } = await import("./shared/pane.js");
const mkConv = (type, title, lines) => {
pm.registerType(type, () => {
const p = new ShellPane({ type, title });
p.tabMenu = () => [
{ label: "Close pane", action: () => pm.close(p.id) },
];
p.onMount = function () {
const wrap = document.createElement("div");
wrap.style.cssText =
"padding:16px;display:flex;flex-direction:column;gap:10px;overflow:auto;";
for (const [role, text] of lines) {
const d = document.createElement("div");
d.className = "msg " + role;
d.textContent = text;
wrap.append(d);
}
this.bodyEl.append(wrap);
};
return p;
});
};
mkConv("repro", "repro-flaky-suite", [
["user", "Track down the flaky retry in the channel gateway tests."],
[
"assistant",
"Three suspects so far — the debounce window in mcp_client, the " +
"circuit-breaker reset, and the socket-mode reconnect. Bisecting now.",
],
[
"assistant",
"Found it: the breaker reset races the stream pre-close. Patch incoming.",
],
]);
mkConv("relnotes", "draft-1.6.2-notes", [
["user", "Draft the 1.6.2 patch notes from the merged PR list."],
[
"assistant",
"Pulling #657#662. Consent badge, orphan verb, MCP task hygiene, " +
"the anthropic-compatible lane, and the mcp<2 cap.",
],
]);
pm.openPane("repro");
pm.openPane("relnotes");
const want = q.get("split") || "right";
let failed = null;
if (want !== "none") {
const r1 = pm.splitFocused("right");
if (!r1.ok) failed = r1.reason;
if (!failed && (want === "three" || want === "down")) {
const r2 = pm.splitFocused("down");
if (!r2.ok) failed = r2.reason;
}
}
const cells = document.querySelectorAll(
".panes > section.pane:not([hidden])",
).length;
document.title = failed
? "SPLIT-FAILED-" + failed
: "SPLIT-READY-" + cells;
}
</script>
</body>
</html>
"""
def build(out: Path) -> None:
ui = out / "ui"
con = out / "console"
ui.mkdir(parents=True, exist_ok=True)
con.mkdir(parents=True, exist_ok=True)
symlink(ui / "shared", ROOT / "turnstone/shared_static")
symlink(ui / "static", ROOT / "turnstone/ui/static")
blocks = extract_dialogs(UI_INDEX)
# the coordinator batch dialog shares the cards.js builder — ride along
blocks += extract_dialogs(CONSOLE_INDEX, only_id="coord-delete-dialog")
(ui / "livepass.html").write_text(
inject(UI_TEMPLATE, "DIALOGS", "\n".join(blocks)), encoding="utf-8"
)
print(f"{ui}/livepass.html — {len(blocks)} dialogs")
symlink(con / "shared", ROOT / "turnstone/shared_static")
symlink(con / "console-static", ROOT / "turnstone/console/static")
frag = extract_admin_fragment()
# Dialog-tier markup living OUTSIDE #admin-layout (confirm, install,
# coord-delete) would otherwise be silently absent — and ?open=confirm
# would screenshot a dialog-less page while the gate stayed green.
riders = [b for b in extract_dialogs(CONSOLE_INDEX) if b not in frag]
page = inject(CONSOLE_TEMPLATE, "FRAGMENT", frag)
page = inject(page, "RIDERS", "\n".join(riders))
(con / "livepass.html").write_text(page, encoding="utf-8")
print(f"{con}/livepass.html — admin fragment + {len(riders)} rider dialogs")
sh = out / "shell"
sh.mkdir(parents=True, exist_ok=True)
symlink(sh / "shared", ROOT / "turnstone/shared_static")
symlink(sh / "static", ROOT / "turnstone/console/static")
(sh / "livepass.html").write_text(SHELL_TEMPLATE, encoding="utf-8")
print(f"{sh}/livepass.html — split-view shell surface")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--out", type=Path, default=Path("/tmp/livepass"))
ap.add_argument("--serve", type=int, metavar="PORT")
args = ap.parse_args()
build(args.out)
if args.serve:
import functools
import http.server
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(args.out))
print(f"serving {args.out} on http://localhost:{args.serve}/ — Ctrl+C stops")
http.server.ThreadingHTTPServer(("127.0.0.1", args.serve), handler).serve_forever()
if __name__ == "__main__":
main()