diff --git a/nix/host-modules/swarm-grafana/dashboards/agents.json b/nix/host-modules/swarm-grafana/dashboards/agents.json index 02b9ec35..9cc817aa 100644 --- a/nix/host-modules/swarm-grafana/dashboards/agents.json +++ b/nix/host-modules/swarm-grafana/dashboards/agents.json @@ -6,7 +6,7 @@ }, "spec": { "title": "hyperhive · agents", - "description": "Per-agent view: resource usage from hive-c0re's container sampler, activity from Claude Code's own export. Renamed from 'overview' — every producer we have today is per-agent, so a genuine swarm overview has nothing cross-cutting to summarise yet and should be built last, not first. Metric names are the OTLP DOTTED form, so every query uses the {__name__=\"...\"} selector — the underscore spelling matches nothing in this store. Cost/token totals use increase(), which is correct here: the claude_code.* counters are cumulative (forced in nix/agent-modules/otel.nix) but reset once per turn, since each turn is a new claude process.", + "description": "Per-agent resource usage and Claude Code activity.", "editable": true, "cursorSync": "Off", "liveNow": false, @@ -129,7 +129,7 @@ "spec": { "id": 1, "title": "Agents reporting", - "description": "Distinct agents whose container metrics reached the store in the selected range. This counts what hive-c0re observes, not what the topology declares — a gap between the two is the interesting case.", + "description": "Distinct agents with container metrics in the selected range.", "links": [], "data": { "kind": "QueryGroup", @@ -196,7 +196,7 @@ "spec": { "id": 10, "title": "Cost rate by agent", - "description": "USD per hour, from rate() over the cumulative counter — reset-safe. A flat line at zero is an idle agent, not a broken exporter; check the CPU panel to tell them apart.\n\nThe 10m minimum step is load-bearing, not a style choice: agents export every 60s, and measured against this store a rate window of 1m or 2m resolves ONE agent instead of five. Any shorter and the panel quietly under-reports.", + "description": "USD per hour spent by each agent.", "links": [], "data": { "kind": "QueryGroup", @@ -272,7 +272,7 @@ "spec": { "id": 11, "title": "Container CPU by agent", - "description": "hyperhive.container.cpu.percent, sampled by hive-c0re. Percent of one core.", + "description": "Percent of one CPU core used by each agent's container.", "links": [], "data": { "kind": "QueryGroup", @@ -346,7 +346,7 @@ "spec": { "id": 2, "title": "Cost", - "description": "sum(increase(claude_code.cost.usage)). Agent-reported, so it covers claude's own token spend only — nothing else in the swarm reports cost.", + "description": "Total USD spent on Claude Code usage.", "links": [], "data": { "kind": "QueryGroup", @@ -485,7 +485,7 @@ "spec": { "id": 21, "title": "Tokens by model (excl. cache reads)", - "description": "Split by model rather than by agent on purpose — 'which model is the spend on' is the question this answers. Excludes type=cacheRead for the same reason as the tile above: at ~99% of volume it flattens every model into one line shaped like its cache. 10m minimum step for the same reason as the cost-rate panel.", + "description": "Tokens consumed per model, excluding cache reads.", "links": [], "data": { "kind": "QueryGroup", @@ -561,7 +561,7 @@ "spec": { "id": 3, "title": "Tokens (excl. cache reads)", - "description": "Excludes type=cacheRead, which would otherwise BE this number: measured over 6h on this store, cacheRead was 273.5M of 275.8M total — 99.2%. Counting it makes the tile a cache-hit-rate readout wearing a token label, and it drowns the input/output volume that actually tracks work done. Remaining types: input, output, cacheCreation.", + "description": "Total tokens consumed, excluding cache reads.", "links": [], "data": { "kind": "QueryGroup", @@ -627,7 +627,7 @@ "spec": { "id": 30, "title": "Container storage by agent", - "description": "Magnitude comparison, so a bar rather than seven lines. One fixed colour on purpose: the agents are nominal categories, and colouring them by size would encode the bar length twice.", + "description": "Disk usage per agent's container.", "links": [], "data": { "kind": "QueryGroup", @@ -702,7 +702,7 @@ "spec": { "id": 31, "title": "Lines of code changed", - "description": "claude_code.lines_of_code.count carries a `type` label (added / removed). 10m minimum step for the same reason as the cost-rate panel.", + "description": "Lines of code added and removed by Claude Code.", "links": [], "data": { "kind": "QueryGroup", @@ -778,7 +778,7 @@ "spec": { "id": 32, "title": "Active time by agent", - "description": "claude_code.active_time.total — seconds an agent was actually working, not wall-clock uptime.", + "description": "Seconds each agent spent actively working.", "links": [], "data": { "kind": "QueryGroup", @@ -854,7 +854,7 @@ "spec": { "id": 33, "title": "CLAUDE.md size by agent", - "description": "hyperhive.agent.claude_md.lines over time — mara: \"i want to see it over time\" (a bargauge snapshot in an earlier round of this panel only showed the current count). A continuously-live gauge, not a per-turn counter, so this is the raw value plotted across the range, same treatment as the CPU/memory-by-agent panels above rather than a rate()/increase(). The harness itself raises a loose-ends todo past 500 lines suggesting a trim, not a hard limit — this panel is visibility, not an alarm.", + "description": "Lines in each agent's CLAUDE.md file over time.", "links": [], "data": { "kind": "QueryGroup", @@ -928,7 +928,7 @@ "spec": { "id": 4, "title": "CLI sessions started", - "description": "claude_code.session.count counts CLI STARTS, and the harness starts one claude process per turn — so this reads as turns, not as long-lived sessions. Named for what it measures rather than what the metric is called.", + "description": "Claude CLI turns started, one per session count.", "links": [], "data": { "kind": "QueryGroup", @@ -994,7 +994,7 @@ "spec": { "id": 40, "title": "Subagent cost", - "description": "sum(increase(claude_code.cost.usage{subagent!=\"\"})). `hive-subagent-mcp` stamps a `subagent=` resource attribute (alongside the parent's own `agent=`) on every claude_code.* sample a spawned subagent turn emits — this label already existed, it just wasn't plotted anywhere.", + "description": "USD spent by subagent turns.", "links": [], "data": { "kind": "QueryGroup", @@ -1060,7 +1060,7 @@ "spec": { "id": 41, "title": "Subagent turns", - "description": "claude_code.session.count where subagent!=\"\" — a proxy for subagent turns, same PROXY caveat as the plain 'CLI sessions started' tile above.", + "description": "Number of subagent turns run.", "links": [], "data": { "kind": "QueryGroup", @@ -1126,7 +1126,7 @@ "spec": { "id": 42, "title": "Subagent cost share", - "description": "Subagent cost as a fraction of all cost in the selected range — 'basically free' is a claim this tile can actually check.", + "description": "Subagent cost as a fraction of total cost.", "links": [], "data": { "kind": "QueryGroup", @@ -1192,7 +1192,7 @@ "spec": { "id": 43, "title": "Cost by subagent name", - "description": "claude_code.cost.usage summed over the selected range, split by the caller-chosen `subagent` name (e.g. an exploration/relabeling task's session name) rather than which agent spawned it.", + "description": "USD spent, split by subagent name.", "links": [], "data": { "kind": "QueryGroup", @@ -1268,7 +1268,7 @@ "spec": { "id": 44, "title": "Subagent cost by parent agent", - "description": "Same claude_code.cost.usage{subagent!=\"\"}, grouped by the spawning agent's own `agent` label instead — which agent's subagent habit is actually costing something.", + "description": "USD spent on subagents, split by parent agent.", "links": [], "data": { "kind": "QueryGroup", @@ -1344,7 +1344,7 @@ "spec": { "id": 45, "title": "Distinct subagents (range)", - "description": "How many differently-named subagents ran at all in the selected range, not how many turns they took — 'Subagent turns' above already covers volume, this answers a different question (mara: \"show subagent count in time range\"). Counts subagent label values with at least one turn (increase(...) > 0) rather than every label value claude_code.session.count has ever carried, so a stale series with no turns in-range doesn't inflate the count.", + "description": "Number of differently-named subagents that ran.", "links": [], "data": { "kind": "QueryGroup", @@ -1410,7 +1410,7 @@ "spec": { "id": 46, "title": "Distinct subagents by agent, over time", - "description": "mara: \"the subagent should already stamp the agent on the metric. so i want to see count of distinct subagent names per agent name over time\" — it does (claude_code.session.count carries both agent and subagent labels on a subagent's own turns). One line per parent agent: at each point, how many differently-named subagents had at least one turn in the trailing $__rate_interval window. This is a ROLLING distinct count, not a running total — it can go back down once a batch of subagents stops running, same shape as any other rate()-style panel on this board. A true concurrent-right-now count (subagents open at this exact instant, able to rise and fall between scrapes) would need hive-subagent-mcp to publish its own gauge on start/stop; nothing does today, and that's a different, harder ask than this one.", + "description": "Distinct subagent names active per agent, over time.", "links": [], "data": { "kind": "QueryGroup", diff --git a/nix/host-modules/swarm-grafana/dashboards/authelia.json b/nix/host-modules/swarm-grafana/dashboards/authelia.json index dc72bab4..b52b4fe3 100644 --- a/nix/host-modules/swarm-grafana/dashboards/authelia.json +++ b/nix/host-modules/swarm-grafana/dashboards/authelia.json @@ -1,7 +1,7 @@ { "title": "hyperhive · authelia", "uid": "hyperhive-authelia", - "description": "The swarm's identity provider. Split out of the swarm-services board because it was most of it — every other service either has no exporter yet or is still failing to scrape. Two things to know before editing: counters here are COUNTS over the dashboard range, not per-second rates, because this IdP has served a few hundred requests in its whole lifetime and rate() over any sane window is exactly zero — which draws a flat line and is indistinguishable from a broken query. And targets are labelled service.name, NOT job; there is no job label in this store.", + "description": "The swarm's identity provider.", "editable": true, "refresh": "1m", "schemaVersion": 39, @@ -20,7 +20,7 @@ "id": 1, "type": "stat", "title": "Uptime", - "description": "Time since the authelia process started. A reset here explains a counter reset below — worth checking before reading a drop as a change in behaviour. Note authelia is restarted by swarmctl on identity changes, so short uptimes are normal rather than alarming.", + "description": "Time since the authelia process started.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -71,7 +71,7 @@ "id": 2, "type": "stat", "title": "Authentications (range)", - "description": "authelia_authn_duration_count over the dashboard's own time range — a login, not a per-request authorization check. Deliberately range-scoped rather than a fixed window: at this volume an hourly figure reads 0 nearly always, and a zero that is always zero teaches nobody anything.", + "description": "Login attempts in the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -122,7 +122,7 @@ "id": 3, "type": "stat", "title": "Failed authentications (range)", - "description": "The success=\"false\" arm of the same counter. Red on any non-zero value — a failed login against the swarm's identity provider is worth a look even as a single event, because nothing here should be guessing passwords.", + "description": "Failed login attempts in the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -186,7 +186,7 @@ "id": 4, "type": "stat", "title": "Authorization decisions (range)", - "description": "authelia_authz — the forward-auth verdict the gateway asks for on every proxied request. This is the busy one: it fires per request, where authn fires per login, which is why the two counts differ by orders of magnitude and should.", + "description": "Forward-auth verdicts issued in the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -237,7 +237,7 @@ "id": 10, "type": "bargauge", "title": "Requests by status code (range)", - "description": "Counts over the dashboard range, not a per-second rate — see the board description for why. A 401 here is normal traffic (an unauthenticated request being told to log in); a 5xx is not.", + "description": "HTTP requests grouped by status code.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -285,7 +285,7 @@ "id": 11, "type": "bargauge", "title": "Authorization verdicts by code (range)", - "description": "The gateway's forward-auth outcomes. A rising 403/401 count against a flat authn count means something is being REFUSED, not that someone is failing to log in — that distinction is the reason this panel sits beside the requests one rather than being folded into it.", + "description": "Forward-auth outcomes grouped by status code.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -333,7 +333,7 @@ "id": 20, "type": "timeseries", "title": "Request latency (p95)", - "description": "histogram_quantile over authelia_request_duration_bucket, using increase() over the dashboard range rather than rate() over a short window. Measured: at this traffic level a 1m rate window contains no requests, every bucket is zero, and the quantile is NaN — the panel renders EMPTY, not zero. Note the sum by (le): dropping every other label before the quantile is required, or buckets from different codes get mixed and the answer is silently wrong rather than absent.", + "description": "95th-percentile request latency.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -390,7 +390,7 @@ "id": 21, "type": "timeseries", "title": "OIDC request latency (p95)", - "description": "The same quantile over authelia's separate openid_connect duration family. Worth its own panel rather than being merged into the one beside it: every machine-to-machine credential in this swarm is minted through the OIDC endpoints, so this is the latency that agents and services actually feel, while the general one is dominated by browser traffic.", + "description": "95th-percentile latency for OIDC endpoints.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -447,7 +447,7 @@ "id": 30, "type": "timeseries", "title": "Resident memory", - "description": "process_resident_memory_bytes for the authelia process. Included because authelia holds its user store in memory and reloads it on file change (file.watch, #3403) — a step here after an identity is created is expected; a monotonic climb is not.", + "description": "Memory used by the authelia process.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -504,7 +504,7 @@ "id": 31, "type": "timeseries", "title": "Goroutines", - "description": "The cheapest leak detector Go gives you. Flat is healthy; a staircase that never comes down usually means a handler is blocking on something that never returns — which for an IdP is most often an outbound call to a service that is down.", + "description": "Number of active Go routines.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -561,7 +561,7 @@ "id": 32, "type": "timeseries", "title": "Open file descriptors", - "description": "process_open_fds. Paired with goroutines because the two rise together when connections are being held rather than closed, and an fd exhaustion is one of the few ways this process fails hard rather than returning an error.", + "description": "Open file descriptors held by the process.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" diff --git a/nix/host-modules/swarm-grafana/dashboards/bao.json b/nix/host-modules/swarm-grafana/dashboards/bao.json index be742cd2..cc3af46d 100644 --- a/nix/host-modules/swarm-grafana/dashboards/bao.json +++ b/nix/host-modules/swarm-grafana/dashboards/bao.json @@ -1,7 +1,7 @@ { "title": "hyperhive · openbao", "uid": "hyperhive-bao", - "description": "The swarm's secret store. Two things to know before editing: the metric prefix is vault_, not openbao_ — the fork kept upstream's names, and a panel built on the name you would guess renders perfectly and shows nothing. And a SEALED store emits almost nothing: only the top row works before an operator has run `bao operator init`, everything below it is empty until then and that is the expected reading, not a broken query. Targets are labelled service.name, NOT job; there is no job label in this store. Counters are counts over the dashboard range rather than per-second rates, because a store this quiet rates to exactly zero.", + "description": "The swarm's secret store.", "editable": true, "refresh": "1m", "schemaVersion": 39, @@ -20,7 +20,7 @@ "id": 1, "type": "stat", "title": "Seal state", - "description": "SEALED means the store is up but refusing every read — the state it boots into, and the state it returns to on restart unless auto-unseal is configured. The max() is load-bearing: once unsealed there are two vault_core_unsealed series and the cluster=\"\" one stays 0 forever, so a bare selector reads SEALED on a healthy store.", + "description": "Whether the store is sealed or unsealed.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -99,7 +99,7 @@ "id": 2, "type": "stat", "title": "Uptime", - "description": "Time since the openbao process started. Read it beside the seal state: a restart re-seals the store, so a short uptime and a SEALED panel are one event rather than two problems.", + "description": "Time since the openbao process started.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -150,7 +150,7 @@ "id": 3, "type": "stat", "title": "Goroutines", - "description": "A Go runtime metric, so it is present even while sealed — which makes it the panel that distinguishes a store with nothing to say from a scrape that is not arriving at all. No data here means the collector is not reaching the metrics listener.", + "description": "Number of active Go routines.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -201,7 +201,7 @@ "id": 4, "type": "stat", "title": "Requests (range)", - "description": "vault_core_handle_request_count over the dashboard range — every API call the store served, unseal attempts included. The summary's _count series, not the summary itself. Zero on a sealed store, and zero is the honest answer there rather than an absent panel.", + "description": "API requests served in the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -252,7 +252,7 @@ "id": 10, "type": "stat", "title": "Seal operations (range)", - "description": "Encrypt and decrypt calls against the seal wrapper — on this swarm a TPM through PKCS#11, so each one is a round trip to hardware. These are counters with no _total suffix. A climbing decrypt count with a flat request count means something is re-reading the barrier rather than serving clients.", + "description": "Encrypt/decrypt calls against the seal wrapper.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -312,7 +312,7 @@ "id": 11, "type": "stat", "title": "Raft peers", - "description": "Voting members of the store's raft cluster. This swarm runs a single node, so 1 is the expected value and anything else means a peer was added — or that this node no longer sees itself as part of the cluster it stores data for.", + "description": "Voting members of the raft cluster.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -363,7 +363,7 @@ "id": 20, "type": "timeseries", "title": "Request latency", - "description": "Quantiles openbao computes itself — a summary, so the store picked the buckets and a histogram_quantile() is neither needed nor possible here. Units are MILLISECONDS: vault's timers report ms, and reading them as seconds understates the store by three orders of magnitude.", + "description": "Request latency quantiles.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -429,7 +429,7 @@ "id": 21, "type": "timeseries", "title": "Leases", - "description": "Outstanding leases, and the irrevocable ones beside them. The second line should sit at zero: an irrevocable lease is one the store gave up trying to revoke, so a credential somewhere outlives what issued it and no amount of waiting will clean it up.", + "description": "Outstanding and irrevocable leases.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -495,7 +495,7 @@ "id": 30, "type": "timeseries", "title": "Heap in use", - "description": "go_memstats_heap_inuse_bytes. Present while sealed, so it is a live line even on a store that is serving nothing — a sawtooth is normal Go behaviour, a monotonic climb on an idle store is not.", + "description": "Go heap memory in use.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" diff --git a/nix/host-modules/swarm-grafana/dashboards/claude-usage.json b/nix/host-modules/swarm-grafana/dashboards/claude-usage.json index 06a22fbc..541efcf4 100644 --- a/nix/host-modules/swarm-grafana/dashboards/claude-usage.json +++ b/nix/host-modules/swarm-grafana/dashboards/claude-usage.json @@ -1,7 +1,7 @@ { "uid": "hyperhive-claude-usage", "title": "hyperhive · claude usage", - "description": "How Claude is being used, rather than which agent is using it: the axes here are model, effort, token type and query source. Per-agent resource usage lives on the 'agents' dashboard; the only overlap is the cost/token headline. Metric names are the OTLP DOTTED form, so every query uses the {__name__=\"...\"} selector — the underscore spelling matches nothing in this store. Every total uses increase(): the claude_code.* counters are cumulative (forced in nix/agent-modules/otel.nix) but reset once per turn, because each turn is a new claude process, so sum_over_time would be wrong. Rate panels pin a 10m minimum interval — agents export every 60s, and a shorter window silently drops most series while still drawing a plausible line.", + "description": "How Claude is used, by model, effort, token type, and source.", "tags": ["hyperhive", "claude"], "timezone": "utc", "schemaVersion": 39, @@ -59,7 +59,7 @@ "id": 1, "type": "stat", "title": "Cost", - "description": "claude_code.cost.usage, summed over the selected range.", + "description": "Total USD spent on Claude Code usage.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -107,7 +107,7 @@ "id": 2, "type": "stat", "title": "Tokens", - "description": "claude_code.token.usage across every type (input, output, cacheRead, cacheCreation).", + "description": "Total tokens used, across all types.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -155,7 +155,7 @@ "id": 3, "type": "stat", "title": "Cache read share", - "description": "cacheRead as a fraction of all tokens. High is good and cheap: cached input is billed at a fraction of fresh input. A sudden drop usually means prompts stopped being stable, not that more work happened.", + "description": "Cache-read tokens as a fraction of all tokens.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -203,7 +203,7 @@ "id": 4, "type": "stat", "title": "Active time", - "description": "claude_code.active_time.total — seconds actually spent working, not wall-clock uptime.", + "description": "Seconds actually spent working.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -251,7 +251,7 @@ "id": 10, "type": "timeseries", "title": "Token rate by type", - "description": "Tokens per minute, split by type. cacheRead usually dominates by an order of magnitude — that is the cache working, not a bug. 10m minimum step: agents export every 60s, so a shorter window drops series silently.", + "description": "Tokens per minute, split by type.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -313,7 +313,7 @@ "id": 11, "type": "bargauge", "title": "Tokens by model", - "description": "Which model the tokens went to, over the selected range.", + "description": "Tokens used, split by model.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -372,7 +372,7 @@ "id": 20, "type": "timeseries", "title": "Cost rate by model", - "description": "USD per minute, split by model. Same 10m minimum step as the token panel.", + "description": "USD per minute, split by model.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -430,7 +430,7 @@ "id": 21, "type": "bargauge", "title": "Cost by effort", - "description": "claude_code.cost.usage split by the effort label (high / medium / xhigh).", + "description": "USD spent, split by effort level.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -489,7 +489,7 @@ "id": 22, "type": "bargauge", "title": "Cost by query source", - "description": "'main' is the turn you asked for; 'auxiliary' is everything Claude runs on the side (titles, summaries, small helper calls). Worth watching as a ratio — auxiliary creeping up means spend that no turn asked for.", + "description": "USD spent, split by main vs. auxiliary queries.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -548,7 +548,7 @@ "id": 30, "type": "bargauge", "title": "Edit tool decisions by language", - "description": "claude_code.code_edit_tool.decision. The decision is in the legend rather than assumed: today every recorded decision is 'accept', so a title saying 'accepted' would be silently wrong the first time one is rejected.", + "description": "Edit-tool accept/reject decisions, split by language.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -607,7 +607,7 @@ "id": 31, "type": "bargauge", "title": "Edit tool decisions by tool", - "description": "Same series split by tool_name (Edit / Write) instead of language.", + "description": "Edit-tool accept/reject decisions, split by tool.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -666,7 +666,7 @@ "id": 5, "type": "stat", "title": "Turns", - "description": "claude_code.session.count. Each turn runs a new claude process, so a session record is emitted per turn — this is a PROXY for turn count, not a turn metric. Nothing exports turn stats to OTEL today; they live in each agent turn-stats.sqlite and are aggregated only for the hive dashboard.", + "description": "Number of Claude turns run.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -714,7 +714,7 @@ "id": 6, "type": "stat", "title": "Active time per turn", - "description": "Mean of claude_code.active_time.total divided by the session count above. This is ACTIVE seconds per turn, not wall-clock turn duration — time spent waiting on a tool or on the operator is not in it. A counter gives a mean only; there is no p50/p95 without the harness exporting a histogram.", + "description": "Average active seconds per turn.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -762,7 +762,7 @@ "id": 40, "type": "timeseries", "title": "Turns per hour by agent", - "description": "Turns per hour, as a rolling 1h count. PROXY: each turn runs a new claude process, so a session record stands in for a turn.\n\n⚠️ The window is a fixed [1h] and deliberately does NOT follow the zoom: \"per hour\" is the definition of the number, not a resolution choice. rate() cannot be used here at all — a session series is only 1-3 samples long, so rate() returns a series whose value is zero, which draws a flat line rather than an error.", + "description": "Turns per hour, per agent.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -820,7 +820,7 @@ "id": 41, "type": "bargauge", "title": "Active time per turn by agent", - "description": "Mean active seconds per turn, per agent, over the selected range. A high value is a long working turn, not a slow one — waiting is not counted.", + "description": "Average active seconds per turn, per agent.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -879,7 +879,7 @@ "id": 42, "type": "bargauge", "title": "Cost by agent", - "description": "claude_code.cost.usage summed over the selected range, broken out by agent — which agent the spend belongs to, as opposed to the model/effort/source breakdowns above.", + "description": "USD spent, split by agent.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -938,7 +938,7 @@ "id": 43, "type": "bargauge", "title": "Tokens by agent", - "description": "claude_code.token.usage summed over the selected range, broken out by agent (all token types combined).", + "description": "Tokens used, split by agent.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" diff --git a/nix/host-modules/swarm-grafana/dashboards/forge.json b/nix/host-modules/swarm-grafana/dashboards/forge.json index a7701b79..6fe20382 100644 --- a/nix/host-modules/swarm-grafana/dashboards/forge.json +++ b/nix/host-modules/swarm-grafana/dashboards/forge.json @@ -1,7 +1,7 @@ { "title": "hyperhive · forge (content)", "uid": "hyperhive-swarm-forge", - "description": "An INVENTORY board, not a service-health board — and the difference is the first thing to know about it. Its three siblings (metrics store, log store, authelia) answer 'is this service healthy': request rates, error rates, latency. Forgejo's exporter publishes no such series; what it publishes is a census of what the forge CONTAINS. So every number here is a gauge of stored things, sampled at scrape time. Nothing on this board can tell you the forge is slow or erroring, and a flat line means nobody created anything, not that scraping stopped — check the metrics-store board for that. Series are labelled service.name, NOT job; there is no job label in this store.", + "description": "An inventory of what the forge holds.", "editable": true, "refresh": "5m", "schemaVersion": 39, @@ -15,7 +15,7 @@ "id": 1, "type": "stat", "title": "Repositories", - "description": "Every repo the forge holds, including agent config repos — which is most of them on a busy hive, so this tracks agent count more closely than it tracks project count.", + "description": "Total repositories on the forge.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 }, "targets": [ @@ -52,7 +52,7 @@ "id": 2, "type": "stat", "title": "Users", - "description": "Forge accounts. Agents hold one each, so this is operators plus the live agent roster — a drop without a matching agent teardown is worth a look.", + "description": "Total forge accounts.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 }, "targets": [ @@ -89,7 +89,7 @@ "id": 3, "type": "stat", "title": "Organizations", - "description": "Orgs — the forge's coarsest grouping, and on a hyperhive forge the line between agent-owned repos and everything else. A change is a structural decision someone made rather than activity; how often that happens is a fact about this hive's projects, not something the panel can assume.", + "description": "Total forge organizations.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 }, "targets": [ @@ -126,7 +126,7 @@ "id": 4, "type": "stat", "title": "Issues open", - "description": "Open issues across every repo. The one number on this board that behaves like a backlog gauge — rising means work is being filed faster than it is closed.", + "description": "Currently open issues, across every repo.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 }, "targets": [ @@ -163,7 +163,7 @@ "id": 5, "type": "stat", "title": "Issues closed", - "description": "Cumulative closed issues. Only ever rises, so read it as throughput-to-date rather than as a current state.", + "description": "Cumulative closed issues, across every repo.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 4, "w": 4, "x": 16, "y": 0 }, "targets": [ @@ -200,7 +200,7 @@ "id": 6, "type": "stat", "title": "Releases", - "description": "Tagged releases across every repo. Cumulative, so it only rises; the rate is the signal, not the level. Whether zero is normal here depends entirely on how this hive's projects ship — a board shipped to every hive cannot know that, so read a CHANGE rather than a value.", + "description": "Cumulative tagged releases, across every repo.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 0 }, "targets": [ @@ -237,7 +237,7 @@ "id": 10, "type": "timeseries", "title": "Issues: open vs closed", - "description": "The two together, because either alone misleads. Open flat while closed climbs is a backlog being worked down; both climbing together is a backlog growing while work happens. ⚠️ A vertical step in closed is a batch operation of some kind — a bulk close, an import, a repo removal — and is not distinguishable here from a burst of real work; the issue list is what tells them apart.", + "description": "Open and closed issue counts, over time.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, "targets": [ @@ -284,7 +284,7 @@ "id": 11, "type": "timeseries", "title": "Repositories and users", - "description": "Both climb when an agent is created — a config repo and an account arrive together. So the interesting reading is when they DIVERGE: repos without users means repos created by hand, users without repos means accounts provisioned for something that never got one.", + "description": "Repository and user counts, over time.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, "targets": [ @@ -331,7 +331,7 @@ "id": 20, "type": "timeseries", "title": "Discussion volume", - "description": "Comments and review comments across the forge. The closest thing here to an activity signal, and still a cumulative count rather than a rate — a steepening slope is more conversation, a flat stretch is a quiet period, and neither says anything about whether the forge is up.", + "description": "Comments and review comments, over time.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 12 }, "targets": [ @@ -378,7 +378,7 @@ "id": 21, "type": "timeseries", "title": "Automation surface", - "description": "Webhooks and their queued delivery tasks. Worth a glance when something that should have triggered did not: a hooktask count that climbs and never settles is deliveries backing up rather than being consumed. The webhook count itself is whatever this hive has wired — it says nothing on its own, and a jump in it is someone's integration change.", + "description": "Webhooks and their queued delivery tasks.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 12 }, "targets": [ @@ -425,7 +425,7 @@ "id": 30, "type": "bargauge", "title": "Issues by label — every issue ever, ranked", - "description": "How the whole issue corpus is distributed across labels, biggest first. NOT the open backlog: this metric counts open and closed issues alike, which is checkable from this board alone — the top label here is far larger than the Issues open stat beside it, and no single label can outnumber the issues it is a subset of. So read it as the shape of what the project has worked on, not as what is left. It is also a RANKING of one moment rather than a time series: the label set is as wide as the tracker's, so one line per label is unreadable by construction, and the history of any single label is an ad-hoc query, not a panel. Colour carries no meaning here; the axis carries the identity. If the panel is empty, check whether the forgejo process restarted since [metrics] last changed — a merged config alone does not enable a series, and an absent series never says why it is absent.", + "description": "Every issue, grouped by label.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" }, "gridPos": { "h": 16, "w": 24, "x": 0, "y": 19 }, "targets": [ diff --git a/nix/host-modules/swarm-grafana/dashboards/logstore.json b/nix/host-modules/swarm-grafana/dashboards/logstore.json index 8e92ff70..b6eb6b76 100644 --- a/nix/host-modules/swarm-grafana/dashboards/logstore.json +++ b/nix/host-modules/swarm-grafana/dashboards/logstore.json @@ -1,7 +1,7 @@ { "title": "hyperhive · logs (victorialogs)", "uid": "hyperhive-swarm-logstore", - "description": "The swarm's log pipeline: whether the store is healthy (top) and which sources are actually feeding it (bottom). One of the per-service boards split out of the combined swarm-services page. The two halves answer different questions and are on one board on purpose — aggregate ingestion stays normal on host-tier units while a whole tier ships nothing, so a healthy top half is not evidence about the bottom one.", + "description": "The swarm's log pipeline: store health and log sources.", "editable": true, "refresh": "1m", "schemaVersion": 39, @@ -17,7 +17,7 @@ "id": 1, "type": "timeseries", "title": "Log rows ingested (range)", - "description": "Rows accepted by the log store over the range. ⚠️ This counts what ARRIVED, never what became findable — the pipeline shipped for days with every record carrying a placeholder in place of its message, while this counter climbed normally. A healthy line here is necessary and not sufficient; the check that closes it is a query returning readable lines.", + "description": "Log rows accepted by the store in the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -74,7 +74,7 @@ "id": 2, "type": "timeseries", "title": "Log store size on disk", - "description": "Compressed on-disk size, split by the store's own `type` label (storage vs indexdb). Retention is 30 days, so this is expected to climb to a plateau rather than forever; a straight line past that is the signal.", + "description": "Compressed on-disk size of the log store.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -131,7 +131,7 @@ "id": 3, "type": "stat", "title": "Log store free disk", - "description": "Free space on the volume holding the log store. It shares a filesystem with the metrics store, so the two panels move together — a drop here that the metrics one does not show means something outside this swarm is filling the disk.", + "description": "Free disk space on the log store's volume.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -194,7 +194,7 @@ "id": 4, "type": "stat", "title": "Log store errors", - "description": "Internal errors plus rejected HTTP requests. Healthy value is zero. ⚠️ A wrong OTLP path answers 400 and lands here, and so does a right path with a bad payload — the status code alone does not separate them, only the store's own log does.", + "description": "Internal errors and rejected requests.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -268,7 +268,7 @@ "id": 5, "type": "stat", "title": "Rows in range · all sources (control)", - "description": "Deliberately ungrouped, and it is what makes the two tables below readable. An empty table renders identically whether the query is malformed or the source genuinely never shipped: nonzero here with an empty table means the QUERY is broken; zero here means the store really is empty for this range. Read it against `Log rows ingested` above too — that counter is the same quantity measured from the store's own metrics rather than by querying, so the two disagreeing means rows arrived that a query cannot reach.", + "description": "Total log rows in the selected range, all sources.", "datasource": { "type": "victoriametrics-logs-datasource", "uid": "@logsDatasourceUid@" @@ -330,7 +330,7 @@ "id": 6, "type": "bargauge", "title": "Rows by unit", - "description": "Every `_SYSTEMD_UNIT` present in the range, discovered rather than enumerated — nothing here names a unit, so a source that starts shipping appears on its own. ⚠️ Absence means NOT COLLECTED, which is not the same as not running: the collector's journald receiver only reads the units listed in `services.hyperhive.swarm.otel.journaldUnits`, so a unit missing from that list can be running and logging and still never reach this board.", + "description": "Log rows, split by systemd unit.", "datasource": { "type": "victoriametrics-logs-datasource", "uid": "@logsDatasourceUid@" @@ -382,7 +382,7 @@ "id": 7, "type": "bargauge", "title": "Rows by host", - "description": "The same count grouped by `_HOSTNAME`. Read together with the unit table this separates two very different failures: one host contributing nothing is a collector or journal-linkage problem on that host, while a unit missing across every host is a configuration problem in what gets collected.", + "description": "Log rows, split by host.", "datasource": { "type": "victoriametrics-logs-datasource", "uid": "@logsDatasourceUid@" @@ -434,7 +434,7 @@ "id": 8, "type": "timeseries", "title": "Rows over time, by unit", - "description": "The tables say who shipped over the whole range; this says when. A line that stops is a source that died, and it is the only view here that distinguishes that from one which never existed — a source absent for the entire range looks the same as an unknown one in a table, but shows up here as a line that ends.", + "description": "Log rows over time, split by unit.", "datasource": { "type": "victoriametrics-logs-datasource", "uid": "@logsDatasourceUid@" diff --git a/nix/host-modules/swarm-grafana/dashboards/metricstore.json b/nix/host-modules/swarm-grafana/dashboards/metricstore.json index 3a0cfe6b..397a7f53 100644 --- a/nix/host-modules/swarm-grafana/dashboards/metricstore.json +++ b/nix/host-modules/swarm-grafana/dashboards/metricstore.json @@ -1,7 +1,7 @@ { "title": "hyperhive · metrics store (victoriametrics)", "uid": "hyperhive-swarm-metricstore", - "description": "VictoriaMetrics health for the swarm metrics store. One of the per-service boards split out of the combined swarm-services page.", + "description": "The swarm's metrics store.", "editable": true, "refresh": "1m", "schemaVersion": 39, @@ -17,7 +17,7 @@ "id": 1, "type": "timeseries", "title": "Samples ingested/s", - "description": "vm_rows_inserted_total across all insert paths. This is the whole swarm's telemetry arriving — a drop to zero here means the collector stopped, not that the services went quiet.", + "description": "Metric samples ingested per second.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -74,7 +74,7 @@ "id": 2, "type": "timeseries", "title": "New time series created (range)", - "description": "Cardinality growth over the range. Bursty by nature — a deploy creates a batch and then nothing — so a per-second rate reads zero between bursts and hides the batches entirely. Sustained growth on a swarm whose shape is not changing is the early warning for a label carrying something unbounded, the failure mode that kills a metrics store slowly rather than loudly.", + "description": "New time-series cardinality, over the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -131,7 +131,7 @@ "id": 3, "type": "timeseries", "title": "Store HTTP errors/s", - "description": "vm_http_request_errors_total — the store refusing or failing requests, which covers both a broken collector push and a broken grafana query. Non-zero here while 'Samples ingested/s' looks fine means the read side, not the write side.", + "description": "HTTP errors served by the store, per second.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -189,7 +189,7 @@ "id": 4, "type": "timeseries", "title": "Store size on disk", - "description": "vm_data_size_bytes summed over the storage components. Paired with free disk below rather than shown alone: the absolute size is uninteresting, the ratio is the operational question.", + "description": "Metrics store size on disk.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -246,7 +246,7 @@ "id": 5, "type": "timeseries", "title": "Free disk space", - "description": "vm_free_disk_space_bytes. VictoriaMetrics stops accepting writes when this falls under vm_free_disk_space_limit_bytes, so this is the panel that predicts a silent ingestion stop rather than reporting one.", + "description": "Free disk space available to the store.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" diff --git a/nix/host-modules/swarm-grafana/dashboards/queue.json b/nix/host-modules/swarm-grafana/dashboards/queue.json index a067578d..16e8bf16 100644 --- a/nix/host-modules/swarm-grafana/dashboards/queue.json +++ b/nix/host-modules/swarm-grafana/dashboards/queue.json @@ -1,7 +1,7 @@ { "title": "hyperhive · queue (nats)", "uid": "hyperhive-swarm-queue", - "description": "NATS / JetStream health for the swarm queue. Split out of the combined swarm-services board: a page whose content is really N services should be N pages.", + "description": "The swarm's NATS/JetStream queue.", "editable": true, "refresh": "1m", "schemaVersion": 39, @@ -17,7 +17,7 @@ "id": 1, "type": "stat", "title": "Queue connections", - "description": "Clients currently attached to the swarm's NATS server. Zero is NOT healthy here: hive-c0re's status publisher, swarm-controller and the notices stream all hold a connection, so an empty count means the queue is refusing everyone rather than being idle. A callout denial reaches a client as a timeout, so this panel is often the first place that failure is visible at all. No `or vector(0)`, deliberately: that fallback belongs on counts-of-failures, and here it would render a missing exporter as zero clients — conflating a dead queue with a dead scrape, which the scrape panels above already answer.", + "description": "Clients currently connected to the queue.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -80,7 +80,7 @@ "id": 2, "type": "stat", "title": "Queue subscriptions", - "description": "Active subscriptions across all connected clients. Read alongside the connection count: connections steady while subscriptions collapse means clients are attached but no longer listening, which a liveness check cannot see.", + "description": "Active subscriptions across all clients.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -139,7 +139,7 @@ "id": 3, "type": "stat", "title": "Slow consumers", - "description": "Clients the server has disconnected for failing to keep up. Healthy value is zero, so this panel reads empty when everything is fine — see the queue-connections panel for whether the server is being talked to at all.", + "description": "Clients disconnected for falling behind.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -203,7 +203,7 @@ "id": 4, "type": "stat", "title": "JetStream API errors", - "description": "Errors answering JetStream API requests. The KV bucket the hive status publisher writes to lives behind this API, so a non-zero value here explains a status page that has gone stale without anything else looking wrong.", + "description": "Errors answering JetStream API requests.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -267,7 +267,7 @@ "id": 5, "type": "timeseries", "title": "Queue messages (range)", - "description": "Messages in and out over the selected range. Deliberately `increase()` rather than a per-second rate: this queue carries a status publish per hive per interval and little else, so a rate divides a handful of messages across an hour and draws a flat zero.", + "description": "Messages sent and received in the selected range.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -333,7 +333,7 @@ "id": 6, "type": "stat", "title": "Pending bytes to clients", - "description": "Bytes queued for delivery to clients. Rises BEFORE a slow consumer is counted, so this is the leading indicator for the panel beside it. Zero is the healthy value.", + "description": "Bytes queued for delivery to clients.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@" @@ -391,7 +391,7 @@ "id": 7, "type": "timeseries", "title": "JetStream store size vs limit", - "description": "The only capacity question the queue has. Used and configured limit on one axis so the headroom is legible without arithmetic; a stream that grows unboundedly shows up here first.", + "description": "Store size used against its configured limit.", "datasource": { "type": "prometheus", "uid": "@datasourceUid@"