dashboard/AGENTS.md
2026-08-31 00:47:02 +02:00

5.2 KiB

AGENTS.md

Guidance for AI coding agents working in this repository.

What this is

A single-host computer dashboard and task manager: FastAPI (Python ≥ 3.12, managed with uv) backend serving htmx + Chart.js HTML fragments to one browser page. Binds to 127.0.0.1:8501, no auth by design (see the README security section). Tabs: Overview, Disks, Processes, Journal, Services (systemd), Plugins (llama.cpp, sleep, LACT). Licensed under the Unlicense (see LICENSE).

Commands

uv sync                 # install dependencies
uv run python main.py   # run the server on http://127.0.0.1:8501

There is no test suite. Verify changes with:

uv run python -m compileall -q app
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8501/api/overview
# ... other endpoints: /api/disks /api/processes /api/journal /api/services
#                      /api/plugins /api/history
grep -c Traceback /tmp/dash.log

Restarting the dev server

The server usually runs detached in the background. To restart it:

PID=$(pgrep -f "python main\.py" | head -1)
[ -n "$PID" ] && kill "$PID"
setsid nohup uv run python main.py > /tmp/dash.log 2>&1 < /dev/null & disown

Never use pkill -f "uv run python main.py" — the pattern also matches the agent's own shell command line and kills the session.

Architecture

  • app/collect/* — collectors (cpu/mem/gpu/disks/procs/net/power) read psutil + sysfs; app/sampling.py runs them every DASH_SAMPLE_INTERVAL (default 2 s) into an in-memory ring buffer (app/state.py).
  • app/utils/ — shared helpers: subprocess.py (run/run_async return (rc, stdout, stderr) and never raise — missing binary or timeout is rc -1 with the reason in stderr; run_json* variants parse stdout as JSON), sysfs.py (read_str/read_int/read_float), gpu.py (shorten), window.py (the /api/history windowing), systemd.py (unit listing/detail/actions; list-units/list-unit-files use --output=json, needs systemd ≥ ~246, show has no JSON).
  • app/routers/* — each tab endpoint is an idempotent GET returning an htmx HTML fragment; templates live in templates/ and self-poll via hx-get + hx-trigger="every Ns" + hx-swap="outerHTML".
  • templates/*.html auto-reload on file change — no restart needed for template-only edits. Python changes require a restart.
  • app/journal.pyjournalctl -o json parser (one JSON object per line) with cursors.
  • app/plugins/base.Plugin (optional open/close lifecycle hooks run from app lifespan) + llamacpp plugin (talks to a router-mode llama-server on port 8080) + sleep plugin (lists block-mode systemd-inhibit locks; holds its own sleep lock via a systemd-inhibit ... sleep infinity child while the UI switch is on, reaps stale locks by who marker on startup)
    • lact plugin (shells out to lact cli: per-GPU profile dropdown with set/reload, active profile polled every 5 s, GPU names shortened with app/utils/gpu.py:shorten like the overview card).

Conventions

  • Google-style docstrings for every function and class: one-line imperative summary, an Args: section for each parameter, and Returns:/Raises: where non-obvious. Complex functions (parsers, subprocess wrappers, anything touching the pitfalls below) get extra prose explaining the behaviour, not just the signature.
  • Inline comments are allowed only for Sample dataclass field docs.
  • basedpyright is configured as linter, use with uvx.
  • Match surrounding style; keep functions small and typed where the codebase already is.
  • Keep polling endpoints cheap: collectors may cache lookups (unit names, enabled-state maps, SSID, temperature paths) with short TTLs.

Hard-won pitfalls

  • Jinja autoescape renders &#8595; as literal text — use literal unicode (e.g. ) in templates.
  • journalctl is queried with -o json on purpose: the old -o export output contains NUL bytes (grep treats it as binary) and multi-line values need continuation-line handling, while JSON escapes both. journalctl still rejects negated matches (!/!=) — filter entries in Python instead.
  • nvidia-smi --format=json keys are underscored and values are strings; lspci has no JSON (use the locale-stable -mm double-quoted format); systemctl show, systemctl is-system-running, iw, and lact cli have no JSON output at all.
  • psutil gotchas: there is no psutil.AF_INET (use socket); net_if_addrs() / net_if_stats() take no arguments; sensors_battery().power_plugged can be None — use /sys/class/power_supply/* (type/capacity/status/online) for battery + AC state.
  • iw dev <if> link prints SSID: name unquoted; the working regex is SSID:\s+(\S.*) (a $ anchor fails without MULTILINE).
  • /api/history window-averages the ring buffer down to at most chart_max_points (default 200) points, emitting {avg, min, max} per key, and pads each of the three arrays with null for windows missing a key so they stay aligned with the timestamps — keep that behaviour if you touch it.
  • AMD sysfs: GPU busy/VRAM/temp under /sys/class/drm/card*/device (+ hwmon), CPU temp from the k10temp hwmon (fallback acpitz thermal zone), both in millidegrees.