feat: Claude Code Monitor — lanes, pipelines and a merged workspace
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.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user