diff --git a/bin/ccam.js b/bin/ccam.js index 6ff3a2a8..d9ac1a44 100755 --- a/bin/ccam.js +++ b/bin/ccam.js @@ -551,7 +551,8 @@ const offlineData = { async function cmdHealth() { const h = await get("/api/health"); - console.log(`${c.green("●")} Dashboard ${c.bold("up")} at ${baseUrl()} (${h.timestamp})`); + const ver = h.version ? ` v${h.version}` : ""; + console.log(`${c.green("●")} Dashboard ${c.bold("up")}${ver} at ${baseUrl()} (${h.timestamp})`); } function renderStats(s, source) { diff --git a/monitoring/scripts/verify.js b/monitoring/scripts/verify.js index db27bfcf..e06339ae 100644 --- a/monitoring/scripts/verify.js +++ b/monitoring/scripts/verify.js @@ -9,6 +9,21 @@ 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 { @@ -127,7 +142,7 @@ async function checkPrometheusConsole() { async function main() { const checks = [ - await check("Dashboard /api/health", `${DASHBOARD_URL}/api/health`), + await checkDashboardHealth(), await check( "Dashboard /api/metrics", `${DASHBOARD_URL}/api/metrics`, @@ -144,13 +159,32 @@ async function main() { let failed = 0; for (const c of checks) { if (c.ok) { - console.log(`✔ ${c.name}${c.detail ? ` (${c.detail})` : ""}`); + if (!JSON_MODE) { + console.log(`✔ ${c.name}${c.detail ? ` (${c.detail})` : ""}`); + } } else { failed += 1; - console.error(`✖ ${c.name}: ${c.detail || "failed"}`); + 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); diff --git a/openapi.yaml b/openapi.yaml index e96ebb55..1669198d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -353,11 +353,16 @@ components: required: - status - timestamp + - version properties: status: type: string enum: - ok + version: + type: string + description: Dashboard release version from package.json + example: 1.4.1 timestamp: type: string format: date-time diff --git a/server/__tests__/api.test.js b/server/__tests__/api.test.js index d546b199..b124ae86 100644 --- a/server/__tests__/api.test.js +++ b/server/__tests__/api.test.js @@ -170,6 +170,7 @@ describe("GET /api/health", () => { const res = await fetch("/api/health"); assert.equal(res.status, 200); assert.equal(res.body.status, "ok"); + assert.equal(res.body.version, pkg.version); assert.ok(res.body.timestamp); }); }); diff --git a/server/__tests__/ccam-cli.test.js b/server/__tests__/ccam-cli.test.js index 128038e2..da1c2b38 100644 --- a/server/__tests__/ccam-cli.test.js +++ b/server/__tests__/ccam-cli.test.js @@ -113,6 +113,7 @@ describe("ccam CLI — monitoring", () => { assert.equal(code, 0); assert.match(out, /Dashboard/); assert.match(out, /up/); + assert.match(out, /v\d+\.\d+\.\d+/); assert.match(out, new RegExp(String(PORT))); }); diff --git a/server/index.js b/server/index.js index 83bcfcea..0ffb9955 100644 --- a/server/index.js +++ b/server/index.js @@ -66,6 +66,14 @@ const webhooksRouter = require("./routes/webhooks"); const remoteSourcesRouter = require("./routes/remote-sources"); const metricsRouter = require("./routes/metrics"); +const APP_VERSION = (() => { + try { + return require("../package.json").version || "0.0.0"; + } catch { + return "0.0.0"; + } +})(); + function createApp() { const app = express(); const openApiSpec = createOpenApiSpec(); @@ -127,7 +135,7 @@ function createApp() { }); app.get("/api/health", (_req, res) => { - res.json({ status: "ok", timestamp: new Date().toISOString() }); + res.json({ status: "ok", version: APP_VERSION, timestamp: new Date().toISOString() }); }); return app; diff --git a/server/openapi.js b/server/openapi.js index 596f4005..3f073ea1 100644 --- a/server/openapi.js +++ b/server/openapi.js @@ -322,9 +322,14 @@ function createOpenApiSpec() { }, HealthResponse: { type: "object", - required: ["status", "timestamp"], + required: ["status", "timestamp", "version"], properties: { status: { type: "string", enum: ["ok"] }, + version: { + type: "string", + description: "Dashboard release version from package.json", + example: "1.4.1", + }, timestamp: { type: "string", format: "date-time" }, }, },