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
11 changes: 11 additions & 0 deletions grafana/dashboards/resource-hub.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
]
}
59 changes: 59 additions & 0 deletions test/unit/selfhost-grafana-resource-hub-dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading