Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion grafana/dashboards/resource-hub.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
},

Expand Down
41 changes: 40 additions & 1 deletion test/unit/selfhost-grafana-resource-hub-dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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([]);
});
});
Loading