From 78576180dd5dd00a235c035df713022bd6742b62 Mon Sep 17 00:00:00 2001 From: luciferlive112116 <291889058+luciferlive112116@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:44:20 +0800 Subject: [PATCH] fix(observability): link the Miner usage (AMS) dashboard from the resource hub resource-hub.json is Grafana's curated landing page for the self-host stack: its "Observability & dashboards" panel deep-links every provisioned dashboard by uid. It listed 9 of the 10 shipped in grafana/dashboards/. miner-usage.json (uid loopover-miner-usage, "LoopOver -- Miner usage (AMS)") was the sole exception, reachable only by someone who already knew its uid. The hub's AMS panel links the "Observing your miner" guide -- how to wire the AMS datasources up -- but never the dashboard itself once that wiring is done, so the dashboard shipped effectively orphaned despite being provisioned exactly like the other nine. Add the missing entry in the same style as its siblings, framed as the miner-side counterpart to the ORB review-service dashboards above it. The regression test asserts the invariant rather than this one uid: it reads every grafana/dashboards/*.json uid and requires a /d/ link somewhere in the hub's panel content, so the next dashboard that forgets to cross-link fails CI instead of silently shipping orphaned. Verified to fail against the pre-fix resource-hub.json. Closes #5820 --- grafana/dashboards/resource-hub.json | 2 +- ...ost-grafana-resource-hub-dashboard.test.ts | 41 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/grafana/dashboards/resource-hub.json b/grafana/dashboards/resource-hub.json index c66f82aaf1..01d4ecc5a4 100644 --- a/grafana/dashboards/resource-hub.json +++ b/grafana/dashboards/resource-hub.json @@ -66,7 +66,7 @@ "gridPos": { "h": 13, "w": 12, "x": 12, "y": 0 }, "options": { "mode": "markdown", - "content": "## ๐Ÿ“Š Dashboards\n- **[Upstream PRs & issues (GitHub)](/d/loopover-github)** โ€” live, accurate census + open-PR triage (GitHub API).\n- **[Reviews & PRs (maintainer)](/d/loopover-maintainer)** โ€” loopover's own review activity + reviewed-PR log.\n- **[AI usage](/d/loopover-ai-usage)** โ€” durable cross-provider ai_usage_events (filterable by provider/feature/model), live Prometheus counters, and Claude Code's own OTEL session telemetry, all in one place.\n- **[LoopOver (infra)](/d/loopover-selfhost)** โ€” queue, jobs, HTTP, GitHub API cache/rate limits.\n- **[GPU metrics](/d/loopover-gpu)** โ€” utilization/VRAM for a self-hosted Ollama GPU box.\n- **[Infra health](/d/loopover-infra-health)** โ€” host CPU/mem/disk/network (node-exporter), per-container resource usage (cAdvisor), Redis, Qdrant, and whether the observability stack itself is up.\n- **[REES (review-enrichment)](/d/loopover-rees)** โ€” request outcomes/latency and per-analyzer run/timeout/degrade rates for the optional `--profile rees` service.\n- **[Browserless (visual review)](/d/loopover-browserless)** โ€” queue depth, concurrency, and rejection/error/timeout rate for the optional `--profile visual-review` screenshot service.\n- **[Sentry issues](/d/loopover-sentry)** โ€” recent unresolved issues, top issues by event count, and error-volume trend, queried live from Sentry (`scripts/setup-sentry-datasource.sh`). The plain link below still opens Sentry itself for actions this read-only view can't do (resolving/assigning issues).\n\n## ๐Ÿ“ˆ Metrics & logs\n- **Prometheus** โ€” [targets](http://localhost:9090/targets) ยท [graph](http://localhost:9090)\n- **Alertmanager** โ€” [alerts](http://localhost:9093)\n- **Loki** โ€” query in [Explore](/explore) (pick the *Loki* datasource), e.g. `{compose_service=\"loopover\"}`\n- **Sentry** โ€” release/source-map enriched errors. Edit the dashboard link if your project URL differs.\n\n## ๐Ÿฉบ Quick health checks\n| What | Where |\n|---|---|\n| App serving | `GET /ready` โ†’ 200 |\n| AI wired | boot log `selfhost_ai_provider` |\n| Embeds wired | boot log `selfhost_embed_provider` |\n| Vectors wired | boot log `selfhost_vectorize` |\n| Token spend | **[AI usage](/d/loopover-ai-usage)** dashboard |\n\n## ๐Ÿ“š Docs\n- [Maintainer self-hosting](https://loopover.ai/docs/maintainer-self-hosting) โ€” setup, configuration, AI, REES, RAG, operations, and troubleshooting." + "content": "## ๐Ÿ“Š Dashboards\n- **[Upstream PRs & issues (GitHub)](/d/loopover-github)** โ€” live, accurate census + open-PR triage (GitHub API).\n- **[Reviews & PRs (maintainer)](/d/loopover-maintainer)** โ€” loopover's own review activity + reviewed-PR log.\n- **[AI usage](/d/loopover-ai-usage)** โ€” durable cross-provider ai_usage_events (filterable by provider/feature/model), live Prometheus counters, and Claude Code's own OTEL session telemetry, all in one place.\n- **[LoopOver (infra)](/d/loopover-selfhost)** โ€” queue, jobs, HTTP, GitHub API cache/rate limits.\n- **[GPU metrics](/d/loopover-gpu)** โ€” utilization/VRAM for a self-hosted Ollama GPU box.\n- **[Infra health](/d/loopover-infra-health)** โ€” host CPU/mem/disk/network (node-exporter), per-container resource usage (cAdvisor), Redis, Qdrant, and whether the observability stack itself is up.\n- **[REES (review-enrichment)](/d/loopover-rees)** โ€” request outcomes/latency and per-analyzer run/timeout/degrade rates for the optional `--profile rees` service.\n- **[Browserless (visual review)](/d/loopover-browserless)** โ€” queue depth, concurrency, and rejection/error/timeout rate for the optional `--profile visual-review` screenshot service.\n- **[Sentry issues](/d/loopover-sentry)** โ€” recent unresolved issues, top issues by event count, and error-volume trend, queried live from Sentry (`scripts/setup-sentry-datasource.sh`). The plain link below still opens Sentry itself for actions this read-only view can't do (resolving/assigning issues).\n- **[Miner usage (AMS)](/d/loopover-miner-usage)** โ€” attempt outcomes, cost/token spend, and per-provider usage from the local `loopover-miner` AMS ledger โ€” the miner-side counterpart to the ORB review-service dashboards above (see the AMS panel below for wiring its datasources up).\n\n## ๐Ÿ“ˆ Metrics & logs\n- **Prometheus** โ€” [targets](http://localhost:9090/targets) ยท [graph](http://localhost:9090)\n- **Alertmanager** โ€” [alerts](http://localhost:9093)\n- **Loki** โ€” query in [Explore](/explore) (pick the *Loki* datasource), e.g. `{compose_service=\"loopover\"}`\n- **Sentry** โ€” release/source-map enriched errors. Edit the dashboard link if your project URL differs.\n\n## ๐Ÿฉบ Quick health checks\n| What | Where |\n|---|---|\n| App serving | `GET /ready` โ†’ 200 |\n| AI wired | boot log `selfhost_ai_provider` |\n| Embeds wired | boot log `selfhost_embed_provider` |\n| Vectors wired | boot log `selfhost_vectorize` |\n| Token spend | **[AI usage](/d/loopover-ai-usage)** dashboard |\n\n## ๐Ÿ“š Docs\n- [Maintainer self-hosting](https://loopover.ai/docs/maintainer-self-hosting) โ€” setup, configuration, AI, REES, RAG, operations, and troubleshooting." } }, diff --git a/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts b/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts index 3d6f5a8e52..768dcb9b14 100644 --- a/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts +++ b/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; @@ -57,3 +57,42 @@ describe("LoopOver โ€” Resource hub: AMS cross-link (#5189)", () => { expect(existsSync(join(process.cwd(), AMS_GUIDE_REPO_PATH))).toBe(true); }); }); + +// #5820: the hub is the curated landing page for every provisioned dashboard, but its list is hand-maintained, +// so miner-usage.json (uid loopover-miner-usage) shipped orphaned โ€” provisioned exactly like the other nine, yet +// reachable from the hub only by someone who already knew its uid. The AMS panel above links the *guide* for +// wiring AMS datasources up, never the dashboard itself once that wiring is done. Pinning that one uid would +// only fix today's miss, so this asserts the invariant over every provisioned dashboard: the NEXT one that +// forgets to cross-link fails CI instead of silently shipping orphaned. +const DASHBOARDS_DIR = join(process.cwd(), "grafana/dashboards"); +const HUB_UID = "loopover-hub"; + +function provisionedDashboards(): { file: string; uid: string }[] { + return readdirSync(DASHBOARDS_DIR) + .filter((file) => file.endsWith(".json")) + .map((file) => ({ file, uid: (JSON.parse(readFileSync(join(DASHBOARDS_DIR, file), "utf8")) as Dashboard).uid })); +} + +/** Every panel's markdown, joined โ€” the hub links dashboards from more than one panel. */ +function hubContent(): string { + return readDashboard() + .panels.map((panel) => panel.options?.content ?? "") + .join("\n"); +} + +describe("LoopOver โ€” Resource hub: every provisioned dashboard is linked (#5820)", () => { + it("deep-links the Miner usage (AMS) dashboard, not just its wiring guide", () => { + expect(hubContent()).toContain("/d/loopover-miner-usage"); + }); + + it("INVARIANT: every provisioned dashboard uid except the hub's own is deep-linked from the hub", () => { + const dashboards = provisionedDashboards(); + // Guard the guard: if the directory read ever matched nothing, the orphan check below would vacuously pass. + expect(dashboards.length).toBeGreaterThan(1); + expect(dashboards.every((d) => typeof d.uid === "string" && d.uid.length > 0)).toBe(true); + + const content = hubContent(); + const orphaned = dashboards.filter((d) => d.uid !== HUB_UID && !content.includes(`/d/${d.uid}`)); + expect(orphaned.map((d) => d.file)).toEqual([]); + }); +});