Skip to content

Define the dashboard project: repo, layout, stack and delivery slices #228

Closed

Description

@lbacik

Question

Given the contract, the agentctl telemetry decisions, discovery and Docker access, and the UI prototype: define the separate dashboard project.

  • Repo name and layout, FastAPI + HTMX/SSE structure (SSE between dashboard and browser only).
  • Collection: polling via docker exec per container, interval, concurrency and timeouts, in-memory buffering.
  • Access to the dashboard itself in v1: it shows issues, token usage and log events even read-only. Bind to localhost, reverse proxy with auth, or other?
  • Deployment: compose service, how it gets Docker access (per the discovery decision).
  • Testing strategy against a fake agentctl / fake Docker exec layer returning recorded snapshots, including version skew (unknown fields ignored, missing = unavailable).
  • First delivery slices, and whether the data layer stays swappable for a later Next.js front end.

Part of map #222.

Activity

  1. lbacik commented on Oct 14, 2026

    @lbacik
    OwnerAuthor

    Requirements from the resolved ticket "Decide the agentctl telemetry command" (#225):

    • Poll telemetry only (control state is embedded); default interval 5 s, configurable, floor 2 s; at most one exec in flight per instance.
    • Classify each exec result:
      • exit 0 with a JSON snapshot → good snapshot;
      • stdout is {"ok": false, "error": {"code", "message"}} → use the code:
        • agent_unreachable → unreachable;
        • unsupported_operation → unsupported;
        • snapshot_failed → agent up, snapshot failed;
      • stdout is not a JSON object (argparse rejects the subcommand on an older image, exit 2) → "telemetry not supported by this agent version";
      • dashboard-side timeout → unreachable (timeout).
    • Keep the last good snapshot visible with its age on every failure. A busy recovery retry makes polls time out for its duration; this is expected.
    • control_observed_at (additive) tells when the control section was built; show the control part as stale when it is older than one poll interval while the rest of the snapshot is fresh.
    • Test fixtures should cover each of these outcomes (fake exec layer returning recorded stdout/exit pairs).
  2. self-assigned this
    on Oct 14, 2026
  3. lbacik commented on Oct 14, 2026

    @lbacik
    OwnerAuthor

    Resolution

    Decided in a grilling session; the operator accepted the recommended answer on every question. Facts checked on the target host rpi5 (aarch64, Debian 13, Python 3.13, systemd --user available, operator in the docker group, no uv/pipx installed) and in the new repository lbacik/agent-dashboard (public, empty before this ticket).

    1. Repository and layout

    • Repo lbacik/agent-dashboard, MIT license. Tooling mirrors the agent: uv + hatchling, pytest, ruff, GitHub Actions CI on Python 3.11 and 3.13, requires-python >=3.11. Entry point agent-dashboard.
    • src/agent_dashboard/, one deep module per concern:
      • gateway.py: the only code that builds Docker argv; exposes exactly list_instances() and telemetry(container_id) (from Decide instance discovery and the dashboard's Docker access).
      • collector.py: the poll cycle and classification of each exec into a poll outcome.
      • fleet.py: in-memory fleet state.
      • snapshot.py: tolerant reader of schema v1 into framework-free view models (version-skew rules from Define the telemetry snapshot contract).
      • presentation.py: status headline, attention rank, formatting.
      • web/: FastAPI app, Jinja2 templates, SSE stream, vendored static assets.

    2. Collection

    • An asyncio task started in the FastAPI lifespan; Docker calls via asyncio.create_subprocess_exec (argv, no shell).
    • Each cycle: docker ps -a --filter label=io.github.lbacik.simple-coding-agent.role=agent --format '{{json .}}' (5 s timeout), drop com.docker.compose.oneoff=True, then telemetry for each running container.
    • Interval 5 s (configurable, floor 2 s); exec timeout 5 s (configurable); at most one exec in flight per instance; global cap of 4 concurrent execs (configurable) to spare the Raspberry Pi.
    • Cycles never overlap: the next starts one interval after the previous start, or when the previous finishes, whichever is later.
    • On timeout the docker client process is killed (agentctl has its own 3 s limit inside the container).
    • Exec results are classified exactly as listed in the requirements comment from Decide the agentctl telemetry command above.
    • The app requires a single uvicorn worker (state is in memory).

    3. In-memory state

    • Per managed instance (keyed by container name): the last good snapshot with its receive time, the last poll outcome (kind, reason, time), and container id and state.
    • Fleet-wide: the last discovery error (e.g. docker CLI missing, permission denied), shown as a banner.
    • No history in v1. A removed container disappears on the next cycle. A dashboard restart starts empty.

    4. Access to the dashboard

    • Binds to 127.0.0.1 only and refuses to start on a non-loopback address in v1. Remote viewing goes through ssh -L 8080:127.0.0.1:8080 rpi5.
    • Validates the Host header against an allow-list (localhost, 127.0.0.1) to defeat DNS rebinding; no CORS.
    • LAN binding with auth or an authenticating reverse proxy is deferred to v2, where control commands need authentication and CSRF/Origin protection anyway.

    5. Installation and running

    6. Browser updates

    • One SSE stream GET /stream. After every cycle the server sends a fleet event carrying the whole re-rendered table body. The whole body is sent because attention ordering reorders rows and ages change every cycle.
    • The instance drawer is hx-get=/instances/{name} with hx-trigger="sse:fleet", so it refreshes with the table. The open drawer is kept in the URL (?instance=<name>) so it survives reloads.
    • htmx and its SSE extension are vendored into static files (no CDN). CSS is carried over from the prototype in Prototype the dashboard UI.

    7. Swappable data layer

    • The seam is the view-model layer (dataclasses), independent of FastAPI and Jinja; templates consume only view models.
    • No public JSON API in v1. A later front end (e.g. Next.js) would add /api/... over the same fleet state.

    8. Testing

    • Fake gateway at the gateway seam, returning recorded stdout/exit pairs for every poll outcome in Decide the agentctl telemetry command: socket operation, snapshot publication, failure isolation #225's list.
    • Argv-pinning test for every argv the gateway can produce.
    • Collector tests call one cycle directly, without the timer.
    • Web tests with FastAPI TestClient, asserting HTML fragments. No browser end-to-end tests.
    • Integration test against real Docker in CI: a minimal image carrying the agent label, whose /usr/local/bin/agentctl is a script printing a fixture.
    • Snapshot fixtures come from the agent repo: the agent repo owns synthetic example snapshots for schema v1 (docs/telemetry/examples/*.json) and tests that its builder produces that shape. The dashboard copies them into tests/fixtures/contract/v1/, recording the source commit, and derives version-skew fixtures from them: unknown fields, missing fields, schema_version: 2, unknown basis, null sections.
    • The repo is public: fixtures are synthetic only, never raw captures from live instances.

    9. Glossary

    • agent-dashboard has its own CONTEXT.md with Poll outcome, Reach, Status headline and Attention rank. It links to the agent's glossary for the contract terms rather than copying them. The agent-side terms are in docs: fleet dashboard glossary and ADR 0006.
    • Initial commit 1cfca31 in agent-dashboard: README (pointers to this map and ticket as the v1 specification), CONTEXT.md, ADR 0001, LICENSE. No code: building is out of this map's scope.

    10. Dashboard delivery slices

    1. S1 – Skeleton and discovery.
      • Repo scaffold and CI; gateway (list_instances, telemetry); collector cycle with outcome classification; SSE.
      • Fleet table with instance, reach and status headline columns; loopback and Host guards; real-Docker integration test with a stub agentctl.
      • Works against today's agents (they show as "telemetry not supported by this agent version"), so it proves the exec path on rpi5 early.
    2. S2 – Telemetry table.
      • Snapshot reader with the version-skew rules; all table columns from the prototype; attention ordering; stale control marking via control_observed_at.
      • Built against the contract example snapshots.
    3. S3 – Instance drawer.
      • Current attempt, control, recent events and last finished sections, with basis markers.
    4. S4 – Release v0.1.0.
      • Tag; uv tool install and systemd --user docs; manual smoke test on rpi5.

    S1 can start immediately. S2 and S3 need only the contract examples (agent-side item 1 below), not the working telemetry.

    11. Consolidated agent-side changes (destination part (a))

    Deduplicated from #224, #225, #226 and this ticket, in the order to split them into implementation issues:

    1. Discovery label and contract examples.
      • LABEL io.github.lbacik.simple-coding-agent.role="agent" in the Dockerfile.
      • docs/telemetry/ with synthetic example snapshots for schema v1: idle, attempt in progress, soft threshold crossed with handoff, recovery hold, truncated, and the error replies.
      • Unblocks dashboard S2/S3. (Decide instance discovery and the dashboard's Docker access #226, this ticket)
    2. Prerequisite bug: Control listener thread dies on an unexpected handler exception.
    3. State the snapshot needs.
      • Record process start time.
      • Pass SDK and CLI versions from provenance evidence to the lifecycle.
      • Checkpoint gains issue_title and the automatic-continuation index at claim.
      • Persist the published PR number and URL in attempt.json. (Define the telemetry snapshot contract #224)
    4. telemetry module, socket operation and command.
    5. Executor publishing immutable attempt views.
    6. Network-free control section.
    7. Recent events: a redacted in-memory ring buffer of the last 50 events in JsonEventLogger, excluding limits_checked and tool_call, with text truncated to 300 characters. (Define the telemetry snapshot contract #224)
    8. Failure isolation: the telemetry handler catches Exception and answers snapshot_failed; loop-side publishing never raises; a rate-limited telemetry_failed event. (Decide the agentctl telemetry command: socket operation, snapshot publication, failure isolation #225)

    Not in the v1 split: the expected-target-repository guard on mutating commands (v2 control, still in the map's fog).

    Records

    • ADR 0001 in agent-dashboard (above); no ADR in this repo.
    • No new tickets surfaced; no fog graduated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

wayfinder:grillingDecision requiring a live human discussion

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions