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:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* @file Downloads official Prometheus + Grafana OSS binaries into monitoring/.bin/.
* Supports macOS (arm64/Intel), Linux (arm64/amd64), and Windows (x64).
* Run via `npm run monitoring:setup` — no Homebrew, apt, or global install needed.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const {
BIN_ROOT,
DATA_ROOT,
VERSIONS,
SUPPORTED_TARGETS,
prometheusUrl,
grafanaUrl,
prometheusArchiveName,
grafanaArchiveName,
prometheusArchiveExt,
grafanaArchiveExt,
prometheusHome,
grafanaHome,
prometheusBinary,
grafanaBinary,
prometheusPlatform,
binariesReady,
} = require("./paths");
const {
downloadFile,
extractArchive,
findExtractedRoot,
replaceDir,
prometheusContainsFile,
grafanaContainsFile,
} = require("./install-utils");
async function installPrometheus(tmpDir) {
const archive = path.join(tmpDir, `${prometheusArchiveName()}.${prometheusArchiveExt()}`);
const extractRoot = path.join(tmpDir, "prometheus-extract");
console.log(`→ Prometheus ${VERSIONS.prometheus} (${prometheusPlatform()})`);
await downloadFile(prometheusUrl(), archive);
await fs.promises.rm(extractRoot, { recursive: true, force: true });
await extractArchive(archive, extractRoot);
const extracted = await findExtractedRoot(
extractRoot,
[prometheusArchiveName(), `prometheus-${VERSIONS.prometheus}`],
prometheusContainsFile
);
await replaceDir(prometheusHome(), extracted);
}
async function installGrafana(tmpDir) {
const archive = path.join(tmpDir, `${grafanaArchiveName()}.${grafanaArchiveExt()}`);
const extractRoot = path.join(tmpDir, "grafana-extract");
console.log(`→ Grafana OSS ${VERSIONS.grafana} (${prometheusPlatform()})`);
await downloadFile(grafanaUrl(), archive);
await fs.promises.rm(extractRoot, { recursive: true, force: true });
await extractArchive(archive, extractRoot);
const extracted = await findExtractedRoot(
extractRoot,
[grafanaArchiveName(), `grafana-${VERSIONS.grafana}`, `grafana-v${VERSIONS.grafana}`],
grafanaContainsFile
);
await replaceDir(grafanaHome(), extracted);
}
async function main() {
if (binariesReady()) {
console.log("Monitoring binaries already present:");
console.log(` Prometheus: ${prometheusBinary()}`);
console.log(` Grafana: ${grafanaBinary()}`);
return;
}
console.log(`Platform: ${process.platform} ${process.arch}`);
console.log(`Supported: ${SUPPORTED_TARGETS.join(" · ")}`);
await fs.promises.mkdir(BIN_ROOT, { recursive: true });
await fs.promises.mkdir(DATA_ROOT, { recursive: true });
const tmpDir = path.join(DATA_ROOT, "downloads");
await fs.promises.mkdir(tmpDir, { recursive: true });
console.log("Downloading CCAM monitoring binaries (one-time setup)…");
await installPrometheus(tmpDir);
await installGrafana(tmpDir);
await fs.promises.rm(tmpDir, { recursive: true, force: true });
if (!binariesReady()) {
throw new Error("Binary install finished but executables were not found.");
}
console.log("Done. Binaries installed to monitoring/.bin/");
console.log(` Prometheus: ${prometheusBinary()}`);
console.log(` Grafana: ${grafanaBinary()}`);
}
main().catch((err) => {
console.error(`monitoring:setup failed: ${err.message}`);
process.exit(1);
});
+88
View File
@@ -0,0 +1,88 @@
/**
* @file Archive extraction and install-dir discovery for cross-platform binaries.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { pipeline } = require("node:stream/promises");
const { Readable } = require("node:stream");
const tar = require("tar");
const AdmZip = require("adm-zip");
const { isWindows } = require("./paths");
async function downloadFile(url, dest) {
const response = await fetch(url, { redirect: "follow" });
if (!response.ok) {
throw new Error(`Download failed (${response.status}): ${url}`);
}
if (!response.body) {
throw new Error(`Empty response body: ${url}`);
}
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest));
}
async function extractTar(archivePath, destDir) {
await fs.promises.mkdir(destDir, { recursive: true });
await tar.x({ file: archivePath, cwd: destDir });
}
async function extractZip(archivePath, destDir) {
await fs.promises.mkdir(destDir, { recursive: true });
const zip = new AdmZip(archivePath);
zip.extractAllTo(destDir, true);
}
async function extractArchive(archivePath, destDir) {
if (archivePath.endsWith(".zip")) {
await extractZip(archivePath, destDir);
return;
}
await extractTar(archivePath, destDir);
}
async function listChildDirs(parent) {
const entries = await fs.promises.readdir(parent, { withFileTypes: true });
return entries.filter((e) => e.isDirectory()).map((e) => path.join(parent, e.name));
}
/**
* Locate a single top-level directory after extraction. Official archives ship
* one root folder; this also tolerates minor naming differences across OS builds.
*/
async function findExtractedRoot(extractRoot, preferredNames, containsFile) {
for (const name of preferredNames) {
const candidate = path.join(extractRoot, name);
if (fs.existsSync(path.join(candidate, containsFile))) return candidate;
}
const dirs = await listChildDirs(extractRoot);
const matches = [];
for (const dir of dirs) {
if (fs.existsSync(path.join(dir, containsFile))) matches.push(dir);
}
if (matches.length === 1) return matches[0];
if (matches.length > 1) {
throw new Error(
`Multiple install directories matched in ${extractRoot}: ${matches.map(path.basename).join(", ")}`
);
}
throw new Error(
`Could not find install directory under ${extractRoot} (expected ${containsFile}).`
);
}
async function replaceDir(target, source) {
await fs.promises.rm(target, { recursive: true, force: true });
await fs.promises.mkdir(path.dirname(target), { recursive: true });
await fs.promises.cp(source, target, { recursive: true });
}
module.exports = {
downloadFile,
extractArchive,
findExtractedRoot,
replaceDir,
prometheusContainsFile: isWindows() ? "prometheus.exe" : "prometheus",
grafanaContainsFile: path.join("bin", isWindows() ? "grafana.exe" : "grafana"),
};
+158
View File
@@ -0,0 +1,158 @@
/**
* @file Shared helpers for starting and stopping the npm-managed monitoring stack.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { spawn, spawnSync } = require("node:child_process");
const {
DATA_ROOT,
RUNTIME_PROVISIONING,
GRAFANA_DASHBOARDS,
GRAFANA_DATASOURCE_TEMPLATE,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
isWindows,
toGrafanaPath,
} = require("./paths");
function readPid(file) {
try {
const raw = fs.readFileSync(file, "utf8").trim();
const pid = Number(raw);
return Number.isInteger(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
function isRunning(pid) {
if (!pid) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function writePid(file, pid) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, String(pid));
}
function removePid(file) {
try {
fs.unlinkSync(file);
} catch {
/* already gone */
}
}
function killProcess(pid) {
if (!pid) return;
if (isWindows()) {
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
return;
}
process.kill(pid, "SIGTERM");
}
function stopPid(file, label) {
const pid = readPid(file);
if (!pid) return false;
if (!isRunning(pid)) {
removePid(file);
return false;
}
try {
killProcess(pid);
console.log(`Stopped ${label} (pid ${pid})`);
} catch (err) {
console.warn(`Could not stop ${label} (pid ${pid}): ${err.message}`);
}
removePid(file);
return true;
}
function writeGrafanaProvisioning() {
const dashboardsDir = path.join(RUNTIME_PROVISIONING, "dashboards");
const datasourcesDir = path.join(RUNTIME_PROVISIONING, "datasources");
fs.mkdirSync(dashboardsDir, { recursive: true });
fs.mkdirSync(datasourcesDir, { recursive: true });
fs.copyFileSync(GRAFANA_DATASOURCE_TEMPLATE, path.join(datasourcesDir, "datasource.yml"));
const provider = [
"apiVersion: 1",
"",
"providers:",
" - name: CCAM",
" orgId: 1",
" type: file",
" disableDeletion: false",
" updateIntervalSeconds: 30",
" allowUiUpdates: true",
" options:",
` path: ${toGrafanaPath(GRAFANA_DASHBOARDS)}`,
" foldersFromFilesStructure: false",
"",
].join("\n");
fs.writeFileSync(path.join(dashboardsDir, "provider.yml"), provider);
}
function spawnDetached(binary, args, env, logFile) {
fs.mkdirSync(path.dirname(logFile), { recursive: true });
const logFd = fs.openSync(logFile, "a");
const child = spawn(binary, args, {
detached: !isWindows(),
stdio: ["ignore", logFd, logFd],
env: { ...process.env, ...env },
windowsHide: true,
});
if (!isWindows()) child.unref();
fs.closeSync(logFd);
return child.pid;
}
function spawnForeground(binary, args, env) {
return spawn(binary, args, {
stdio: "inherit",
env: { ...process.env, ...env },
windowsHide: true,
});
}
async function waitForHttp(url, attempts = 30, intervalMs = 500) {
for (let i = 0; i < attempts; i += 1) {
try {
const res = await fetch(url);
if (res.ok) return true;
} catch {
/* retry */
}
await new Promise((r) => setTimeout(r, intervalMs));
}
return false;
}
module.exports = {
readPid,
isRunning,
writePid,
removePid,
killProcess,
stopPid,
writeGrafanaProvisioning,
spawnDetached,
spawnForeground,
waitForHttp,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
DATA_ROOT,
RUNTIME_PROVISIONING,
};
+212
View File
@@ -0,0 +1,212 @@
/**
* @file Shared paths and version pins for the CCAM monitoring stack.
* Binaries are downloaded into monitoring/.bin/ by ensure-binaries.js so the
* stack runs with plain npm on macOS, Linux, and Windows — no Homebrew, apt,
* or global installs required.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const path = require("node:path");
const MONITORING_ROOT = path.resolve(__dirname, "..");
const REPO_ROOT = path.resolve(MONITORING_ROOT, "..");
const VERSIONS = {
prometheus: "3.2.1",
grafana: "11.6.1",
};
/** Human-readable list for error messages. */
const SUPPORTED_TARGETS = [
"macOS (Apple Silicon / Intel)",
"Linux (arm64 / amd64)",
"Windows (x64)",
];
const BIN_ROOT = path.join(MONITORING_ROOT, ".bin");
const DATA_ROOT = path.join(MONITORING_ROOT, ".data");
const PROMETHEUS_DATA = path.join(DATA_ROOT, "prometheus");
const GRAFANA_DATA = path.join(DATA_ROOT, "grafana");
const RUNTIME_PROVISIONING = path.join(DATA_ROOT, "grafana-provisioning");
const PROMETHEUS_CONFIG = path.join(MONITORING_ROOT, "prometheus", "prometheus-native.yml");
const PROMETHEUS_CONSOLES = path.join(MONITORING_ROOT, "prometheus", "consoles");
const GRAFANA_DASHBOARDS = path.join(MONITORING_ROOT, "grafana", "dashboards");
const GRAFANA_DATASOURCE_TEMPLATE = path.join(
MONITORING_ROOT,
"grafana",
"provisioning-native",
"datasources",
"datasource.yml"
);
const PROMETHEUS_PID = path.join(DATA_ROOT, "prometheus.pid");
const GRAFANA_PID = path.join(DATA_ROOT, "grafana.pid");
const PROMETHEUS_LOG = path.join(DATA_ROOT, "prometheus.log");
const GRAFANA_LOG = path.join(DATA_ROOT, "grafana.log");
function isWindows() {
return process.platform === "win32";
}
/**
* Maps Node's `process.arch` to Prometheus/Grafana release asset arch slugs.
* @returns {"arm64"|"amd64"}
*/
function releaseArch() {
if (process.arch === "arm64") return "arm64";
if (process.arch === "x64") return "amd64";
throw new Error(
`Unsupported CPU architecture "${process.arch}". Supported: ${SUPPORTED_TARGETS.join(", ")}.`
);
}
/** @returns {string} Prometheus platform slug used in release asset names. */
function prometheusPlatform() {
const arch = releaseArch();
if (process.platform === "darwin") return arch === "arm64" ? "darwin-arm64" : "darwin-amd64";
if (process.platform === "linux") return arch === "arm64" ? "linux-arm64" : "linux-amd64";
if (process.platform === "win32") return "windows-amd64";
throw new Error(
`Unsupported OS "${process.platform}". Supported: ${SUPPORTED_TARGETS.join(", ")}.`
);
}
/** @returns {string} Grafana platform slug used in release asset names. */
function grafanaPlatform() {
return prometheusPlatform();
}
function prometheusArchiveName() {
return `prometheus-${VERSIONS.prometheus}.${prometheusPlatform()}`;
}
function grafanaArchiveName() {
return `grafana-${VERSIONS.grafana}.${grafanaPlatform()}`;
}
function prometheusArchiveExt() {
return isWindows() ? "zip" : "tar.gz";
}
function grafanaArchiveExt() {
return isWindows() ? "zip" : "tar.gz";
}
function prometheusUrl() {
const name = prometheusArchiveName();
return `https://github.com/prometheus/prometheus/releases/download/v${VERSIONS.prometheus}/${name}.${prometheusArchiveExt()}`;
}
function grafanaUrl() {
const plat = grafanaPlatform();
return `https://dl.grafana.com/oss/release/grafana-${VERSIONS.grafana}.${plat}.${grafanaArchiveExt()}`;
}
function prometheusHome() {
return path.join(BIN_ROOT, "prometheus");
}
function grafanaHome() {
return path.join(BIN_ROOT, "grafana");
}
function prometheusBinary() {
const name = isWindows() ? "prometheus.exe" : "prometheus";
return path.join(prometheusHome(), name);
}
function grafanaBinary() {
const name = isWindows() ? "grafana.exe" : "grafana";
return path.join(grafanaHome(), "bin", name);
}
const GRAFANA_ADMIN_USER = "admin";
const GRAFANA_ADMIN_PASSWORD = "admin";
/** Grafana env vars that seed the default admin account on first start. */
function grafanaAdminEnv() {
return {
GF_SECURITY_ADMIN_USER: GRAFANA_ADMIN_USER,
GF_SECURITY_ADMIN_PASSWORD: GRAFANA_ADMIN_PASSWORD,
GF_USERS_ALLOW_SIGN_UP: "false",
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID: "ccam-overview",
};
}
function grafanaLoginLabel() {
return `${GRAFANA_ADMIN_USER} / ${GRAFANA_ADMIN_PASSWORD}`;
}
/** Grafana file providers require forward slashes even on Windows. */
function toGrafanaPath(filePath) {
return filePath.replace(/\\/g, "/");
}
/** CLI args for Prometheus 3.x (console libraries removed upstream). */
function prometheusServerArgs(configFile, storagePath = PROMETHEUS_DATA) {
return [
`--config.file=${configFile}`,
`--storage.tsdb.path=${storagePath}`,
"--web.enable-lifecycle",
`--web.console.templates=${PROMETHEUS_CONSOLES}`,
];
}
function prometheusDockerServerArgs(configFile) {
return [
`--config.file=${configFile}`,
`--storage.tsdb.path=/prometheus`,
"--web.enable-lifecycle",
`--web.console.templates=/etc/prometheus/consoles`,
];
}
const PROMETHEUS_CONSOLES_URL = "http://localhost:9090/consoles/index.html";
function binariesReady() {
const fs = require("node:fs");
return fs.existsSync(prometheusBinary()) && fs.existsSync(grafanaBinary());
}
module.exports = {
MONITORING_ROOT,
REPO_ROOT,
VERSIONS,
SUPPORTED_TARGETS,
BIN_ROOT,
DATA_ROOT,
PROMETHEUS_DATA,
GRAFANA_DATA,
RUNTIME_PROVISIONING,
PROMETHEUS_CONFIG,
PROMETHEUS_CONSOLES,
PROMETHEUS_CONSOLES_URL,
GRAFANA_DASHBOARDS,
GRAFANA_DATASOURCE_TEMPLATE,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
isWindows,
releaseArch,
prometheusPlatform,
grafanaPlatform,
prometheusArchiveName,
grafanaArchiveName,
prometheusArchiveExt,
grafanaArchiveExt,
prometheusUrl,
grafanaUrl,
prometheusHome,
grafanaHome,
prometheusBinary,
grafanaBinary,
prometheusServerArgs,
prometheusDockerServerArgs,
GRAFANA_ADMIN_USER,
GRAFANA_ADMIN_PASSWORD,
grafanaAdminEnv,
grafanaLoginLabel,
toGrafanaPath,
binariesReady,
};
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env node
/**
* @file Starts the npm-managed Prometheus + Grafana stack (no Docker / no Brew).
* Downloads binaries on first run if monitoring:setup has not been run yet.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const {
PROMETHEUS_CONFIG,
PROMETHEUS_DATA,
GRAFANA_DATA,
RUNTIME_PROVISIONING,
prometheusBinary,
prometheusServerArgs,
PROMETHEUS_CONSOLES_URL,
grafanaBinary,
grafanaHome,
grafanaAdminEnv,
grafanaLoginLabel,
binariesReady,
} = require("./paths");
const {
readPid,
isRunning,
writePid,
writeGrafanaProvisioning,
spawnDetached,
spawnForeground,
waitForHttp,
killProcess,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
} = require("./lib");
const detached = process.argv.includes("--detach") || process.argv.includes("-d");
const foreground = process.argv.includes("--foreground") || process.argv.includes("-f");
async function ensureBinaries() {
if (binariesReady()) return;
console.log("Monitoring binaries not found — running setup…");
const script = path.join(__dirname, "ensure-binaries.js");
const result = spawnSync(process.execPath, [script], { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status || 1);
}
function assertNotRunning() {
for (const [file, label] of [
[PROMETHEUS_PID, "Prometheus"],
[GRAFANA_PID, "Grafana"],
]) {
const pid = readPid(file);
if (isRunning(pid)) {
console.error(`${label} is already running (pid ${pid}). Run: npm run monitoring:down`);
process.exit(1);
}
}
}
function startPrometheus() {
fs.mkdirSync(PROMETHEUS_DATA, { recursive: true });
const args = prometheusServerArgs(PROMETHEUS_CONFIG);
const binary = prometheusBinary();
if (detached || (!foreground && !process.stdout.isTTY)) {
const pid = spawnDetached(binary, args, {}, PROMETHEUS_LOG);
writePid(PROMETHEUS_PID, pid);
return pid;
}
return spawnForeground(binary, args, {});
}
function startGrafana() {
fs.mkdirSync(GRAFANA_DATA, { recursive: true });
writeGrafanaProvisioning();
const env = {
GF_PATHS_HOME: grafanaHome(),
GF_PATHS_DATA: GRAFANA_DATA,
GF_PATHS_PROVISIONING: RUNTIME_PROVISIONING,
...grafanaAdminEnv(),
};
const args = ["server", "--homepath", grafanaHome()];
const binary = grafanaBinary();
if (detached || (!foreground && !process.stdout.isTTY)) {
const pid = spawnDetached(binary, args, env, GRAFANA_LOG);
writePid(GRAFANA_PID, pid);
return pid;
}
return spawnForeground(binary, args, env);
}
async function main() {
await ensureBinaries();
assertNotRunning();
console.log("Starting CCAM monitoring stack…");
console.log(` Prometheus config: ${PROMETHEUS_CONFIG}`);
console.log(` Grafana home: ${grafanaHome()}`);
if (detached || (!foreground && !process.stdout.isTTY)) {
const promPid = startPrometheus();
const grafPid = startGrafana();
const promOk = await waitForHttp("http://127.0.0.1:9090/-/ready");
const grafOk = await waitForHttp("http://127.0.0.1:3000/api/health");
console.log("");
console.log(
`Prometheus http://localhost:9090 (pid ${promPid})${promOk ? "" : " [still starting]"}`
);
console.log(` CCAM console: ${PROMETHEUS_CONSOLES_URL}`);
console.log(
`Grafana http://localhost:3000 (pid ${grafPid}) ${grafanaLoginLabel()}${grafOk ? "" : " [still starting]"}`
);
console.log(
"CCAM dashboards auto-provisioned (Overview, Sessions & Agents, Tokens & Events, Platform Health)."
);
console.log("Stop with: npm run monitoring:down");
if (!promOk || !grafOk) {
console.log(`Logs: ${PROMETHEUS_LOG} ${GRAFANA_LOG}`);
}
return;
}
// Foreground: Prometheus in background, Grafana in foreground (Grafana owns the tty).
const promPid = spawnDetached(
prometheusBinary(),
prometheusServerArgs(PROMETHEUS_CONFIG),
{},
PROMETHEUS_LOG
);
writePid(PROMETHEUS_PID, promPid);
console.log(`Prometheus running at http://localhost:9090 (pid ${promPid})`);
console.log(` CCAM console: ${PROMETHEUS_CONSOLES_URL}`);
console.log(
`Grafana starting at http://localhost:3000 (${grafanaLoginLabel()}) — Ctrl+C stops both`
);
const graf = startGrafana();
const shutdown = () => {
killProcess(promPid);
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
graf.on("exit", (code) => {
shutdown();
process.exit(code ?? 0);
});
}
main().catch((err) => {
console.error(`monitoring:start failed: ${err.message}`);
process.exit(1);
});
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env node
/**
* @file Stops the npm-managed Prometheus + Grafana stack started by start.js.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { stopPid, PROMETHEUS_PID, GRAFANA_PID } = require("./lib");
const stoppedGrafana = stopPid(GRAFANA_PID, "Grafana");
const stoppedPrometheus = stopPid(PROMETHEUS_PID, "Prometheus");
if (!stoppedGrafana && !stoppedPrometheus) {
console.log("Monitoring stack is not running.");
} else {
console.log("Monitoring stack stopped.");
}
+199
View File
@@ -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();