diff --git a/WALLET.md b/WALLET.md index 9d4d147..c888960 100644 --- a/WALLET.md +++ b/WALLET.md @@ -146,18 +146,23 @@ pacta wallet mcp --wallet ./my-warden # stdio JSON-RPC MCP server ## The custody cockpit (human surface, read-only) `pacta wallet cockpit --wallet
This board has not probed yet — probes run only when " + "you press the button, so the cockpit never phones home on an ordinary " + "page load. Press «Probe now» to check every service and repo live.
" + "") + return (f"git rev-parse / "
+ "git status: present, at which commit, clean or "
+ "dirty.ATTENTION not alive: '
+ + ", ".join(f"{esc(x)}" for x in down) + "
all probed targets alive
') + return ( + f"| service | state | " + f"observed | expected | latency |
|---|
| repo | state | " + f"observed | expected | took |
|---|
request_signature "
+ "tool on the wallet's MCP surface. A human drives the same surface over "
+ "stdio (the JSON-RPC lines are in WALLET.md).",
+ "pacta wallet mcp --wallet WALLET_DIR\n# then call the request_signature tool"),
+ ("Track your request",
+ "Your request appears in the Queue instrument (embedded below) as "
+ "«awaiting device» until the offline signer answers.",
+ None),
+ ("On refusal: read the receipt, fix, retry",
+ "A refusal receipt names the rule (code), what was missing, and "
+ "the remediation. It is a to-do list, not a verdict on you.",
+ None),
+ ("On success: verify what you got",
+ "Never forward a signature you have not checked. For a Solana transaction, "
+ "the quorum re-verifies every signature locally:",
+ "pacta wallet treasury-verify --wallet WALLET_DIR --tx-file tx.bin"),
+ ],
+ "never": [
+ "Approve or verify the evidence behind your own proposal — that is the "
+ "Quorum bench's seat, and the whole point is that it is not yours.",
+ "Touch the air-gap device or its outbox/inbox files — the Operator owns "
+ "that walk.",
+ "Clear a latch, or edit policy.json to make your own request "
+ "fit — policy changes are the Operator's deliberate, recorded act.",
+ ],
+ "receives": ["a signature, or a refusal receipt — from the wallet",
+ "custody posture answers — from the Operator"],
+ "delivers": ["fingerprinted signing requests — to the signing firewall",
+ "escalations after repeated refusals — to the Operator"],
+ "instruments": [("/queue", "Queue"), ("/incidents", "Incidents (refusals)")],
+ },
+ {
+ "id": "quorum", "name": "Quorum bench", "monogram": "QM",
+ "hue": "#3b4d8f", "tint": "#eef0f7",
+ "question": "Would I stake custody on this evidence? Four seats, one answer each.",
+ "card": "Four independent verifier seats; unanimity admits, one dissent latches.",
+ "lead": ("You are at the Quorum bench — four seats, one per "
+ "independently built verifier (dalek, anza, risc0, betrusted). Each seat "
+ "answers for itself. The bench never averages: unanimity admits a "
+ "component, a single dissent freezes custody. Your value is your "
+ "independence."),
+ "mission": ("Hold an independent verdict on every piece of cryptographic "
+ "evidence the wallet is asked to trust. Your seat's judgment must "
+ "come from your seat's binary, built from your seat's verified "
+ "sources — nobody else's."),
+ "duties": [
+ ("Know your seat",
+ "Which verified repository you are built from, at which commit, with which "
+ "binary fingerprint — the live bench roster is embedded below.",
+ None),
+ ("Rebuild your member from pinned proven sources",
+ "When sources or toolchains move, rebuild from the pinned fork checkouts "
+ "and let the capsule re-pin your fingerprint:",
+ "pacta wallet build-quorum --sources-root FORK_CHECKOUTS_DIR"),
+ ("Guard your independence",
+ "A shared toolchain is a shared bug. Do not copy another seat's build "
+ "artifacts, caches, or patches — four seats that agree because they are "
+ "secretly one seat protect nothing.",
+ None),
+ ("On divergence: your dissent worked",
+ "If your seat says INVALID while others say OK, custody latches and an "
+ "incident is written. That is the system succeeding, not you failing. Read "
+ "the incident (embedded on the Incidents instrument) and defend your "
+ "verdict to the Operator.",
+ None),
+ ],
+ "never": [
+ "Propose a request — the bench judges evidence, it never originates spends.",
+ "Vote another seat's verdict, or harmonize before answering — the bench "
+ "never averages; unanimity or latch.",
+ "Clear a latch your own dissent caused — the Operator investigates; you are "
+ "a witness, not the judge of your own alarm.",
+ ],
+ "receives": ["component evidence (attestations + receipts) — from the wallet's "
+ "inbound boundary",
+ "rebuilt source workspaces — from the Architect's pinned forks"],
+ "delivers": ["a unanimous admit, or a latch-tripping dissent — to the wallet",
+ "divergence incidents — to the Operator"],
+ "instruments": [("/posture", "Posture (bench roster)"), ("/incidents", "Incidents")],
+ },
+ {
+ "id": "operator", "name": "Operator", "monogram": "OP",
+ "hue": "#1e7f4f", "tint": "#e2f2e9",
+ "question": "Is everything that should be running, running — and is custody unfrozen?",
+ "card": "Watches liveness of every service and repo; owns latch recovery.",
+ "lead": ("You are at the Operator station. You watch the "
+ "liveness of everything — every service, every repo, the wallet's own "
+ "health — and you own the emergency procedures. When the latch trips, "
+ "everyone else stops and you start."),
+ "mission": ("Keep the estate observably alive: probe the public services, check "
+ "the local repos, re-verify the wallet daily, advance the log pin, "
+ "and run latch recovery by the book. You are the only station that "
+ "may clear a latch — deliberately, with a permanent written note."),
+ "duties": [
+ ("Daily watch",
+ "Press «Probe now» on the liveness board below — it re-checks every "
+ "service and repo live, on demand. Then re-verify the wallet from the "
+ "command line:",
+ "pacta wallet status --wallet WALLET_DIR\npacta wallet verify-ledger --wallet WALLET_DIR"),
+ ("Advance the log pin",
+ "Fetch the latest signed head of the transparency log, verify signature "
+ "and consistency against your pinned size, and advance the pin — this is "
+ "the split-view/rollback defense:",
+ "pacta sth-refresh --url https://ltl.zkdefi.org \\\n --sth-store sth-store.json --log-public-key log.pub"),
+ ("Witness the published log",
+ "Audit a full clone: recompute every prefix root, check every historical "
+ "signed head. An unwatched log is safe only on paper — you are the watcher:",
+ "pacta witness-audit --published-dir path/to/lean-transparency-log"),
+ ("On latch: run the book",
+ "Follow docs/runbook-latch.md step by step. Only when the "
+ "cause is understood and fixed, clear the latch — the note is recorded "
+ "permanently in the ledger:",
+ 'pacta wallet unlatch --wallet WALLET_DIR --note "root cause and fix"'),
+ ],
+ "never": [
+ "Clear a latch without a written root cause — the CLI refuses an empty "
+ "note, and the ledger keeps it forever.",
+ "Rewrite log history: never force-push the log repo, never edit or delete "
+ "under entries/, never re-sign a published head, never "
+ "backdate. The log is append-only or it is nothing.",
+ "Override the bench: if the quorum diverged, the answer is investigation, "
+ "not a fifth vote.",
+ "Sign or propose — you hold the brakes, not the pen.",
+ ],
+ "receives": ["incidents and latch events — from the wallet and the Quorum bench",
+ "escalations — from the Proposer"],
+ "delivers": ["unlatch decisions with permanent notes — to the wallet ledger",
+ "outage and repair notes — to the Architect (map updates)"],
+ "instruments": [("/posture", "Posture"), ("/incidents", "Incidents"),
+ ("/queue", "Queue")],
+ },
+ {
+ "id": "cryptographer", "name": "Cryptographer", "monogram": "CR",
+ "hue": "#6d4a8f", "tint": "#f0e8f7",
+ "question": "Does the evidence really prove what it claims — no more, no less?",
+ "card": "Re-verifies receipts, replays the log offline, guards claim boundaries.",
+ "lead": ("You are at the Cryptographer station. You take "
+ "nothing on trust that you can recompute: receipts, inclusion proofs, "
+ "signed heads, the whole log. And you guard the boundary of every "
+ "claim — what is proven, and exactly where the proof stops."),
+ "mission": ("Independently re-verify any evidence artifact anyone hands you, "
+ "replay the public log offline, and keep everyone honest about what "
+ "the mathematics does and does not cover."),
+ "duties": [
+ ("Verify a receipt end to end",
+ "Use the Inspect instrument (embedded below) or the CLI with the hardened "
+ "flags — pin store, freshness policy, verified-verifier requirement:",
+ "pacta receipt-verify --attestation a.json --receipt r.json \\\n"
+ " --log-public-key log.pub --sth-store sth-store.json \\\n"
+ " --max-sth-age-seconds 604800 --require-verified-verifier"),
+ ("Fetch fresh evidence yourself",
+ "Never verify only what you were handed — fetch from the live log and "
+ "verify locally:",
+ "pacta log-fetch --url https://ltl.zkdefi.org --component dalek-ed25519-verified"),
+ ("Replay the whole log offline",
+ "In a clone of the published log repo: a fail-closed, standard-library "
+ "verifier re-checks every leaf, every signed head, every receipt — with "
+ "its adversarial self-test shipped beside it:",
+ "python3 verify.py --all\npython3 verify_selftest.py"),
+ ("Guard the claim boundary",
+ "The evidence grades (R0–R5) are exact: R4 means machine-checked proofs on "
+ "the documented boundary — SHA-512 opaque, reproducible builds out of "
+ "scope (R5). A claim stretched past its boundary is a false claim. The "
+ "grading tools are pacta claims, pacta score, "
+ "pacta report.",
+ None),
+ ],
+ "never": [
+ "Accept a green light you did not recompute — including this cockpit's.",
+ "Extend a claim beyond its stated boundary — «verified» never means more "
+ "than the certificate says.",
+ "Treat the log as a truth oracle — it is an accountability ledger: leaves "
+ "can lie, and only independent replay catches a fabricated claim.",
+ ],
+ "receives": ["evidence artifacts to audit — from anyone",
+ "fresh attestations and receipts — from the live log"],
+ "delivers": ["audit verdicts — to the Operator and the Proposer",
+ "claim-boundary corrections — to the Architect (docs and map)"],
+ "instruments": [("/inspect", "Inspect"), ("/guide", "Guide (limits)")],
+ },
+ {
+ "id": "architect", "name": "Architect", "monogram": "AR",
+ "hue": "#2b5b78", "tint": "#e8eef2",
+ "question": "Does the map still match the territory — every repo, service, loop?",
+ "card": "Keeps the estate map true; watches the loops and the public boundary.",
+ "lead": ("You are at the Architect station. The estate is many "
+ "repos, services, mirrors, and two self-referential loops — too much "
+ "for anyone's head, which is why the map exists. Your job is that the "
+ "map never lies: about what exists, what runs, and what is public."),
+ "mission": ("Keep the estate map congruent with reality after every change, "
+ "keep its two renderings from drifting, watch the loops, and gate "
+ "what may be named in public."),
+ "duties": [
+ ("Walk the map after every landed change",
+ "Open the Estate instrument and check the changed entity's card — "
+ "runtime (always-on / on-demand / not-running / static), mutability, "
+ "custody lane. The live drift tripwire is embedded below.",
+ None),
+ ("Keep the two renderings synced",
+ "The map exists twice: ESTATE.md (canonical, committed) and "
+ "the cockpit's estate view. A name-level tripwire test fails the suite if "
+ "they drift — run it after map edits:",
+ "python3 scripts/mini_pytest.py"),
+ ("Watch the loops",
+ "Loop 1: the dogfood signer is attested at leaf 8 of the very log it "
+ "signs. Loop 2: entry 13 — the log carries the kernel-checked mechanization "
+ "of its own accumulator's soundness. Both must stay tellable in one "
+ "breath; if an explanation of a loop stops being crisp, the estate has "
+ "drifted somewhere.",
+ None),
+ ("Gate the public boundary",
+ "Public documents list only entities whose existence is already public or "
+ "must be public for trust. Private infrastructure stays unnamed, "
+ "everywhere, always.",
+ None),
+ ],
+ "never": [
+ "Name private infrastructure in a public artifact — not in maps, not in "
+ "docs, not in commit messages.",
+ "Let a generated file drift from its canonical source — when you fix a "
+ "published file, find and fix its generator in the same change.",
+ "Redraw the map from memory — the map is recomputed from the repos and "
+ "services, never from recollection.",
+ ],
+ "receives": ["outage and repair notes — from the Operator",
+ "claim-boundary corrections — from the Cryptographer"],
+ "delivers": ["the updated, drift-guarded map — to everyone "
+ "(ESTATE.md + the estate view)"],
+ "instruments": [("/estate", "Estate map"), ("/guide", "Guide")],
+ },
+ {
+ "id": "newcomer", "name": "Newcomer", "monogram": "NC",
+ "hue": "#0f766e", "tint": "#e0f2f0",
+ "question": "What is all this? Where do I even start?",
+ "card": "Learns the system hands-on with the DEMO wallet; supplies fresh eyes.",
+ "lead": ("You are at the Newcomer station — everyone's first "
+ "station, including the people now sitting at the other five. Your "
+ "first hour is mapped out below, and your confusion is valuable: it "
+ "finds the gaps the veterans stopped seeing."),
+ "mission": ("Learn the system hands-on, with a wallet that cannot hurt "
+ "anything, until you can read the Bridge at a glance — then pick a "
+ "station and shadow it."),
+ "duties": [
+ ("Your first hour, step 1: run the demo",
+ "A throwaway, custody-inert wallet with fake members — every view has "
+ "content, nothing can sign anything real:",
+ "pacta wallet cockpit --demo"),
+ ("Step 2: read the Guide",
+ "All of it — ten minutes. What warden is, how to read any page, the color "
+ "code, and a glossary of every term you will meet.",
+ None),
+ ("Step 3: take the five-minute tour",
+ "Posture → Incidents → Queue → Inspect → Estate, in that order, reading "
+ "each page's verdict first.",
+ None),
+ ("Step 4: verify something real",
+ "Paste the sample evidence from examples/wallet-evidence/ "
+ "into the Inspect instrument and watch the deployed verifier accept it — "
+ "then break one character and watch it refuse.",
+ None),
+ ("Step 5: pick a station and shadow it",
+ "Read that station's mission, duties, and its «never» list — the never "
+ "list is the fastest way to understand a role.",
+ None),
+ ],
+ "never": [
+ "Pretend to understand — every other station once sat exactly here.",
+ "Assume a confusing page is your fault: if a page confuses you, that is a "
+ "bug in the page, not in you. Report it.",
+ ],
+ "receives": ["the Guide, the demo wallet, and patient answers — from every "
+ "station"],
+ "delivers": ["fresh eyes: every page that confuses you, reported — to the "
+ "Architect and the Operator"],
+ "instruments": [("/guide", "Guide"), ("/inspect", "Inspect"),
+ ("/estate", "Estate map")],
+ },
+]
+
+STATION_BY_ID = {s["id"]: s for s in STATIONS}
+
+# the andon board: event -> who acts, with what
+DISPATCH: list[tuple[str, str, str]] = [
+ ("A request was refused", "proposer",
+ "read the refusal receipt: code → missing → remediation, then retry"),
+ ("CUSTODY FROZEN, or a new incident", "operator",
+ "runbook-latch, root cause, permanent unlatch note (plus the Quorum bench if it "
+ "was a divergence)"),
+ ("A new component wants to be trusted", "quorum",
+ "evidence through the inbound boundary; unanimity admits, one dissent latches"),
+ ("Someone handed you evidence", "cryptographer",
+ "Inspect, or receipt-verify with the hardened flags — recompute, never trust"),
+ ("A service or repo looks dead", "operator",
+ "liveness board: Probe now"),
+ ("The map feels wrong", "architect",
+ "estate view + drift tripwire; fix the generator, not just the page"),
+ ("“I don’t understand any of this”", "newcomer",
+ "the Newcomer station is the entry point, not an insult"),
+]
+
+
+# ---------------------------------------------------------------------------
+# renderers (pure)
+# ---------------------------------------------------------------------------
+
+def _role_vars(s: dict[str, Any]) -> str:
+ return f"--role:{s['hue']};--roletint:{s['tint']}"
+
+
+def render_bridge(strip_html: str, live: dict[str, str]) -> str:
+ """The Level-1 overview: whole-system verdict, then the crew."""
+ cards = "".join(
+ f'Running this estate without an AI takes six roles. Each " + "station page states its mission, its duties as runnable commands, what it " + "hands to whom — and what it never does. Roles cooperate through those " + "handoffs; they do not blur into each other.
" + f"| event | station | first move |
|---|
{s["mission"]}
' + f'Instruments this station works with: {instruments}
Every duty is a runnable command or a concrete act — this is " + "the work, not advice about the work.
" + f'{duties}Separation of duties is the control that makes the team " + "trustworthy — these lines are load-bearing, not etiquette.
" + f'{esc(via)}'
+ f' — nothing on this panel is cached or asserted.{esc(f'{type(error).__name__}: {error}')}. "
+ f"A cockpit that cannot verify shows red, never a stale green. "
+ f"What to do: check that the wallet directory still exists and is "
+ f"readable, then reload. If this persists, inspect from the command line with "
+ f"pacta wallet status."
+ f"{provenance(via)}{esc(command)}'
diff --git a/src/pacta/walletui.py b/src/pacta/walletui.py
index f2654e9..5cf99f9 100644
--- a/src/pacta/walletui.py
+++ b/src/pacta/walletui.py
@@ -1,153 +1,68 @@
-"""walletui - the warden custody cockpit (local, read-only).
+"""walletui - the warden custody cockpit (local, read-only): a bridge
+with six role stations over shared evidence instruments.
-A localhost web surface over an existing wallet directory, for the human
-operator who ultimately answers for the money. Six views: posture, the
-pending-signature queue (airgap outbox), the incident & refusal browser,
-a receipt inspector, the estate map, and a plain-language guide.
+Information architecture (control-room style, four levels):
+ 1. BRIDGE (/) - whole-system verdict + the crew of six roles
+ 2. STATIONS (/station/*) - one console per role: mission, duties as
+ runnable commands, live instruments, the
+ never-list, explicit handoffs
+ 3. INSTRUMENTS - shared evidence views: posture, queue,
+ incidents, inspect, estate, guide
+ 4. RAW - the wallet files and the CLI themselves
-Design law: THE COCKPIT RENDERS EVIDENCE, IT NEVER ASSERTS IT. Every
+Three laws, each enforced by tests:
+
+DESIGN LAW - THE COCKPIT RENDERS EVIDENCE, IT NEVER ASSERTS IT. Every
panel is recomputed from wallet state or submitted artifacts at request
time by the same functions the wallet itself uses, and every panel names
the function and timestamp that produced it. Anything that cannot be
-recomputed renders as a loud FAILED-TO-VERIFY panel - there is no cached
-green and no neutral gray.
+recomputed renders as a loud FAILED-TO-VERIFY panel - no cached green,
+no neutral gray.
-UX law (the design law's twin): THE COCKPIT NEVER LEAVES A HUMAN IN THE
-DARK. Every page opens with a plain-language statement of what it shows;
-every verdict is stated in words, not just color; every panel carries a
-"how to read this" explainer; every piece of jargon links to the /guide
-glossary. A person who has never heard of warden must be able to read
-every screen.
+UX LAW - THE COCKPIT NEVER LEAVES A HUMAN IN THE DARK. Every page opens
+with a plain-language lead; every verdict is stated in words; every
+panel carries a "how to read this" explainer; every jargon term links to
+the /guide glossary.
+
+CREW LAW - ROLES ARE DISTINCT AND COOPERATE THROUGH HANDOFFS. The bridge
+presents everything a human crew would need if no AI were around: six
+stations (proposer, quorum bench, operator, cryptographer, architect,
+newcomer), each with runnable duties and a "this station never" list.
+Separation of duties is a custody control; the stations do not melt into
+each other. (Role content lives in stations.py; liveness probes in
+liveness.py; shared primitives in uikit.py.)
Read-only guarantee: this module calls only read paths (``Wallet.posture``,
-``verify_ledger``, directory listings) and ``transparency.verify_receipt``
-on submitted artifacts (parsed in memory / temp files outside the wallet).
-It cannot approve, sign, unlatch, or modify custody state; the HTTP surface
-exposes no mutating route. Human approve/deny is deliberately NOT here -
-that would be a custody-semantics change, which belongs to a separate,
-explicitly reviewed milestone.
+``verify_ledger``, directory listings), ``transparency.verify_receipt`` on
+submitted artifacts (parsed in memory / temp files outside the wallet),
+and - only when the operator explicitly presses "Probe now" - outbound
+liveness observations (HTTP GET, git queries). It cannot approve, sign,
+unlatch, or modify custody state; the HTTP surface exposes no mutating
+route. Human approve/deny is deliberately NOT here - that would be a
+custody-semantics change, which belongs to a separate, explicitly
+reviewed milestone. The mutating acts a human crew needs are provided as
+exact CLI commands on the stations instead.
The server binds 127.0.0.1 by default and is not meant to be exposed.
"""
from __future__ import annotations
-import html
import json
import tempfile
import urllib.parse
-from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any, Callable
from .attestation import load_attestation
+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
+from .uikit import (STYLE, esc as _esc, explain as _explain,
+ failed_panel as _failed_panel, help_link as _help,
+ now_utc as _now, provenance as _provenance)
from .wallet import Wallet
-_STYLE = """
- :root{--ink:#1c2430;--ink2:#5a6675;--line:#dde2e9;--ok:#1e7f4f;--okbg:#e2f2e9;
- --bad:#a3242c;--badbg:#fbe4e6;--warn:#a86a10;--warnbg:#fdf0da;
- --accent:#3b4d8f;--accentbg:#eef0f7;--bg:#f8f9fa}
- *{box-sizing:border-box}
- body{font-family:system-ui,sans-serif;max-width:62rem;margin:0 auto;
- padding:1.4rem 1.2rem 4rem;color:var(--ink);line-height:1.55;background:var(--bg)}
- h1{font-size:1.35rem;margin:.2rem 0 0}
- h2{font-size:1.05rem;margin:1.6rem 0 .5rem}
- code{font-family:ui-monospace,Menlo,Consolas,monospace;background:#eef0f3;
- border-radius:4px;padding:.08rem .3rem;font-size:.88em}
- .sub{color:var(--ink2);font-size:.85rem;margin:.3rem 0 .6rem}
- nav{margin:.7rem 0 1rem;display:flex;gap:.5rem;flex-wrap:wrap}
- nav a{color:var(--accent);text-decoration:none;border:1px solid var(--line);
- background:#fff;border-radius:6px;padding:.3rem .7rem;font-size:.85rem;
- display:flex;flex-direction:column;line-height:1.25;min-width:7.5rem}
- nav a.here{border-color:var(--accent);font-weight:600;background:var(--accentbg)}
- nav a .navsub{font-size:.67rem;color:var(--ink2);font-weight:400}
- .banner{background:var(--warnbg);border:1px solid var(--warn);color:var(--warn);
- border-radius:6px;padding:.45rem .8rem;font-size:.82rem;font-weight:600}
- .banner a{color:var(--warn)}
- .lead{font-size:.92rem;margin:.9rem 0 .2rem}
- .verdict{border-radius:8px;padding:.8rem 1.1rem;margin:.8rem 0;border:1px solid}
- .verdict strong{font-size:1.05rem;letter-spacing:.02em}
- .verdict p{margin:.3rem 0 0;font-size:.88rem;font-weight:400}
- .verdict.ok{background:var(--okbg);border-color:var(--ok);color:var(--ok)}
- .verdict.warn{background:var(--warnbg);border-color:var(--warn);color:var(--warn)}
- .verdict.bad{background:var(--badbg);border-color:var(--bad);color:var(--bad)}
- .panel{background:#fff;border:1px solid var(--line);border-radius:8px;
- padding:.9rem 1.1rem;margin:.7rem 0}
- .panel.bad{border-color:var(--bad);background:var(--badbg)}
- .prov{color:var(--ink2);font-size:.72rem;margin-top:.6rem;border-top:1px dashed var(--line);
- padding-top:.35rem}
- .pill{display:inline-block;border-radius:9px;padding:.06rem .55rem;font-size:.76rem;
- font-weight:700}
- .pill.ok{background:var(--okbg);color:var(--ok)}
- .pill.bad{background:var(--badbg);color:var(--bad)}
- .pill.warn{background:var(--warnbg);color:var(--warn)}
- a.help{display:inline-block;width:1.05rem;height:1.05rem;line-height:1.05rem;text-align:center;
- border-radius:50%;background:var(--accentbg);color:var(--accent);font-size:.72rem;
- font-weight:700;text-decoration:none;vertical-align:.15em}
- details.explain{margin-top:.55rem;font-size:.82rem}
- details.explain summary{cursor:pointer;color:var(--accent);font-weight:600;font-size:.78rem}
- details.explain .expl{color:var(--ink2);margin:.4rem 0 0;padding:.5rem .7rem;
- background:var(--accentbg);border-radius:6px}
- details.explain .expl ul{margin:.3rem 0;padding-left:1.1rem}
- details.explain .expl li{margin:.15rem 0}
- .plain{font-size:.88rem;margin:.3rem 0 .6rem}
- .empty{color:var(--ink2);font-size:.88rem;background:var(--accentbg);border-radius:6px;
- padding:.5rem .8rem}
- pre{overflow-x:auto}
- .tablewrap{overflow-x:auto}
- table{border-collapse:collapse;width:100%;font-size:.88rem;background:#fff}
- td,th{border:1px solid var(--line);padding:.4rem .6rem;text-align:left;vertical-align:top}
- th{background:var(--accentbg)}
- ul.diag{margin:.4rem 0 0;padding-left:1.2rem}
- ul.diag li{font-size:.85rem;margin:.2rem 0}
- dl.gloss dt{font-weight:700;margin-top:.8rem}
- dl.gloss dd{margin:.15rem 0 0 0;font-size:.88rem;color:var(--ink)}
- textarea{width:100%;min-height:7.5rem;font-family:ui-monospace,monospace;font-size:.8rem;
- border:1px solid var(--line);border-radius:6px;padding:.5rem}
- button{background:var(--accent);color:#fff;border:0;border-radius:6px;
- padding:.5rem 1.1rem;font-size:.9rem;cursor:pointer}
- .muted{color:var(--ink2);font-size:.85rem}
- .mono{font-family:ui-monospace,monospace}
-"""
-
-
-def _now() -> str:
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
-
-
-def _esc(value: Any) -> str:
- return html.escape(str(value))
-
-
-def _help(anchor: str) -> str:
- """A small ? that jumps to the glossary entry for a term."""
- return (f'?')
-
-
-def _explain(body: str) -> str:
- """The per-panel interpretation aid: always present, opt-in detail."""
- return (f'{_esc(via)} — nothing on this panel is cached or asserted.{_esc(f'{type(error).__name__}: {error}')}. "
- f"A cockpit that cannot verify shows red, never a stale green. "
- f"What to do: check that the wallet directory still exists and is "
- f"readable, then reload. If this persists, inspect from the command line with "
- f"pacta wallet posture."
- f"{_provenance(via)}Watching wallet {_esc(wallet_dir)} — everything below is "
"recomputed live from that directory each time a page loads; nothing is cached, "
"nothing is taken on trust.