#!/usr/bin/env python3 """ HTTP admin debug dashboard for ``zkac-node serve``. Runs the ZKAC TCP node in a background thread and serves a read-only web UI on another port. Intended for loopback + I2P server-tunnel forwarding. **Security:** This page is fully transparent (registry metadata, live sessions, public keys). Do not expose it to untrusted networks without tunnel ACLs. Usage:: uv sync --extra demo uv run python demo/zkac_admin_serve.py alice \\ --node-host 127.0.0.1 --node-port 9800 \\ --web-host 127.0.0.1 --web-port 8766 """ from __future__ import annotations import argparse import json import os import sys import threading import time from pathlib import Path from flask import Flask, Response, jsonify, render_template_string, request from zkac_cli.paths import user_dir from zkac_cli.server import serve from zkac_cli.server_debug import ( ServerDebugState, collect_registry_debug, data_dir_tree, server_key_meta, ) _PAGE = """
{% if refresh_s %} {% endif %}User {{ snap.userid }} · TCP {{ snap.listen.host }}:{{ snap.listen.port }} · uptime {{ snap.uptime_s }}s {% if refresh_s %} · auto-refresh {{ refresh_s }}s {% endif %}
| Data directory | {{ snap.data_dir }} |
|---|---|
| Started (wall) | {{ snap.started_wall }} |
| Server public key | {{ snap.server_public_key_hex or "—" }} |
| Registries (boot) | {{ snap.registries_loaded_boot }} |
| Active TCP sessions | {{ snap.active_connection_count }} live |
| Process | pid={{ proc.pid }} threads={{ proc.threads }} |
{{ sk_meta | tojson(indent=2) }}
| ID (file) | version | state hash | bytes (state / cert) | parse |
|---|---|---|---|---|
| {{ r.file_registry_id_hex[:16] }}… | {{ r.get("version", "—") }} | {% if r.get("state_hash_hex") %}{{ r.state_hash_hex[:24] }}…{% else %}—{% endif %} | {{ r.state_bytes }} / {{ r.cert_bytes }} | {% if r.parsed_ok %}ok{% else %}fail{% endif %} |
No registry state files yet.
{% endif %}| id | peer | phase | op | transcript | auth registry | role | mgmt # | echo bytes |
|---|---|---|---|---|---|---|---|---|
| {{ c.id }} | {{ c.peer }} | {{ c.phase }} | {{ c.hello_op or "—" }} | {% if c.transcript_hash_hex %}{{ c.transcript_hash_hex[:20] }}…{% else %}—{% endif %} | {% if c.auth_registry_hex %}{{ c.auth_registry_hex[:16] }}…{% else %}—{% endif %} | {% if c.auth_role_hex %}{{ c.auth_role_hex[:16] }}…{% else %}—{% endif %} | {{ c.mgmt_commands or 0 }} | {{ c.bytes_echoed or 0 }} |
No active connections.
{% endif %}| id | peer | phase | mgmt # | echo bytes | error |
|---|---|---|---|---|---|
| {{ c.id }} | {{ c.peer }} | {{ c.phase }} | {{ c.mgmt_commands or 0 }} | {{ c.bytes_echoed or 0 }} | {{ c.error or "—" }} |
No recent disconnects recorded.
{% endif %}{{ files | tojson(indent=2) }}
Machine-readable snapshot (same as /api/debug.json).
{{ full_json }}
Auto-refresh 3s · 5s · static · debug.json
""" def _full_payload(debug: ServerDebugState, data_dir: Path) -> dict: snap = debug.snapshot() reg = collect_registry_debug(data_dir) sk = server_key_meta(data_dir) files = data_dir_tree(data_dir) return { "snapshot": snap, "server_key_file": sk, "registries": reg, "data_dir_files": files, "process": {"pid": os.getpid(), "threads": threading.active_count()}, "generated_wall": time.time(), } def create_app(debug: ServerDebugState, data_dir: Path) -> Flask: app = Flask(__name__) @app.get("/") def index(): refresh = request.args.get("refresh", "").strip() refresh_s = int(refresh) if refresh.isdigit() and 1 <= int(refresh) <= 60 else None data = _full_payload(debug, data_dir) snap = data["snapshot"] proc = data["process"] full_json = json.dumps(data, indent=2, sort_keys=True) return render_template_string( _PAGE, snap=snap, reg=data["registries"], sk_meta=data["server_key_file"], files=data["data_dir_files"], full_json=full_json, proc=proc, refresh_s=refresh_s, ) @app.get("/api/debug.json") def api_debug(): return jsonify(_full_payload(debug, data_dir)) @app.get("/healthz") def healthz(): return Response("ok", mimetype="text/plain") return app def main() -> None: p = argparse.ArgumentParser(description="ZKAC node TCP + HTTP admin debug dashboard") p.add_argument("userid", help="user whose ~/.zkac/