diff --git a/grafana/dashboards/resource-hub.json b/grafana/dashboards/resource-hub.json index 0f5c3fa99b..4963956ac6 100644 --- a/grafana/dashboards/resource-hub.json +++ b/grafana/dashboards/resource-hub.json @@ -68,6 +68,17 @@ "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://gittensory.aethereal.dev/docs/maintainer-self-hosting) โ€” setup, configuration, AI, REES, RAG, operations, and troubleshooting." } + }, + + { + "id": 3, + "type": "text", + "title": "AMS โ€” Autonomous Miner System", + "gridPos": { "h": 4, "w": 24, "x": 0, "y": 13 }, + "options": { + "mode": "markdown", + "content": "## ๐Ÿค– AMS โ€” Autonomous Miner System\n- **[Observing your miner](https://github.com/JSONbored/gittensory/blob/main/packages/gittensory-miner/docs/observability.md)** โ€” AMS is a separate, local `gittensory-miner` CLI a dual-role operator may run alongside ORB on the same box. This guide is the AMS observability entry point: it covers pointing Grafana at the redacted AMS ledger datasources and loading its Grafana dashboard, separate from the ORB review-service observability above." + } } ] } diff --git a/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts b/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts new file mode 100644 index 0000000000..f980e43a82 --- /dev/null +++ b/test/unit/selfhost-grafana-resource-hub-dashboard.test.ts @@ -0,0 +1,59 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +type TextPanel = { + id?: number; + type?: string; + title?: string; + gridPos?: { h: number; w: number; x: number; y: number }; + options?: { mode?: string; content?: string }; +}; +type Dashboard = { uid: string; title: string; panels: TextPanel[] }; + +const dashboardPath = join(process.cwd(), "grafana/dashboards/resource-hub.json"); +const readDashboard = (): Dashboard => JSON.parse(readFileSync(dashboardPath, "utf8")) as Dashboard; + +// The AMS cross-link (#5189) points the hub at the "Observing your miner" guide โ€” the AMS observability entry +// point (mirrors the just-merged #5191 callout target). Kept as an in-repo path so the link-target-exists +// invariant below can prove it is not a dead link. +const AMS_GUIDE_REPO_PATH = "packages/gittensory-miner/docs/observability.md"; +const AMS_GUIDE_URL = `https://github.com/JSONbored/gittensory/blob/main/${AMS_GUIDE_REPO_PATH}`; + +function amsPanel(dashboard = readDashboard()): TextPanel | undefined { + return dashboard.panels.find((panel) => /AMS/i.test(panel.title ?? "")); +} + +describe("LoopOver โ€” Resource hub: AMS cross-link (#5189)", () => { + it("keeps the hub's own identity and its existing two panels untouched (additive change only)", () => { + const dashboard = readDashboard(); + expect(dashboard.uid).toBe("loopover-hub"); + expect(dashboard.title).toBe("LoopOver โ€” Resource hub"); + // The two original panels (Integrated services, Observability & dashboards) still exist unchanged. + expect(dashboard.panels.some((p) => p.title === "Integrated services")).toBe(true); + expect(dashboard.panels.some((p) => p.title === "Observability & dashboards")).toBe(true); + }); + + it("adds exactly one AMS panel, as a markdown text panel matching the hub's existing panel style", () => { + const panel = amsPanel(); + expect(panel, "an AMS panel must exist").toBeDefined(); + expect(panel?.type).toBe("text"); + expect(panel?.options?.mode).toBe("markdown"); + expect(panel?.gridPos, "must have grid sizing like the other panels").toBeDefined(); + // It notes AMS is a separate CLI a dual-role operator runs alongside ORB (context, not a feature spec). + expect(panel?.options?.content).toMatch(/separate/i); + expect(panel?.options?.content).toMatch(/gittensory-miner/); + }); + + it("gives every panel a unique id (a duplicate id silently breaks Grafana panel rendering)", () => { + const ids = readDashboard().panels.map((p) => p.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + it("INVARIANT: the AMS link target is a well-formed reference to a file that actually exists โ€” never a dead link", () => { + const content = amsPanel()?.options?.content ?? ""; + expect(content).toContain(AMS_GUIDE_URL); + // The GitHub-blob URL resolves to a real in-repo file: prove it exists so a typo'd path can't ship. + expect(existsSync(join(process.cwd(), AMS_GUIDE_REPO_PATH))).toBe(true); + }); +});