dashboard/README.md

161 lines
7.2 KiB
Markdown

# dashboard
A single-host computer dashboard and combined task manager: live system
metrics, disk IO, process explorer, journal tail, and a systemd unit manager —
all in one browser page. Built with **FastAPI** (Python, managed by [uv]) and
**htmx** + **Chart.js** on the front end (no build step, JS is vendored).
## Screenshots
| Overview | Disks |
| --------------- | --------------- |
| ![Overview](screenshots/Overview_20260830.png) | ![Disks](screenshots/Disks_20260830.png) |
| Processes | Journal |
| ![Processes](screenshots/Processes_20260830.png) | ![Journal](screenshots/Journal_20260830.png) |
| Services | Plugins |
| ![Services](screenshots/Services_20260830.png) | ![Plugins](screenshots/Plugins_20260830.png) |
## Authorship
This code was written by **Qwen3.8-27B** (an LLM) and corrected by
Johannes Schriewer <hallo@dunkelstern.de>.
If you want to see the OpenCode transcripts see the `opencode` sub-directory.
I will include all transcripts when changing the software there.
## ⚠️ Security
This app has **no authentication and no TLS**. It binds to `127.0.0.1` by
default and is meant to be used from the machine itself. To use it remotely,
tunnel it — e.g. `ssh -L 8501:127.0.0.1:8501 user@host` and open
`http://127.0.0.1:8501` — or serve it through your own authenticated reverse
proxy / VPN. Do **not** point `DASH_HOST` at a public interface without adding
authentication yourself.
The Services tab can start/stop/enable/disable systemd units; any user who can
reach the dashboard can do that (the app uses `sudo` for privileged actions,
so the dashboard user needs sudo rights for `systemctl`).
## Quick start
Requirements: Python ≥ 3.12 and [uv]. On Linux (it is tested on Arch).
```sh
uv sync
uv run python main.py
```
Then open <http://127.0.0.1:8501>.
## Configuration
All settings are environment variables with the `DASH_` prefix; a `.env` file
in the project root is read automatically (see `.env.example`).
| Variable | Default | Meaning |
| ------------------------ | ----------------------- | ------------------------------------------------ |
| `DASH_HOST` | `127.0.0.1` | Bind address |
| `DASH_PORT` | `8501` | Port |
| `DASH_SAMPLE_INTERVAL` | `2.0` | Seconds between samples |
| `DASH_RETENTION_MINUTES` | `60` | In-memory history window (lost on restart) |
| `DASH_LLAMA_BASE_URL` | `http://127.0.0.1:8080` | llama-server (router mode) base URL |
| `DASH_LLAMA_API_KEY` | *(empty)* | Set if llama-server runs with `--api-key` |
| `DASH_LLAMA_TIMEOUT` | `4.0` | Seconds for llama-server requests |
## Tabs
- **Overview** — CPU + GPU (utilisation, CPU temperature, VRAM, GPU temperature
when exposed by sysfs/hwmon), RAM + Swap + VRAM, active Wi-Fi connection
(SSID via `iw`) and IP addresses of all up interfaces. History charts
(CPU/GPU, memory/VRAM, disk I/O) are in-memory, sampled every
`DASH_SAMPLE_INTERVAL` for `DASH_RETENTION_MINUTES`.
- **Disks** — partitions with size/use and per-device read/write rates.
- **Processes** — live table, filterable and sortable by CPU, memory, RSS, GPU
and IO columns.
- **Journal** — streaming `journalctl` tail with level filter, unit filter,
free-text search and a "hide sudo" toggle (on by default, since the
dashboard's own `sudo systemctl`/`journalctl` calls otherwise log noise).
Uses journal cursors, so filtering does not re-dump history.
- **Services** — all systemd service units with state and enabled-ness,
sortable by name, state and enabled-ness; click a
name for details (main PID, start time, restarts, recent journal lines) and
run `start` / `stop` / `restart` / `enable` / `disable` actions.
- **Plugins** — currently **llama.cpp**: model status, load/unload buttons and
a rescan for a `llama-server` running in router mode; **sleep
inhibitors**: active block-mode `systemd-inhibit` locks with a verdict on
whether the machine may sleep right now, plus a switch that makes the
dashboard itself hold a sleep lock (released again on shutdown); and
**GPU power profiles** (LACT): the active profile per GPU is polled, and
each GPU gets a profile dropdown with a *set* and a *reload* button.
### llama.cpp router mode
The plugin talks to a `llama-server` started with a models directory (router
mode), which exposes the native `/models`, `/models/load` and `/models/unload`
endpoints:
```sh
llama-server --models-dir /path/to/your/models --host 127.0.0.1 --port 8080
```
The plugin polls `GET /health` and `GET /models`, posts `{"model": id}` to
`/models/load` and `/models/unload`, and rescans with `GET /models?reload=1`.
If the server is down the plugin shows *unreachable* and the rest of the
dashboard keeps working.
### GPU power profiles (LACT)
The plugin shells out to `lact cli` (`lact` must be in PATH). It lists the
GPUs with `lact cli list`, polls the active profile of every GPU with
`lact cli --gpu-id <id> profile get`, and applies a selected profile with
`lact cli --gpu-id <id> profile set <name>`. The per-GPU profile dropdowns
are *not* refreshed automatically (LACT auto-switching can change the active
profile behind the scenes, which the polled badge picks up); use the per-GPU
*reload* button to refresh them.
## Running as a systemd service
A ready-made unit is in [`deploy/dashboard.service`](deploy/dashboard.service):
You have to edit that file to point to the checkout of the tool and change the user and group!
```sh
cp deploy/dashboard.service /etc/systemd/system/dashboard.service
# adjust User= and paths if needed
uv sync # once, after changing dependencies
systemctl daemon-reload
systemctl enable --now dashboard
journalctl -u dashboard -f
```
## Project layout
```
main.py # uvicorn entry point
app/
config.py # pydantic-settings (DASH_* env)
main.py # app factory, lifespan sampler
sampling.py # background sampler task
state.py # in-memory ring buffers
collect/ # cpu / mem / gpu / disks / procs / net collectors (psutil + sysfs)
systemd/units.py # unit list / detail / whitelisted actions (sudo fallback)
journal.py # journalctl -o export parser + cursors
render.py # jinja env + filters
routers/ # overview / disks / processes / journal / services / plugins
plugins/ # base.Plugin + llamacpp + sleep + lact plugins
templates/ # htmx fragments
static/ # css, js, vendored htmx + chart.js
deploy/ # dashboard.service
```
Adding a plugin: create a module in `app/plugins/` defining a `Plugin`
instance (id, title, skeleton function) and register it in
`app/plugins/__init__.py`. The skeleton is the static card shell, rendered
once; it embeds the dynamically polled regions (e.g. a div with
`hx-get`/`hx-trigger="every Ns"` pointing at the plugin's own endpoints).
## License
[The Unlicense](LICENSE) — public domain dedication, no conditions.
[uv]: https://docs.astral.sh/uv/