57dc91585d
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
200 lines
6.6 KiB
JavaScript
200 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @file Verifies the CCAM dashboard and optional Prometheus/Grafana stack are up.
|
|
* Used after `monitoring:up` or `monitoring:docker:up` to confirm scrape health.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
const { GRAFANA_ADMIN_USER, GRAFANA_ADMIN_PASSWORD } = require("./paths");
|
|
|
|
const DASHBOARD_URL = process.env.CCAM_DASHBOARD_URL || "http://127.0.0.1:4820";
|
|
const PROMETHEUS_URL = process.env.CCAM_PROMETHEUS_URL || "http://127.0.0.1:9090";
|
|
const GRAFANA_URL = process.env.CCAM_GRAFANA_URL || "http://127.0.0.1:3000";
|
|
const JSON_MODE = process.argv.includes("--json");
|
|
|
|
async function checkDashboardHealth() {
|
|
try {
|
|
const res = await fetch(`${DASHBOARD_URL}/api/health`);
|
|
if (!res.ok) {
|
|
return { name: "Dashboard /api/health", ok: false, detail: `HTTP ${res.status}` };
|
|
}
|
|
const body = await res.json();
|
|
const detail = body.version ? `v${body.version}` : undefined;
|
|
return { name: "Dashboard /api/health", ok: true, detail, version: body.version };
|
|
} catch (err) {
|
|
return { name: "Dashboard /api/health", ok: false, detail: err.message };
|
|
}
|
|
}
|
|
|
|
async function check(name, url, ok = (res) => res.ok) {
|
|
try {
|
|
const res = await fetch(url);
|
|
if (!ok(res)) {
|
|
return { name, ok: false, detail: `HTTP ${res.status}` };
|
|
}
|
|
return { name, ok: true };
|
|
} catch (err) {
|
|
return { name, ok: false, detail: err.message };
|
|
}
|
|
}
|
|
|
|
async function checkPrometheusTarget(attempts = 12, intervalMs = 2500) {
|
|
for (let i = 0; i < attempts; i += 1) {
|
|
const result = await checkPrometheusTargetOnce();
|
|
if (result.ok) return result;
|
|
if (i < attempts - 1) await new Promise((r) => setTimeout(r, intervalMs));
|
|
if (i === attempts - 1) return result;
|
|
}
|
|
return { name: "Prometheus ccam target", ok: false, detail: "timeout" };
|
|
}
|
|
|
|
async function checkPrometheusTargetOnce() {
|
|
try {
|
|
const res = await fetch(`${PROMETHEUS_URL}/api/v1/targets`);
|
|
if (!res.ok) return { name: "Prometheus ccam target", ok: false, detail: `HTTP ${res.status}` };
|
|
const body = await res.json();
|
|
const target = body?.data?.activeTargets?.find((t) => t.labels?.job === "ccam");
|
|
if (!target) return { name: "Prometheus ccam target", ok: false, detail: "job not found" };
|
|
if (target.health !== "up") {
|
|
return {
|
|
name: "Prometheus ccam target",
|
|
ok: false,
|
|
detail: target.lastError || `health=${target.health}`,
|
|
};
|
|
}
|
|
return { name: "Prometheus ccam target", ok: true, detail: target.scrapeUrl };
|
|
} catch (err) {
|
|
return { name: "Prometheus ccam target", ok: false, detail: err.message };
|
|
}
|
|
}
|
|
|
|
async function checkGrafanaLogin() {
|
|
try {
|
|
const res = await fetch(`${GRAFANA_URL}/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
user: GRAFANA_ADMIN_USER,
|
|
password: GRAFANA_ADMIN_PASSWORD,
|
|
}),
|
|
});
|
|
if (!res.ok) {
|
|
return { name: "Grafana admin login", ok: false, detail: `HTTP ${res.status}` };
|
|
}
|
|
const body = await res.json();
|
|
if (body?.message !== "Logged in") {
|
|
return { name: "Grafana admin login", ok: false, detail: body?.message || "login rejected" };
|
|
}
|
|
return { name: "Grafana admin login", ok: true, detail: grafanaLoginDetail() };
|
|
} catch (err) {
|
|
return { name: "Grafana admin login", ok: false, detail: err.message };
|
|
}
|
|
}
|
|
|
|
function grafanaLoginDetail() {
|
|
return `${GRAFANA_ADMIN_USER} / ${GRAFANA_ADMIN_PASSWORD}`;
|
|
}
|
|
|
|
async function checkPrometheusMetrics() {
|
|
try {
|
|
const queries = [
|
|
{ name: "Prometheus ccam_up", q: "ccam_up" },
|
|
{ name: "Prometheus total sessions", q: "sum(ccam_sessions)" },
|
|
];
|
|
const results = [];
|
|
for (const { name, q } of queries) {
|
|
const res = await fetch(
|
|
`${PROMETHEUS_URL}/api/v1/query?${new URLSearchParams({ query: q })}`
|
|
);
|
|
if (!res.ok) {
|
|
results.push({ name, ok: false, detail: `HTTP ${res.status}` });
|
|
continue;
|
|
}
|
|
const body = await res.json();
|
|
const series = body?.data?.result;
|
|
if (!Array.isArray(series) || series.length === 0) {
|
|
results.push({ name, ok: false, detail: "no series (is CCAM scraping?)" });
|
|
continue;
|
|
}
|
|
const sample = series[0]?.value?.[1];
|
|
results.push({ name, ok: true, detail: `${q} = ${sample}` });
|
|
}
|
|
return results;
|
|
} catch (err) {
|
|
return [{ name: "Prometheus ccam metrics", ok: false, detail: err.message }];
|
|
}
|
|
}
|
|
|
|
async function checkPrometheusConsole() {
|
|
try {
|
|
const res = await fetch(`${PROMETHEUS_URL}/consoles/index.html`);
|
|
if (!res.ok) {
|
|
return { name: "Prometheus CCAM console", ok: false, detail: `HTTP ${res.status}` };
|
|
}
|
|
const html = await res.text();
|
|
if (!html.includes("CCAM")) {
|
|
return { name: "Prometheus CCAM console", ok: false, detail: "unexpected page body" };
|
|
}
|
|
return { name: "Prometheus CCAM console", ok: true, detail: "/consoles/index.html" };
|
|
} catch (err) {
|
|
return { name: "Prometheus CCAM console", ok: false, detail: err.message };
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const checks = [
|
|
await checkDashboardHealth(),
|
|
await check(
|
|
"Dashboard /api/metrics",
|
|
`${DASHBOARD_URL}/api/metrics`,
|
|
(res) => res.ok && res.headers.get("content-type")?.includes("text/plain")
|
|
),
|
|
await check("Prometheus /-/ready", `${PROMETHEUS_URL}/-/ready`),
|
|
await checkPrometheusConsole(),
|
|
await check("Grafana /api/health", `${GRAFANA_URL}/api/health`),
|
|
await checkGrafanaLogin(),
|
|
await checkPrometheusTarget(),
|
|
...(await checkPrometheusMetrics()),
|
|
];
|
|
|
|
let failed = 0;
|
|
for (const c of checks) {
|
|
if (c.ok) {
|
|
if (!JSON_MODE) {
|
|
console.log(`✔ ${c.name}${c.detail ? ` (${c.detail})` : ""}`);
|
|
}
|
|
} else {
|
|
failed += 1;
|
|
if (!JSON_MODE) {
|
|
console.error(`✖ ${c.name}: ${c.detail || "failed"}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (JSON_MODE) {
|
|
const payload = {
|
|
ok: failed === 0,
|
|
checks,
|
|
urls: {
|
|
dashboard: DASHBOARD_URL,
|
|
prometheus: PROMETHEUS_URL,
|
|
grafana: GRAFANA_URL,
|
|
metrics: `${DASHBOARD_URL}/api/metrics`,
|
|
},
|
|
};
|
|
console.log(JSON.stringify(payload, null, 2));
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
if (failed > 0) {
|
|
console.error(`\n${failed} check(s) failed.`);
|
|
process.exit(1);
|
|
}
|
|
console.log("\nMonitoring stack OK.");
|
|
console.log(` Grafana: ${GRAFANA_URL} (${grafanaLoginDetail()})`);
|
|
console.log(` Console: ${PROMETHEUS_URL}/consoles/index.html`);
|
|
console.log(` Graph: ${PROMETHEUS_URL}/graph`);
|
|
console.log(` Metrics: ${DASHBOARD_URL}/api/metrics`);
|
|
}
|
|
|
|
main();
|