Runtime Dashboard Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a read-only localhost dashboard for runtime memory/debugging with scoped status, events, and graph-inspection API endpoints.

Architecture: Add src/zaxy/dashboard.py with a dependency-light HTTP server built on Python's standard library. The first slice serves bundled HTML/CSS/JS plus read-only JSON endpoints backed by existing Eventloom memory status/log helpers and bounded graph adapter methods. Add zaxy dashboard CLI wiring without coupling the dashboard to MCP.

Tech Stack: Python 3.11 standard library HTTP server, Typer CLI, Eventloom helpers, Neo4j graph adapter interfaces, plain HTML/CSS/JS, Cytoscape.js loaded by the browser UI.

---

File Structure

Task 1: Dashboard Scope And API Models

Files:

Add tests that prove dashboard scope defaults to the active workspace and does not discover unrelated projects:

from pathlib import Path

from zaxy.dashboard import DashboardConfig, resolve_dashboard_scope


def test_dashboard_scope_defaults_to_current_workspace(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    workspace.mkdir()

    scope = resolve_dashboard_scope(DashboardConfig(workspace=workspace))

    assert scope.workspace == workspace.resolve()
    assert scope.eventloom_path == workspace.resolve() / ".eventloom"
    assert scope.host == "127.0.0.1"
    assert scope.port == 8765
    assert scope.read_only is True


def test_dashboard_scope_accepts_explicit_eventloom_and_session(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    custom_eventloom = tmp_path / "memory"
    workspace.mkdir()

    scope = resolve_dashboard_scope(
        DashboardConfig(
            workspace=workspace,
            eventloom_path=custom_eventloom,
            session_id="agent-1",
            domain="demo",
            host="localhost",
            port=9000,
        )
    )

    assert scope.workspace == workspace.resolve()
    assert scope.eventloom_path == custom_eventloom.resolve()
    assert scope.session_id == "agent-1"
    assert scope.domain == "demo"
    assert scope.host == "localhost"
    assert scope.port == 9000

Run: pytest tests/test_dashboard.py::test_dashboard_scope_defaults_to_current_workspace tests/test_dashboard.py::test_dashboard_scope_accepts_explicit_eventloom_and_session -q

Expected: import failure for zaxy.dashboard.

Create DashboardConfig, DashboardScope, and resolve_dashboard_scope(). The implementation resolves paths, defaults to .eventloom, defaults host to 127.0.0.1, port to 8765, and marks the scope read-only.

Run: pytest tests/test_dashboard.py::test_dashboard_scope_defaults_to_current_workspace tests/test_dashboard.py::test_dashboard_scope_accepts_explicit_eventloom_and_session -q

Expected: both tests pass.

Task 2: Read-Only Eventloom API

Files:

Add tests for DashboardApp.handle_api():

from zaxy.dashboard import DashboardApp, DashboardConfig, resolve_dashboard_scope
from zaxy.event import EventLog


def test_dashboard_status_and_events_use_resolved_eventloom(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    workspace.mkdir()
    eventloom = workspace / ".eventloom"
    log = EventLog(eventloom / "default.jsonl")
    event = log.append("decision.recorded", actor="tester", payload={"decision": "Build dashboard."})
    app = DashboardApp(resolve_dashboard_scope(DashboardConfig(workspace=workspace)))

    status_code, headers, body = app.handle_api("GET", "/api/status", "")
    assert status_code == 200
    assert headers["content-type"] == "application/json; charset=utf-8"
    assert body["scope"]["workspace"] == str(workspace.resolve())
    assert body["memory"]["total_events"] == 1
    assert body["memory"]["sessions"][0]["latest_hash"] == event.hash

    status_code, _headers, body = app.handle_api("GET", "/api/events", "session_id=default&limit=5")
    assert status_code == 200
    assert body["events"][0]["type"] == "decision.recorded"
    assert body["events"][0]["summary"] == "Build dashboard."


def test_dashboard_rejects_non_get_api_methods(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    workspace.mkdir()
    app = DashboardApp(resolve_dashboard_scope(DashboardConfig(workspace=workspace)))

    status_code, _headers, body = app.handle_api("POST", "/api/events", "")

    assert status_code == 405
    assert body["error"] == "read_only"

Run: pytest tests/test_dashboard.py::test_dashboard_status_and_events_use_resolved_eventloom tests/test_dashboard.py::test_dashboard_rejects_non_get_api_methods -q

Expected: DashboardApp or handle_api missing.

Add DashboardApp.handle_api() with:

Run: pytest tests/test_dashboard.py::test_dashboard_status_and_events_use_resolved_eventloom tests/test_dashboard.py::test_dashboard_rejects_non_get_api_methods -q

Expected: tests pass.

Task 3: Graph API Contract

Files:

Add a fake read-only graph provider and tests for scoped, bounded graph endpoints:

class FakeGraphProvider:
    def __init__(self) -> None:
        self.calls: list[tuple[str, dict[str, object]]] = []

    def summary(self, *, session_id: str | None) -> dict[str, object]:
        self.calls.append(("summary", {"session_id": session_id}))
        return {"nodes": 2, "edges": 1}

    def neighborhood(
        self,
        *,
        session_id: str | None,
        node_id: str,
        view: str,
        hops: int,
        limit: int,
    ) -> dict[str, object]:
        self.calls.append(
            (
                "neighborhood",
                {
                    "session_id": session_id,
                    "node_id": node_id,
                    "view": view,
                    "hops": hops,
                    "limit": limit,
                },
            )
        )
        return {"nodes": [{"id": node_id}], "edges": [], "omitted_nodes": 0, "omitted_edges": 0}


def test_dashboard_graph_summary_uses_session_scope(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    workspace.mkdir()
    graph = FakeGraphProvider()
    app = DashboardApp(
        resolve_dashboard_scope(DashboardConfig(workspace=workspace, session_id="agent-1")),
        graph_provider=graph,
    )

    status_code, _headers, body = app.handle_api("GET", "/api/graph/summary", "")

    assert status_code == 200
    assert body["graph"] == {"nodes": 2, "edges": 1}
    assert graph.calls == [("summary", {"session_id": "agent-1"})]


def test_dashboard_graph_neighborhood_enforces_bounds(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    workspace.mkdir()
    graph = FakeGraphProvider()
    app = DashboardApp(resolve_dashboard_scope(DashboardConfig(workspace=workspace)), graph_provider=graph)

    status_code, _headers, body = app.handle_api(
        "GET",
        "/api/graph/neighborhood",
        "node_id=n1&view=temporal&hops=99&limit=5000",
    )

    assert status_code == 200
    assert body["graph"]["nodes"] == [{"id": "n1"}]
    assert graph.calls[0][1]["hops"] == 2
    assert graph.calls[0][1]["limit"] == 250

Run: pytest tests/test_dashboard.py::test_dashboard_graph_summary_uses_session_scope tests/test_dashboard.py::test_dashboard_graph_neighborhood_enforces_bounds -q

Expected: graph endpoints return 404.

Add:

Run: pytest tests/test_dashboard.py::test_dashboard_graph_summary_uses_session_scope tests/test_dashboard.py::test_dashboard_graph_neighborhood_enforces_bounds -q

Expected: tests pass.

Task 4: Static Dashboard UI

Files:

Add tests for the HTML shell:

def test_dashboard_index_html_references_core_tabs_and_api() -> None:
    html = render_dashboard_html()

    assert "Runtime Dashboard" in html
    assert "Overview" in html
    assert "Sessions" in html
    assert "Graph" in html
    assert "Checkout" in html
    assert "Events" in html
    assert "/api/status" in html
    assert "/api/graph/summary" in html
    assert "cytoscape" in html.lower()

Run: pytest tests/test_dashboard.py::test_dashboard_index_html_references_core_tabs_and_api -q

Expected: render_dashboard_html missing.

Add render_dashboard_html() returning a self-contained operational UI shell. Include a graph canvas placeholder, read-only badge, scope header, tab buttons, and JavaScript that fetches /api/status, /api/events, and /api/graph/summary.

Run: pytest tests/test_dashboard.py::test_dashboard_index_html_references_core_tabs_and_api -q

Expected: test passes.

Task 5: HTTP Server And CLI Command

Files:

Add tests that create_dashboard_handler() serves HTML and JSON without starting a real long-running server, and that the CLI exposes the command:

from typer.testing import CliRunner

from zaxy.__main__ import app as cli_app
from zaxy.dashboard import create_dashboard_handler


def test_dashboard_handler_serves_index_and_api(tmp_path: Path) -> None:
    workspace = tmp_path / "project"
    workspace.mkdir()
    handler_cls = create_dashboard_handler(
        DashboardApp(resolve_dashboard_scope(DashboardConfig(workspace=workspace)))
    )

    assert handler_cls.dashboard_app.scope.workspace == workspace.resolve()


def test_dashboard_cli_help_exposes_localhost_default() -> None:
    result = CliRunner().invoke(cli_app, ["dashboard", "--help"])

    assert result.exit_code == 0
    assert "127.0.0.1" in result.output
    assert "8765" in result.output

Run: pytest tests/test_dashboard.py::test_dashboard_handler_serves_index_and_api tests/test_cli.py::test_dashboard_cli_help_exposes_localhost_default -q

Expected: handler or CLI command missing.

Add:

The command accepts --workspace, --eventloom-path, --session-id, --domain, --host, and --port, then prints Zaxy dashboard listening on http://host:port before serving.

Run: pytest tests/test_dashboard.py tests/test_cli.py::test_dashboard_cli_help_exposes_localhost_default -q

Expected: tests pass.

Task 6: Quality Gate

Files:

Run: pytest tests/test_dashboard.py tests/test_cli.py::test_dashboard_cli_help_exposes_localhost_default -q

Expected: all selected tests pass.

Run: ruff check src/zaxy/dashboard.py tests/test_dashboard.py src/zaxy/__main__.py tests/test_cli.py

Expected: no lint failures.

Run: ruff format --check src/zaxy/dashboard.py tests/test_dashboard.py src/zaxy/__main__.py tests/test_cli.py

Expected: no formatting changes needed.

Run: pytest tests/test_viewer.py tests/test_memory_status.py tests/test_cli.py::test_viewer_command_writes_static_html -q

Expected: tests pass.

git add src/zaxy/dashboard.py src/zaxy/__main__.py tests/test_dashboard.py tests/test_cli.py docs/superpowers/plans/2026-05-16-runtime-dashboard.md
git commit -m "feat: add read-only runtime dashboard"