edge.monitor
edge.monitor
The edge monitor (Zone B) — a small dashboard + chat surface over Tailscale.
A SEPARATE process palace supervises. Network-facing, so by Invariant 2 it cannot share the
sealed core process: it renders a core-emitted status snapshot (a read-only file) and relays chat
over the existing filesystem handoff to the core inbox. It NEVER imports core and never reads a
store. See server.py.
MonitorApp
dataclass
Pure request routing over two injected seams: read_status (the snapshot file) and chat
(the handoff round-trip). No sockets here — testable directly.
read_status
instance-attribute
chat
instance-attribute
get(path)
post_chat(payload)
make_handler(app)
render_dashboard(status)
Render the snapshot as a self-contained HTML page (inline CSS/JS, no external deps). Shows only metadata the snapshot carries — never corpus content.
serve(host, port, *, status_path, handoff, request_timeout_s=30.0)
Wire the file reader + handoff chat closure and run the server. Imports the handoff channel lazily so the routing/render units stay import-light for tests.
page
The dashboard's static HTML/CSS/JS template (an embedded asset, not logic).
Kept separate from server.py so the server's Python stays fully lint-covered while these long
asset lines are exempted (E501) in one asset-only place (see pyproject per-file-ignores).
render_dashboard fills {{BODY}} and {{GEN}}.
CHAT = '\n<div class="panel">\n <h2>Talk to it</h2>\n <div id="log" class="log"></div>\n <form id="chat" onsubmit="return send(event)">\n <input id="msg" autocomplete="off" placeholder="ask, recall, or capture a thought…" />\n <button type="submit">Send</button>\n </form>\n</div>\n<script>\nasync function send(e){\n e.preventDefault();\n const i=document.getElementById(\'msg\'), log=document.getElementById(\'log\');\n const text=i.value.trim(); if(!text) return false;\n add(log,\'you\',text); i.value=\'\'; i.disabled=true;\n try{\n const r=await fetch(\'/chat\',{method:\'POST\',headers:{\'Content-Type\':\'application/json\'},\n body:JSON.stringify({text})});\n const j=await r.json(); add(log,\'palace\', j.reply || j.error || \'(no reply)\');\n }catch(err){ add(log,\'palace\',\'(could not reach the monitor)\'); }\n i.disabled=false; i.focus(); return false;\n}\nfunction add(log,who,text){\n const d=document.createElement(\'div\'); d.className=\'turn \'+who;\n d.innerHTML=\'<b>\'+who+\':</b> \'+text.replace(/&/g,\'&\').replace(/</g,\'<\');\n log.appendChild(d); log.scrollTop=log.scrollHeight;\n}\nasync function refresh(){\n try{ const r=await fetch(\'/status.json\'); const s=await r.json();\n if(s && s.generated_at){ document.getElementById(\'gen\').textContent=s.generated_at; } }catch(e){}\n}\nsetInterval(refresh, 5000);\n</script>\n'
module-attribute
PAGE = '<!doctype html><html><head><meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>mind-palace</title>\n<style>\n:root{color-scheme:dark}\nbody{font:15px/1.5 -apple-system,system-ui,sans-serif;margin:0;background:#0e1116;color:#d7dce3}\nheader{padding:18px 22px;border-bottom:1px solid #232934;display:flex;\n justify-content:space-between;align-items:baseline}\nheader h1{font-size:18px;margin:0;letter-spacing:.3px}\n.muted{color:#7a8595}.sub{color:#7a8595;font-size:12px}\nmain{padding:22px;max-width:920px;margin:0 auto}\n.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}\n.card{background:#161b22;border:1px solid #232934;border-radius:10px;padding:14px}\n.card .label{color:#7a8595;font-size:12px;text-transform:uppercase;letter-spacing:.5px}\n.card .value{font-size:20px;margin-top:6px}\n.pill{padding:3px 10px;border-radius:999px;font-size:14px}\n.pill.ok{background:#0f3d2e;color:#5fe3a1}.pill.warn{background:#3d340f;color:#e3c95f}\n.pill.bad{background:#3d1414;color:#ff8a8a}\n.panel{background:#161b22;border:1px solid #232934;border-radius:10px;padding:16px;margin-top:16px}\n.panel h2{font-size:13px;text-transform:uppercase;letter-spacing:.5px;color:#7a8595;margin:0 0 10px}\n.warnbox{border-color:#5a4a16}\nul{margin:0;padding:0;list-style:none}\n.activity li{padding:5px 0;border-bottom:1px solid #1c222b}\n.role{color:#8ab4ff}\n.log{max-height:300px;overflow:auto;margin-bottom:10px}\n.turn{padding:6px 0}.turn.you b{color:#8ab4ff}.turn.palace b{color:#5fe3a1}\nform{display:flex;gap:8px}\ninput{flex:1;background:#0e1116;border:1px solid #232934;border-radius:8px;padding:10px;\n color:#d7dce3}\nbutton{background:#1f6feb;border:0;border-radius:8px;color:#fff;padding:10px 16px;cursor:pointer}\n</style></head><body>\n<header><h1>🧠 mind-palace</h1>\n<span class="sub">snapshot <span id="gen">{{GEN}}</span></span></header>\n<main>{{BODY}}</main></body></html>'
module-attribute
server
The edge monitor HTTP server (Zone B, Invariant 2).
Two surfaces, both behind the tailnet:
* dashboard — GET / renders the core-emitted status snapshot (health, activity shape, queue
depth, memory, dream counts). GET /status.json is the raw snapshot for the
page's auto-refresh. Read-only — it opens a file, never a store.
* chat — POST /chat {text, conversation} writes to the interface handoff and awaits the
core's reply (the Ambassador answers inside the walls). The handoff is the ONLY
coupling to the core; no import crosses the boundary.
Routing is split from the socket (MonitorApp returns (code, content_type, body) tuples) so it
is unit-testable without binding a port. serve() wires the file reader + the handoff chat closure
and runs a stdlib HTTPServer. The process is deliberately NOT sealed — it is Zone B and must bind
a (Tailscale) socket; the sealing guard protects the core, not this one.
Response = tuple[int, str, bytes]
module-attribute
MonitorApp
dataclass
Pure request routing over two injected seams: read_status (the snapshot file) and chat
(the handoff round-trip). No sockets here — testable directly.
read_status
instance-attribute
chat
instance-attribute
get(path)
post_chat(payload)
make_handler(app)
serve(host, port, *, status_path, handoff, request_timeout_s=30.0)
Wire the file reader + handoff chat closure and run the server. Imports the handoff channel lazily so the routing/render units stay import-light for tests.
render_dashboard(status)
Render the snapshot as a self-contained HTML page (inline CSS/JS, no external deps). Shows only metadata the snapshot carries — never corpus content.