Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
11 changed files with 94 additions and 203 deletions

View file

@ -57,17 +57,6 @@ jobs:
- name: lint
run: sh scripts/check-attribution-trailers.sh
dashboard-descriptions:
name: dashboard-description lint
runs-on: [hive-ci]
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
- name: lint
# jq isn't a project dependency elsewhere in this repo — same
# nix-shell-for-one-tool shape the vale/shellcheck jobs above use.
run: nix shell nixpkgs#jq --command sh scripts/check-dashboard-descriptions.sh
shellcheck:
name: shellcheck
runs-on: [hive-ci]

View file

@ -6,7 +6,7 @@
},
"spec": {
"title": "hyperhive · agents",
"description": "Per-agent resource usage and Claude Code activity.",
"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.",
"editable": true,
"cursorSync": "Off",
"liveNow": false,
@ -129,7 +129,7 @@
"spec": {
"id": 1,
"title": "Agents reporting",
"description": "Distinct agents with container metrics in the selected range.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -196,7 +196,7 @@
"spec": {
"id": 10,
"title": "Cost rate by agent",
"description": "USD per hour spent by each 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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -272,7 +272,7 @@
"spec": {
"id": 11,
"title": "Container CPU by agent",
"description": "Percent of one CPU core used by each agent's container.",
"description": "hyperhive.container.cpu.percent, sampled by hive-c0re. Percent of one core.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -346,7 +346,7 @@
"spec": {
"id": 2,
"title": "Cost",
"description": "Total USD spent on Claude Code usage.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -485,7 +485,7 @@
"spec": {
"id": 21,
"title": "Tokens by model (excl. cache reads)",
"description": "Tokens consumed per model, excluding 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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -561,7 +561,7 @@
"spec": {
"id": 3,
"title": "Tokens (excl. cache reads)",
"description": "Total tokens consumed, excluding 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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -627,7 +627,7 @@
"spec": {
"id": 30,
"title": "Container storage by agent",
"description": "Disk usage per agent's container.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -702,7 +702,7 @@
"spec": {
"id": 31,
"title": "Lines of code changed",
"description": "Lines of code added and removed by Claude Code.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -778,7 +778,7 @@
"spec": {
"id": 32,
"title": "Active time by agent",
"description": "Seconds each agent spent actively working.",
"description": "claude_code.active_time.total — seconds an agent was actually working, not wall-clock uptime.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -854,7 +854,7 @@
"spec": {
"id": 33,
"title": "CLAUDE.md size by agent",
"description": "Lines in each agent's CLAUDE.md file over time.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -928,7 +928,7 @@
"spec": {
"id": 4,
"title": "CLI sessions started",
"description": "Claude CLI turns started, one per session count.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -994,7 +994,7 @@
"spec": {
"id": 40,
"title": "Subagent cost",
"description": "USD spent by subagent turns.",
"description": "sum(increase(claude_code.cost.usage{subagent!=\"\"})). `hive-subagent-mcp` stamps a `subagent=<name>` resource attribute (alongside the parent's own `agent=<name>`) on every claude_code.* sample a spawned subagent turn emits — this label already existed, it just wasn't plotted anywhere.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -1060,7 +1060,7 @@
"spec": {
"id": 41,
"title": "Subagent turns",
"description": "Number of subagent turns run.",
"description": "claude_code.session.count where subagent!=\"\" — a proxy for subagent turns, same PROXY caveat as the plain 'CLI sessions started' tile above.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -1126,7 +1126,7 @@
"spec": {
"id": 42,
"title": "Subagent cost share",
"description": "Subagent cost as a fraction of total cost.",
"description": "Subagent cost as a fraction of all cost in the selected range — 'basically free' is a claim this tile can actually check.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -1192,7 +1192,7 @@
"spec": {
"id": 43,
"title": "Cost by subagent name",
"description": "USD spent, split 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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -1268,7 +1268,7 @@
"spec": {
"id": 44,
"title": "Subagent cost by parent agent",
"description": "USD spent on subagents, split 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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -1344,7 +1344,7 @@
"spec": {
"id": 45,
"title": "Distinct subagents (range)",
"description": "Number of differently-named subagents that ran.",
"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.",
"links": [],
"data": {
"kind": "QueryGroup",
@ -1410,7 +1410,7 @@
"spec": {
"id": 46,
"title": "Distinct subagents by agent, over time",
"description": "Distinct subagent names active per 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.",
"links": [],
"data": {
"kind": "QueryGroup",

View file

@ -1,7 +1,7 @@
{
"title": "hyperhive · authelia",
"uid": "hyperhive-authelia",
"description": "The swarm's identity provider.",
"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.",
"editable": true,
"refresh": "1m",
"schemaVersion": 39,
@ -20,7 +20,7 @@
"id": 1,
"type": "stat",
"title": "Uptime",
"description": "Time since the authelia process started.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -71,7 +71,7 @@
"id": 2,
"type": "stat",
"title": "Authentications (range)",
"description": "Login attempts in the selected 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -122,7 +122,7 @@
"id": 3,
"type": "stat",
"title": "Failed authentications (range)",
"description": "Failed login attempts in the selected 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -186,7 +186,7 @@
"id": 4,
"type": "stat",
"title": "Authorization decisions (range)",
"description": "Forward-auth verdicts issued in the selected 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -237,7 +237,7 @@
"id": 10,
"type": "bargauge",
"title": "Requests by status code (range)",
"description": "HTTP requests grouped by status code.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -285,7 +285,7 @@
"id": 11,
"type": "bargauge",
"title": "Authorization verdicts by code (range)",
"description": "Forward-auth outcomes grouped by status code.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -333,7 +333,7 @@
"id": 20,
"type": "timeseries",
"title": "Request latency (p95)",
"description": "95th-percentile request latency.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -390,7 +390,7 @@
"id": 21,
"type": "timeseries",
"title": "OIDC request latency (p95)",
"description": "95th-percentile latency for OIDC endpoints.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -447,7 +447,7 @@
"id": 30,
"type": "timeseries",
"title": "Resident memory",
"description": "Memory used by the authelia process.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -504,7 +504,7 @@
"id": 31,
"type": "timeseries",
"title": "Goroutines",
"description": "Number of active Go routines.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -561,7 +561,7 @@
"id": 32,
"type": "timeseries",
"title": "Open file descriptors",
"description": "Open file descriptors held by the process.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"

View file

@ -1,7 +1,7 @@
{
"title": "hyperhive · openbao",
"uid": "hyperhive-bao",
"description": "The swarm's secret store.",
"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.",
"editable": true,
"refresh": "1m",
"schemaVersion": 39,
@ -20,7 +20,7 @@
"id": 1,
"type": "stat",
"title": "Seal state",
"description": "Whether the store is sealed or unsealed.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -99,7 +99,7 @@
"id": 2,
"type": "stat",
"title": "Uptime",
"description": "Time since the openbao process started.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -150,7 +150,7 @@
"id": 3,
"type": "stat",
"title": "Goroutines",
"description": "Number of active Go routines.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -201,7 +201,7 @@
"id": 4,
"type": "stat",
"title": "Requests (range)",
"description": "API requests served in the selected 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -252,7 +252,7 @@
"id": 10,
"type": "stat",
"title": "Seal operations (range)",
"description": "Encrypt/decrypt calls against the seal wrapper.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -312,7 +312,7 @@
"id": 11,
"type": "stat",
"title": "Raft peers",
"description": "Voting members of the raft cluster.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -363,7 +363,7 @@
"id": 20,
"type": "timeseries",
"title": "Request latency",
"description": "Request latency quantiles.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -429,7 +429,7 @@
"id": 21,
"type": "timeseries",
"title": "Leases",
"description": "Outstanding and irrevocable 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -495,7 +495,7 @@
"id": 30,
"type": "timeseries",
"title": "Heap in use",
"description": "Go heap memory 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"

View file

@ -1,7 +1,7 @@
{
"uid": "hyperhive-claude-usage",
"title": "hyperhive · claude usage",
"description": "How Claude is used, by model, effort, token type, and source.",
"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.",
"tags": ["hyperhive", "claude"],
"timezone": "utc",
"schemaVersion": 39,
@ -59,7 +59,7 @@
"id": 1,
"type": "stat",
"title": "Cost",
"description": "Total USD spent on Claude Code usage.",
"description": "claude_code.cost.usage, summed over the selected range.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -107,7 +107,7 @@
"id": 2,
"type": "stat",
"title": "Tokens",
"description": "Total tokens used, across all types.",
"description": "claude_code.token.usage across every type (input, output, cacheRead, cacheCreation).",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -155,7 +155,7 @@
"id": 3,
"type": "stat",
"title": "Cache read share",
"description": "Cache-read tokens as a fraction of all tokens.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -203,7 +203,7 @@
"id": 4,
"type": "stat",
"title": "Active time",
"description": "Seconds actually spent working.",
"description": "claude_code.active_time.total — seconds actually spent working, not wall-clock uptime.",
"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.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -313,7 +313,7 @@
"id": 11,
"type": "bargauge",
"title": "Tokens by model",
"description": "Tokens used, split by model.",
"description": "Which model the tokens went to, over the selected range.",
"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.",
"description": "USD per minute, split by model. Same 10m minimum step as the token panel.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -430,7 +430,7 @@
"id": 21,
"type": "bargauge",
"title": "Cost by effort",
"description": "USD spent, split by effort level.",
"description": "claude_code.cost.usage split by the effort label (high / medium / xhigh).",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -489,7 +489,7 @@
"id": 22,
"type": "bargauge",
"title": "Cost by query source",
"description": "USD spent, split by main vs. auxiliary queries.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -548,7 +548,7 @@
"id": 30,
"type": "bargauge",
"title": "Edit tool decisions by language",
"description": "Edit-tool accept/reject decisions, split 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -607,7 +607,7 @@
"id": 31,
"type": "bargauge",
"title": "Edit tool decisions by tool",
"description": "Edit-tool accept/reject decisions, split by tool.",
"description": "Same series split by tool_name (Edit / Write) instead of language.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -666,7 +666,7 @@
"id": 5,
"type": "stat",
"title": "Turns",
"description": "Number of Claude turns run.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -714,7 +714,7 @@
"id": 6,
"type": "stat",
"title": "Active time per turn",
"description": "Average active seconds 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -762,7 +762,7 @@
"id": 40,
"type": "timeseries",
"title": "Turns per hour by agent",
"description": "Turns per hour, per 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -820,7 +820,7 @@
"id": 41,
"type": "bargauge",
"title": "Active time per turn by agent",
"description": "Average active seconds per turn, per 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -879,7 +879,7 @@
"id": 42,
"type": "bargauge",
"title": "Cost by agent",
"description": "USD spent, split 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -938,7 +938,7 @@
"id": 43,
"type": "bargauge",
"title": "Tokens by agent",
"description": "Tokens used, split by agent.",
"description": "claude_code.token.usage summed over the selected range, broken out by agent (all token types combined).",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"

View file

@ -1,7 +1,7 @@
{
"title": "hyperhive · forge (content)",
"uid": "hyperhive-swarm-forge",
"description": "An inventory of what the forge holds.",
"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.",
"editable": true,
"refresh": "5m",
"schemaVersion": 39,
@ -15,7 +15,7 @@
"id": 1,
"type": "stat",
"title": "Repositories",
"description": "Total repositories on the forge.",
"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.",
"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": "Total forge accounts.",
"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.",
"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": "Total forge 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.",
"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": "Currently open issues, across every repo.",
"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.",
"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, across every repo.",
"description": "Cumulative closed issues. Only ever rises, so read it as throughput-to-date rather than as a current state.",
"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": "Cumulative tagged releases, across every repo.",
"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.",
"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": "Open and closed issue counts, over time.",
"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.",
"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": "Repository and user counts, over time.",
"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.",
"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, over time.",
"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.",
"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.",
"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.",
"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": "Every issue, grouped by label.",
"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.",
"datasource": { "type": "prometheus", "uid": "@datasourceUid@" },
"gridPos": { "h": 16, "w": 24, "x": 0, "y": 19 },
"targets": [

View file

@ -1,7 +1,7 @@
{
"title": "hyperhive · logs (victorialogs)",
"uid": "hyperhive-swarm-logstore",
"description": "The swarm's log pipeline: store health and log sources.",
"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.",
"editable": true,
"refresh": "1m",
"schemaVersion": 39,
@ -17,7 +17,7 @@
"id": 1,
"type": "timeseries",
"title": "Log rows ingested (range)",
"description": "Log rows accepted by the store in the selected 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -74,7 +74,7 @@
"id": 2,
"type": "timeseries",
"title": "Log store size on disk",
"description": "Compressed on-disk size of the log store.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -131,7 +131,7 @@
"id": 3,
"type": "stat",
"title": "Log store free disk",
"description": "Free disk space on the log store's volume.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -194,7 +194,7 @@
"id": 4,
"type": "stat",
"title": "Log store errors",
"description": "Internal errors and rejected requests.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -268,7 +268,7 @@
"id": 5,
"type": "stat",
"title": "Rows in range · all sources (control)",
"description": "Total log rows in the selected range, all sources.",
"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.",
"datasource": {
"type": "victoriametrics-logs-datasource",
"uid": "@logsDatasourceUid@"
@ -330,7 +330,7 @@
"id": 6,
"type": "bargauge",
"title": "Rows by unit",
"description": "Log rows, split by systemd 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.",
"datasource": {
"type": "victoriametrics-logs-datasource",
"uid": "@logsDatasourceUid@"
@ -382,7 +382,7 @@
"id": 7,
"type": "bargauge",
"title": "Rows by host",
"description": "Log rows, split 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.",
"datasource": {
"type": "victoriametrics-logs-datasource",
"uid": "@logsDatasourceUid@"
@ -434,7 +434,7 @@
"id": 8,
"type": "timeseries",
"title": "Rows over time, by unit",
"description": "Log rows over time, split 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.",
"datasource": {
"type": "victoriametrics-logs-datasource",
"uid": "@logsDatasourceUid@"

View file

@ -1,7 +1,7 @@
{
"title": "hyperhive · metrics store (victoriametrics)",
"uid": "hyperhive-swarm-metricstore",
"description": "The swarm's metrics store.",
"description": "VictoriaMetrics health for the swarm metrics store. One of the per-service boards split out of the combined swarm-services page.",
"editable": true,
"refresh": "1m",
"schemaVersion": 39,
@ -17,7 +17,7 @@
"id": 1,
"type": "timeseries",
"title": "Samples ingested/s",
"description": "Metric samples ingested per second.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -74,7 +74,7 @@
"id": 2,
"type": "timeseries",
"title": "New time series created (range)",
"description": "New time-series cardinality, over the selected 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -131,7 +131,7 @@
"id": 3,
"type": "timeseries",
"title": "Store HTTP errors/s",
"description": "HTTP errors served by the store, per second.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -189,7 +189,7 @@
"id": 4,
"type": "timeseries",
"title": "Store size on disk",
"description": "Metrics 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -246,7 +246,7 @@
"id": 5,
"type": "timeseries",
"title": "Free disk space",
"description": "Free disk space available to the store.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"

View file

@ -1,7 +1,7 @@
{
"title": "hyperhive · queue (nats)",
"uid": "hyperhive-swarm-queue",
"description": "The swarm's NATS/JetStream 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.",
"editable": true,
"refresh": "1m",
"schemaVersion": 39,
@ -17,7 +17,7 @@
"id": 1,
"type": "stat",
"title": "Queue connections",
"description": "Clients currently connected to the queue.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -80,7 +80,7 @@
"id": 2,
"type": "stat",
"title": "Queue subscriptions",
"description": "Active subscriptions across all clients.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -139,7 +139,7 @@
"id": 3,
"type": "stat",
"title": "Slow consumers",
"description": "Clients disconnected for falling behind.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -203,7 +203,7 @@
"id": 4,
"type": "stat",
"title": "JetStream API errors",
"description": "Errors answering JetStream API requests.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -267,7 +267,7 @@
"id": 5,
"type": "timeseries",
"title": "Queue messages (range)",
"description": "Messages sent and received in the selected 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.",
"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.",
"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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"
@ -391,7 +391,7 @@
"id": 7,
"type": "timeseries",
"title": "JetStream store size vs limit",
"description": "Store size used against its configured 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.",
"datasource": {
"type": "prometheus",
"uid": "@datasourceUid@"

View file

@ -1,90 +0,0 @@
#!/bin/sh
# CI lint: keeps grafana dashboard panel/board descriptions short and free of
# meta-commentary — a tooltip answers "what is this number", not "how did we
# arrive at this panel" or "what did someone say about it".
#
# Two checks: length (over MAX_LEN means it became prose, not a short
# answer — fix by deleting the editorializing, not trimming words), and
# keywords (the operator's name, hardcoded rather than read from config —
# the class of mistake is what's caught, not one specific name; and a
# tracker tag `#1234`, reusing check-issue-refs.sh's own pattern and its
# same known false-positive class, e.g. `PKCS#11` — no `lint:allow` escape
# hatch here though, since JSON has no comment syntax to carry the marker;
# rephrase to drop the `#digits` shape instead).
#
# Deliberately NOT a blanket ban on the word "issue": forge/gitea panels
# legitimately show issue-tracker counts ("Currently open issues, across
# every repo." is exactly the short description this check wants to keep).
# The tracker-tag pattern is the part machine-checkable; a prose
# meta-reference with no `#N` needs a human reviewer, same as elsewhere.
#
# Uses `jq` — the right tool for JSON, unlike its zero-dependency siblings
# in this directory. This script does NOT nix-shell for it itself: the
# caller declares that dependency (see `pre-push` and
# `.forgejo/workflows/ci.yml`, the same `nix shell nixpkgs#jq --command sh
# <this file>` shape the vale/shellcheck jobs already use for their own
# tools), so a leaf script never hides a nix invocation inside itself.
#
# Emits a CI error annotation per hit, exits 1 if any hit is found.
set -eu
dashboards_dir="nix/host-modules/swarm-grafana/dashboards"
max_len=100
tracker_pattern='#[0-9]{2,5}([^0-9a-zA-Z]|$)'
files="$(find "$dashboards_dir" -maxdepth 1 -name '*.json' 2>/dev/null | sort)"
if [ -z "$files" ]; then
echo 'check-dashboard-descriptions: no dashboard json files found — path moved?' >&2
exit 1
fi
violations=0
checked=0
for f in $files; do
# One JSON object per description hit: {"title": "...", "description": "..."}
# -- recursive `..` walk catches both schema shapes uniformly, since in
# both a description always sits beside a sibling title in the same object.
while IFS= read -r row; do
[ -n "$row" ] || continue
title="$(printf '%s' "$row" | jq -r '.title')"
desc="$(printf '%s' "$row" | jq -r '.description')"
[ -n "$desc" ] || continue
checked=$((checked + 1))
len="$(printf '%s' "$desc" | wc -m | tr -d ' ')"
if [ "$len" -gt "$max_len" ]; then
violations=$((violations + 1))
printf '::error file=%s::panel "%s" description is %s chars (max %s) — shorten it to what the number is, drop the reasoning\n' \
"$f" "$title" "$len" "$max_len"
fi
if printf '%s' "$desc" | grep -qiE 'mara'; then
violations=$((violations + 1))
printf '::error file=%s::panel "%s" description names the operator — describe the metric, not who asked for the panel\n' \
"$f" "$title"
fi
if printf '%s' "$desc" | grep -qE "$tracker_pattern"; then
violations=$((violations + 1))
printf '::error file=%s::panel "%s" description references a tracker issue — write prose that stands on its own\n' \
"$f" "$title"
fi
done <<EOF
$(jq -c '[.. | objects | select(has("title") and has("description"))] | .[]' "$f")
EOF
done
if [ "$checked" -eq 0 ]; then
echo 'check-dashboard-descriptions: found zero title+description pairs — extractor or tree changed' >&2
exit 1
fi
if [ "$violations" -gt 0 ]; then
printf 'check-dashboard-descriptions: %s violation(s) across %s description(s) checked\n' \
"$violations" "$checked" >&2
exit 1
fi
echo "check-dashboard-descriptions: $checked description(s) checked, clean" >&2
exit 0

View file

@ -31,13 +31,5 @@ if ! sh "$repo_root/scripts/check-attribution-trailers.sh"; then
exit 1
fi
echo "pre-push: running dashboard-description lint..." >&2
# jq isn't a project dependency elsewhere in this repo, so the check script
# assumes nothing about the caller's PATH — nix-shell it here instead.
if ! nix shell nixpkgs#jq --command sh "$repo_root/scripts/check-dashboard-descriptions.sh"; then
echo "pre-push: dashboard-description lint FAILED — fix before pushing" >&2
exit 1
fi
echo "pre-push: lints passed ✓" >&2
exit 0