diff --git a/docs/cockpit.md b/docs/cockpit.md index afeffc4..a45d40f 100644 --- a/docs/cockpit.md +++ b/docs/cockpit.md @@ -91,6 +91,38 @@ never-list/handoffs on all six), `test_stations_are_distinct_roles` (each role's signature phrase appears on its own station and on no other — no melting), `test_operator_probe_is_explicit_and_live`. +## The deck (`/deck`) — all roles live, in parallel, with the wizard + +The **deck** is the crew law made physical: a tmux-style grid of six +panes, one per role, all live at the same time — because a real crew +works in parallel, roles do not take turns existing. Each pane is an +independent viewport (an iframe onto that role's station in +chrome-stripped **pane mode**, `?pane=1`): it scrolls, reloads (⟳), and +zooms (⤢, tmux-style single-pane zoom) independently, and a tiny shim +keeps every link and form inside the pane (`pane=1` is re-carried), so +pressing «Probe now» in the operator pane runs the probe *in that pane*. +Pane mode strips the page chrome but keeps the READ-ONLY label and the +full station content — one source of truth, two shells. + +On the right rides the **wizard**: a ten-step guided first watch that +takes a newcomer by the hand through every role's real actions on the +live demo wallet — probe as the operator, read the queue and a refusal +as the proposer, find the dissenting seat as the bench, verify (and then +deliberately break) real sample evidence as the cryptographer +(`/inspect?sample=1` pre-fills `examples/wallet-evidence/`), check the +drift tripwire as the architect, then run the handoff lap. Each step +card is **camouflaged in the color of the role being lived** ("YOU ARE +THE OPERATOR"), and the matching pane **glows** — instruction and +instrument are bound by hue. Every step states what success looks like +and what was just learned. Step position is remembered per browser +session. + +Deck contract tests: `test_deck_serves_all_panes_and_wizard` (six live +panes + all six roles visited by the wizard + success criteria), +`test_pane_mode_is_chromeless_but_labeled` (no chrome, READ-ONLY label, +stay-in-pane shim), `test_inspect_sample_prefill`; the read-only byte +sweep covers `/deck` and pane routes. + ## The read-only guarantee The cockpit cannot approve, sign, unlatch, or modify custody state. It diff --git a/src/pacta/cli.py b/src/pacta/cli.py index aa29a64..dbb9a63 100644 --- a/src/pacta/cli.py +++ b/src/pacta/cli.py @@ -693,7 +693,8 @@ def cmd_wallet_cockpit(args: argparse.Namespace) -> int: server = serve(wallet_dir, host=args.host, port=args.port) host, port = server.server_address[0], server.server_address[1] print(f"warden cockpit (READ-ONLY) on http://{host}:{port} - Ctrl-C to stop") - print(f" first time? start at http://{host}:{port}/guide - every term explained") + print(f" the deck: http://{host}:{port}/deck - all six roles live, guided by the wizard") + print(f" the guide: http://{host}:{port}/guide - every term explained") try: server.serve_forever() except KeyboardInterrupt: diff --git a/src/pacta/deck.py b/src/pacta/deck.py new file mode 100644 index 0000000..b63d8bb --- /dev/null +++ b/src/pacta/deck.py @@ -0,0 +1,281 @@ +"""deck - the ops deck: every role station live in one tmux-style grid, +with the guided wizard rail. + +The deck is the crew law made physical: six panes, one per role, all +live at the same time - because in real life the roles act in parallel, +they do not take turns existing. Each pane is an independent viewport +(iframe) onto that role's station in chrome-stripped "pane mode"; panes +reload and zoom independently, tmux-style. + +On the right rides the WIZARD: a guided first watch that takes a +newcomer by the hand through every role's real actions on the live DEMO +wallet. Each step card is camouflaged in the color of the role being +lived, and the matching pane glows - instruction and instrument are +bound by hue, so the learner always knows where to act. + +Pure presentation: this module renders strings from the station model; +all live evidence stays inside the panes, which are ordinary cockpit +routes and therefore inherit the read-only guarantee wholesale. The +~60 lines of vanilla JS here do layout only (zoom, reload, step +navigation, glow) - they never touch wallet data. +""" +from __future__ import annotations + +from .stations import STATIONS +from .uikit import STYLE, esc + +_DECK_EXTRA_STYLE = """ + body.deckbody{max-width:none;margin:0;padding:0;height:100vh;display:flex; + flex-direction:column;overflow:hidden} + .deckbar{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap; + padding:.45rem .9rem;background:#1c2430;color:#e8ecf2;font-size:.82rem} + .deckbar a{color:#aebcf0;text-decoration:none} + .deckbar .mono{opacity:.75} + .deckgrid{flex:1;display:grid;grid-template-columns:minmax(0,1fr) 21.5rem; + min-height:0} + .deckmain{display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr; + gap:6px;padding:6px;min-height:0;overflow:auto} + @media(min-width:1500px){.deckmain{grid-template-columns:1fr 1fr 1fr}} + .pane{display:flex;flex-direction:column;border:1px solid var(--line); + border-left:4px solid var(--role);border-radius:6px;background:#fff; + min-height:13rem;min-width:0} + .pane header{display:flex;align-items:center;gap:.45rem;padding:.22rem .5rem; + font-size:.78rem;background:var(--roletint);border-radius:0 5px 0 0} + .pane header .q{color:var(--ink2);font-size:.7rem;font-style:italic; + overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1} + .pane header .monogram{min-width:1.5rem;height:1.5rem;line-height:1.5rem; + font-size:.68rem} + .paneacts{display:flex;gap:.25rem} + .paneacts button,.paneacts a{border:1px solid var(--line);background:#fff; + color:var(--ink);border-radius:4px;font-size:.72rem;line-height:1.25; + padding:.05rem .4rem;cursor:pointer;text-decoration:none} + .pane iframe{flex:1;border:0;width:100%;min-height:0;border-radius:0 0 5px 5px} + .pane.focus{outline:3px solid var(--role);outline-offset:-1px} + .deckmain.zoom .pane{display:none} + .deckmain.zoom .pane.zoomed{display:flex;grid-column:1/-1;grid-row:1/-1} + .wizard{border-left:1px solid var(--line);background:#fff;overflow-y:auto; + padding:.8rem .9rem;min-height:0} + .wizard h2{margin:.1rem 0 .3rem;font-size:1rem} + .wizlegend{display:flex;gap:.3rem;flex-wrap:wrap;margin:.4rem 0 .7rem} + .wizlegend span{font-size:.66rem;font-weight:700;border-radius:5px; + padding:.1rem .4rem;background:var(--roletint);color:var(--role)} + .wizstep{display:none;border-left:4px solid var(--role); + background:var(--roletint);border-radius:6px;padding:.65rem .8rem; + font-size:.86rem} + .wizstep.on{display:block} + .wizstep .rolechip{display:inline-block;font-weight:800;font-size:.7rem; + letter-spacing:.04em;color:var(--role);margin-bottom:.25rem} + .wizstep h3{margin:.1rem 0 .35rem;font-size:.92rem} + .wizstep .succ{margin:.45rem 0 0;font-size:.8rem} + .wizstep .succ b{color:var(--ok)} + .wizstep .learn{margin:.35rem 0 0;font-size:.8rem;color:var(--ink2); + font-style:italic} + .wizctl{display:flex;align-items:center;gap:.6rem;margin:.7rem 0 0} + .wizctl button{padding:.35rem .8rem;font-size:.84rem} + .wizctl button:disabled{opacity:.4;cursor:default} + #wizprog{font-size:.78rem;color:var(--ink2)} + @media(max-width:999px){ + body.deckbody{overflow:auto;height:auto} + .deckgrid{display:flex;flex-direction:column-reverse} + .deckmain{grid-template-columns:1fr} + .pane{min-height:22rem} + .wizard{border-left:0;border-bottom:2px solid var(--line)} + } +""" + +# The guided first watch. Each step: (role_id, title, do_html, success_html, +# learn_html). The step card wears the role's colors; the matching pane glows. +WIZARD_STEPS: list[tuple[str, str, str, str, str]] = [ + ("newcomer", "Welcome to the deck", + "Six panes, six roles, all live at the same time — a real crew works in " + "parallel, and so does this deck. The colors ARE the roles; this wizard " + "wears the color of whoever you are acting as, and that role's pane " + "glows. Nothing you do here can sign or change anything — every pane is " + "read-only evidence.", + "You can point at each pane and say what its role is for (the italic " + "line in each pane header helps).", + "One system, six distinct jobs — never one blurred job."), + ("operator", "Morning watch: probe everything", + "Act as the Operator (green pane): press " + "«Probe now» on the liveness board. Watch the rows fill " + "with live facts. Find the log head row and read its " + "tree_size — that number is the public transparency log " + "answering you, right now.", + "Every service row says alive with observed facts and latency; " + "the repo rows show HEAD commits.", + "Liveness is checked on demand, never assumed — and never silently in " + "the background."), + ("operator", "Read the wallet's own pulse", + "Still in green: scroll to Custody latch — it says " + "unlatched, and the panel explains what the latch would do. " + "Then Recorded history: this demo wallet carries " + "1 incident and 1 refusal receipt on file. You will " + "meet both in the next steps.", + "You found the latch state and the two counters without leaving the " + "green pane.", + "The operator reads state from evidence panels, not from memory."), + ("proposer", "Live the proposer: find your request", + "Switch hats — amber pane. One signing request sits in " + "the queue, awaiting device. Read its payload " + "fingerprint: that is the SHA-256 of the exact bytes the " + "offline signer would sign — and nothing else.", + "You can quote the first characters of the fingerprint of what would be " + "signed.", + "A proposal is precise, or it is nothing."), + ("proposer", "Read a refusal like a to-do list", + "Still amber: open Incidents (refusals) from the " + "station's instrument links. The demo refusal says " + "POLICY_DENIED, missing allowlisted destination, " + "with a remediation naming exactly what would make the " + "same request succeed.", + "You can say, in one sentence, what the proposer would fix before " + "retrying.", + "warden never says a bare «no» — every refusal is machine-readable " + "instructions."), + ("quorum", "Sit on the bench: find the dissenter", + "Now the indigo pane. Count the seats: four, each " + "built from a different verified codebase. The demo incident (you saw " + "its counter in step 3) records that risc0 answered " + "INVALID while the other three said OK — in a real wallet, that single " + "dissent freezes custody on the spot.", + "You found all four seats and can name the dissenting member.", + "One honest dissenter beats three comfortable agreements — that is the " + "whole bench."), + ("cryptographer", "Recompute — never trust", + "The purple pane: click «load the sample " + "evidence», then Verify (read-only). The " + "deployed verifier re-checks the signatures and the log inclusion in " + "front of you. Then delete one character from the receipt box and " + "verify again — watch it refuse, loudly.", + "First run: ACCEPTED with a diagnostics list. Broken run: " + "REJECTED, and the diagnostics name the exact failing check.", + "You never trusted this page — you recomputed it. That is the " + "cryptographer's entire posture."), + ("architect", "Zoom out: map versus territory", + "The slate pane: read the drift " + "tripwire — it just compared the two renderings of the estate " + "map, name by name, and they agree. Then open the Estate " + "map link and find this wallet's place among the repos, " + "services, and the two self-referential loops.", + "You can say where warden sits on the map, and what the tripwire would " + "catch.", + "The map is recomputed, never remembered — or it lies."), + ("operator", "The handoff lap — the team in one incident", + "Watch the whole crew move once, in your head, across the panes: the " + "proposer escalates a refusal → the operator " + "investigates and probes → the bench defends its dissent → " + "the cryptographer re-verifies the evidence → the " + "architect records what changed. Five roles touched one " + "incident — and not one of them did another's job.", + "You can retell the lap naming who hands what to whom.", + "Teamwork here is handoffs between distinct roles — never a blur."), + ("newcomer", "Graduation", + "Pick the station that felt most like you and open it full-size (the " + "↗ in its pane header). Read its Mission, its Duties — every one a " + "real command — and its «This station never…» list; the never-list is " + "the fastest way to understand a role. When you are ready for a real " + "wallet: pacta wallet init.", + "You have a station — and you know what it never does.", + "If any page confused you on this watch, that is a bug in the page, " + "not in you. Report it — that is the newcomer's superpower."), +] + + +def render_deck(wallet_dir: str) -> str: + """The full deck document: bar, pane grid, wizard rail, layout JS.""" + demo_badge = (' DEMO WALLET — custody-inert' + if "DEMO" in wallet_dir else "") + panes = "".join( + f'
' + f'
{s["monogram"]}' + f'{esc(s["name"])}' + f'{esc(s["question"])}' + f'' + f'' + f'' + f'' + f'
' + f'' + f'
' + for s in STATIONS) + legend = "".join( + f'' + f'{s["monogram"]} {esc(s["name"])}' + for s in STATIONS) + role_by_id = {s["id"]: s for s in STATIONS} + steps = "".join( + f'
' + f'{role_by_id[role_id]["monogram"]} · YOU ARE THE ' + f'{esc(role_by_id[role_id]["name"]).upper()}' + f'

{title}

' + f'
{do}
' + f'

Success looks like: {success}

' + f'

{learn}

' + f'
' + for role_id, title, do, success, learn in WIZARD_STEPS) + return ( + "" + "" + "warden deck — all stations live" + f"" + "" + "
warden deck" + "six stations, live in parallel" + "← Bridge" + "READ-ONLY" + f"{demo_badge}" + f"{esc(wallet_dir)}
" + "
" + f"
{panes}
" + "
" + "" + ) diff --git a/src/pacta/walletui.py b/src/pacta/walletui.py index 5cf99f9..f2eb544 100644 --- a/src/pacta/walletui.py +++ b/src/pacta/walletui.py @@ -55,6 +55,7 @@ from pathlib import Path from typing import Any, Callable from .attestation import load_attestation +from .deck import render_deck from .liveness import collect_liveness, render_liveness from .stations import STATION_BY_ID, STATIONS, render_bridge, render_station from .transparency import load_receipt, verify_receipt @@ -169,7 +170,8 @@ def _collect_drift() -> dict[str, Any]: # page shell - two-row navigation (stations / instruments), lead on every view # --------------------------------------------------------------------------- -_STATION_TABS = [("/", "Bridge", "the whole system at a glance")] + [ +_STATION_TABS = [("/", "Bridge", "the whole system at a glance"), + ("/deck", "Deck", "all stations live, side by side")] + [ (f"/station/{s['id']}", s["name"], sub) for s, sub in zip(STATIONS, [ "ask for signatures", "four seats, one answer each", "liveness + latch recovery", "recompute everything", @@ -224,6 +226,68 @@ def _tabs(items: list[tuple[str, str, str]], active: str) -> str: for href, label, sub in items) +def _pane_shell(title: str, body: str) -> str: + """Chrome-stripped render for deck panes: same content, no h1/banner/nav. + + A tiny script keeps navigation inside the pane (every same-origin link + and form submit re-carries ?pane=1), so a pane behaves like a tmux pane: + an independent, self-contained viewport onto the cockpit. + """ + return ( + "" + "" + f"{_esc(title)} — pane" + f"" + "" + f"{body}" + "
READ-ONLY pane — part of the " + "deck. Links stay inside this pane; " + "use the pane header's ↗ for the full page.
" + "" + "") + + +def _load_sample_evidence() -> tuple[dict[str, str] | None, str]: + """Pre-fill the inspector from examples/wallet-evidence (read-only).""" + root = Path(__file__).resolve().parents[2] / "examples" / "wallet-evidence" + attestations = sorted(root.glob("*.attestation.json")) + receipts = sorted(root.glob("*.receipt.json")) + key = root / "log.pub" + fallback_note = ( + "

No sample evidence found on this " + "machine (expected under examples/wallet-evidence/). Fetch " + "real evidence from the live log instead: pacta log-fetch --url " + "https://ltl.zkdefi.org --component dalek-ed25519-verified, then " + "paste the two files and the log's log.pub.

") + if not (attestations and receipts and key.exists()): + return None, fallback_note + by_stem = {p.name.removesuffix(".attestation.json"): p for p in attestations} + for rec in receipts: + stem = rec.name.removesuffix(".receipt.json") + if stem in by_stem: + note = ( + "

" + "sample loaded evidence for " + f"{_esc(stem)} is pre-filled below — press «Verify " + "(read-only)» to watch the deployed verifier run. Then delete one " + "character from the receipt box and verify again: watch it refuse, " + "and read which check failed.

") + return ({"attestation": by_stem[stem].read_text(encoding="utf-8"), + "receipt": rec.read_text(encoding="utf-8"), + "pubkey": key.read_text(encoding="utf-8")}, note) + return None, fallback_note + + def _page(title: str, active: str, body: str, wallet_dir: str) -> str: nav = (f'' @@ -537,7 +601,10 @@ def render_inspect(result: dict[str, Any] | None, "duration of the check, and the wallet directory is never written." "
  • If the input is malformed, the page shows FAILED TO " "VERIFY rather than guessing.
  • ") - + "
    " + + "

    Load the sample evidence " + "from examples/wallet-evidence/ — it fills the " + "three boxes below so you can watch a real verification.

    " + "" f"

    attestation.json — the signed verification statement
    " f"

    " f"

    receipt.json — the log's proof that the statement is recorded
    " @@ -573,6 +640,8 @@ def _bridge_strip(posture: dict[str, Any], airgap: dict[str, Any]) -> str: f"queue {pending} awaiting device" "liveness — probe from " "the Operator station" + "Open the deck → all six " + "stations live, side by side, with the guided wizard" "" + _provenance("Wallet.posture() + airgap listing (liveness only on demand)") ) @@ -922,29 +991,42 @@ def make_handler(wallet_dir: Path): parsed = urllib.parse.urlparse(self.path) route = parsed.path query = urllib.parse.parse_qs(parsed.query) + pane = query.get("pane", ["0"])[0] == "1" wd = str(wallet_dir) + + def page(title: str, active: str, body: str, status: int = 200) -> None: + self._send(_pane_shell(title, body) if pane + else _page(title, active, body, wd), status) + if route == "/": wallet = self._wallet() posture = collect("Wallet.posture()", wallet.posture) airgap = collect_airgap(wallet) body = render_bridge(_bridge_strip(posture, airgap), _bridge_live(posture, airgap)) - self._send(_page("bridge", "/", body, wd)) + page("bridge", "/", body) + elif route == "/deck": + self._send(render_deck(wd)) elif route == "/posture": wallet = self._wallet() body = render_posture(collect("Wallet.posture()", wallet.posture)) - self._send(_page("posture", "/posture", body, wd)) + page("posture", "/posture", body) elif route == "/queue": - body = render_queue(collect_airgap(self._wallet())) - self._send(_page("signature queue", "/queue", body, wd)) + page("signature queue", "/queue", + render_queue(collect_airgap(self._wallet()))) elif route == "/incidents": wallet = self._wallet() - body = render_incidents(collect_incidents(wallet), collect_refusals(wallet)) - self._send(_page("incidents", "/incidents", body, wd)) + page("incidents", "/incidents", + render_incidents(collect_incidents(wallet), + collect_refusals(wallet))) elif route == "/inspect": - self._send(_page("receipt inspector", "/inspect", render_inspect(None), wd)) + defaults, note = (None, "") + if query.get("sample", ["0"])[0] == "1": + defaults, note = _load_sample_evidence() + page("receipt inspector", "/inspect", + note + render_inspect(None, defaults)) elif route == "/guide": - self._send(_page("guide", "/guide", render_guide(), wd)) + page("guide", "/guide", render_guide()) elif route == "/estate": from .estateview import ESTATE_HTML self._send(ESTATE_HTML + _ESTATE_BACK_CHIP) @@ -952,22 +1034,23 @@ def make_handler(wallet_dir: Path): station_id = route.removeprefix("/station/") station = STATION_BY_ID.get(station_id) if station is None: - self._send(_page("not found", "", - "

    No such station. The " - "STATIONS row above lists the whole crew.
    ", - wd), 404) + page("not found", "", + "
    No such station. The STATIONS " + "row above lists the whole crew.
    ", 404) return probe = query.get("probe", ["0"])[0] == "1" body = render_station(station, self._station_embeds(station_id, probe)) - self._send(_page(f"{station['name']} station", route, body, wd)) + page(f"{station['name']} station", route, body) else: - self._send(_page("not found", "", - "
    No such view. The tabs above list " - "everything this cockpit can show.
    ", wd), 404) + page("not found", "", + "
    No such view. The tabs above list " + "everything this cockpit can show.
    ", 404) def do_POST(self) -> None: # noqa: N802 - route = urllib.parse.urlparse(self.path).path + parsed = urllib.parse.urlparse(self.path) + route = parsed.path + pane = urllib.parse.parse_qs(parsed.query).get("pane", ["0"])[0] == "1" if route != "/inspect": self._send("
    No such action.
    ", 404) return @@ -975,8 +1058,10 @@ def make_handler(wallet_dir: Path): form = urllib.parse.parse_qs(self.rfile.read(length).decode("utf-8")) fields = {k: form.get(k, [""])[0] for k in ("attestation", "receipt", "pubkey")} result = inspect_receipt(fields["attestation"], fields["receipt"], fields["pubkey"]) - self._send(_page("receipt inspector", "/inspect", - render_inspect(result, fields), str(wallet_dir))) + body = render_inspect(result, fields) + self._send(_pane_shell("receipt inspector", body) if pane + else _page("receipt inspector", "/inspect", body, + str(wallet_dir))) def log_message(self, fmt: str, *args: Any) -> None: # quiet return diff --git a/tests/test_walletui.py b/tests/test_walletui.py index dd76cc0..e7ac607 100644 --- a/tests/test_walletui.py +++ b/tests/test_walletui.py @@ -179,8 +179,9 @@ def test_server_routes_and_read_only_guarantee(tmp_path): thread.start() try: routes = ("/", "/posture", "/queue", "/incidents", "/inspect", "/guide", - "/station/proposer", "/station/quorum", "/station/operator", - "/station/operator?probe=1", "/station/cryptographer", + "/deck", "/station/proposer", "/station/quorum", + "/station/operator", "/station/operator?probe=1", + "/station/operator?pane=1", "/station/cryptographer", "/station/architect", "/station/newcomer") for route in routes: with urllib.request.urlopen(f"http://127.0.0.1:{port}{route}") as resp: @@ -406,6 +407,65 @@ def test_stations_are_distinct_roles(tmp_path): thread.join(timeout=5) +def test_deck_serves_all_panes_and_wizard(tmp_path): + """The deck: every role live in its own pane (tmux-style grid), plus the + color-camouflaged wizard rail that walks a newcomer through each role.""" + wallet = _seal_wallet(tmp_path) + server, thread, port = _serve(wallet.dir) + try: + status, body = _get(port, "/deck") + assert status == 200 + for sid in STATION_IDS: + assert f'src="/station/{sid}?pane=1"' in body, f"deck missing pane: {sid}" + assert "The wizard" in body and "your first watch" in body + assert body.count('class="wizstep"') >= 8, "wizard lost its steps" + for sid in STATION_IDS: + assert f'data-role="{sid}"' in body, f"wizard never visits {sid}" + assert "Success looks like" in body # every step tells the learner when they're done + assert "YOU ARE THE" in body # the role-camouflage chip + assert "READ-ONLY" in body + finally: + server.shutdown() + thread.join(timeout=5) + + +def test_pane_mode_is_chromeless_but_labeled(tmp_path): + """Panes are independent viewports: full station content, no page chrome, + still labeled read-only; links inside stay in pane mode via the shim.""" + wallet = _seal_wallet(tmp_path) + server, thread, port = _serve(wallet.dir) + try: + status, body = _get(port, "/station/operator?pane=1") + assert status == 200 + assert 'class="navrow"' not in body # no nav chrome inside a pane + assert "warden custody cockpit" not in body # no page h1 either + assert "Probe now" in body # the actual station content + assert "READ-ONLY pane" in body # but the guarantee stays visible + assert "searchParams.set('pane','1')" in body # the stay-in-pane shim + status, body = _get(port, "/inspect?pane=1") + assert status == 200 and 'class="navrow"' not in body + assert "Verify (read-only)" in body + finally: + server.shutdown() + thread.join(timeout=5) + + +def test_inspect_sample_prefill(tmp_path): + evidence = Path("examples") / "wallet-evidence" + if not (evidence / "log.pub").exists(): + pytest.skip("example wallet evidence not present") + wallet = _seal_wallet(tmp_path) + server, thread, port = _serve(wallet.dir) + try: + status, body = _get(port, "/inspect?sample=1") + assert status == 200 + assert "sample loaded" in body + assert "BEGIN PUBLIC KEY" in body # log.pub really pre-filled + finally: + server.shutdown() + thread.join(timeout=5) + + def test_operator_probe_is_explicit_and_live(tmp_path): """The liveness board never phones home on an ordinary page load; probes run only on the operator's explicit demand, then show per-target rows."""