Every dashboard in the stack is generated by grafana/build.py. Grafana provisions the generated JSON read-only, so the Python file is the source of truth and the Grafana UI holds no edits of its own.
Workflow
- Edit
grafana/build.py. - Run
python3 grafana/build.py. It writes one JSON file per dashboard tografana/dashboards/and prints each panel count. - Commit the Python and the JSON together.
- Copy the repository to the home server and run
docker compose up -d --build.
On 2026-10-09 a rebuild from the committed build.py in a clean directory produced JSON identical to the seven committed files.
Building blocks
| Helper | What it produces |
|---|---|
Layout | Places panels left to right on Grafana's 24-column grid and starts rows |
stat, timeseries, heatmap, logs, table | Panels with fixed defaults; a Loki stat runs an instant query |
loki_series | A time series on Loki, drawn as lines or bars |
request_sum | LogQL sum of one field of Claude Code's api_request events, with keep before unwrap |
dashboard | Shared settings: tag agent-observability, not editable, a dashboards dropdown link |
query_var | A multi-value template variable with All as .* |
Generated dashboards
| uid | Title | Panels with rows | Variables | Default range |
|---|---|---|---|---|
| local-llm | Local LLM (Ollama) | 19 | model | 6 h |
| local-llm-public | Local LLM (Ollama), public | 19 | none | 6 h |
| host | Host (Ollama box) | 41 | none | 6 h |
| host-public | Host (Ollama box), public | 36 | none | 6 h |
| claude-code | Claude Code agents | 20 | none | 24 h |
| claude-code-public | Claude Code agents, public | 10 | none | 24 h |
| site-deploys | Website deploys | 18 | none | 7 days |
Public cuts
Each public cut is a function that takes the internal dashboard and transforms it:
local_llm_publicreplaces every$modelfilter with.*and empties the variable list.host_publicswaps per-device queries for sums or maxima, wraps every expression inmax without (...)over name labels, and drops the containers row.claude_code_publicdrops the log, trace and skill panels and wraps every expression insum without (...)over identifying labels.site_deployshas no cut: it has no variables and no private details.
A public cut gets its own uid with a -public suffix, so the internal and public versions sit side by side in the folder "Agent Observability". Which cuts are shared: Public dashboards.
Provisioning
The provider in grafana/provisioning/dashboards/dashboards.yml sets disableDeletion: true and allowUiUpdates: false, and every dashboard sets editable: false. A change made in the UI cannot be saved over a provisioned file, so every change goes through build.py.
The now() helper in site_deploys() and the request_sum helper exist because of two traps: Stat panel shows No data and Counter resets from parallel sessions.