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
+309
View File
@@ -0,0 +1,309 @@
/**
* @file Universal webhook delivery for fired alerts. A "target" is an outbound
* destination described by the provider registry (server/lib/webhook-providers.js)
* — Slack, Discord, Teams, Mattermost, Rocket.Chat, Telegram, PagerDuty,
* Opsgenie, Splunk On-Call, Zapier, Make, n8n, Pipedream, or a generic endpoint.
* When the alerting engine fires an alert (server/lib/alerts.js), it calls
* dispatchAlert(), which formats the provider-native payload and POSTs it to
* every enabled target (optionally scoped to specific rules) with a timeout and
* bounded retry/backoff. Every attempt-chain is recorded in webhook_deliveries.
*
* Delivery is detached and fully fail-safe: it never throws into, slows, or
* blocks the alert path or hook ingestion.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const crypto = require("crypto");
const { stmts } = require("../db");
const {
PROVIDERS,
WEBHOOK_TYPES,
isGenericFamily,
resolveUrl,
resolveAuthHeaders,
formatPayload,
truncate,
} = require("./webhook-providers");
// Tunables (env-overridable so tests can shrink timeouts/backoff). All read at
// module load — restart to change.
function posEnv(name, fallback) {
const raw = parseInt(process.env[name], 10);
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
}
const TIMEOUT_MS = posEnv("WEBHOOK_TIMEOUT_MS", 10_000);
const MAX_ATTEMPTS = posEnv("WEBHOOK_MAX_ATTEMPTS", 3);
const RETRY_BASE_MS = posEnv("WEBHOOK_RETRY_BASE_MS", 1500);
// Enabled-target cache. Alert fires are hot; targets only change through the
// CRUD routes, which call invalidateWebhookCache().
let targetsCache = null;
function invalidateWebhookCache() {
targetsCache = null;
}
/** Parse the JSON columns and coerce the enabled flag for a raw target row. */
function normalizeTarget(row) {
if (!row) return null;
let headers = null;
let ruleIds = null;
let config = null;
try {
headers = row.headers ? JSON.parse(row.headers) : null;
} catch {
/* tolerate hand-edited bad JSON — extra headers simply not applied */
}
try {
ruleIds = row.rule_ids ? JSON.parse(row.rule_ids) : null;
} catch {
/* tolerate bad JSON — target falls back to "all rules" */
}
try {
config = row.config ? JSON.parse(row.config) : null;
} catch {
/* tolerate bad JSON — provider config falls back to empty */
}
return { ...row, enabled: row.enabled === 1, headers, rule_ids: ruleIds, config };
}
function loadEnabledTargets() {
if (targetsCache) return targetsCache;
targetsCache = stmts.listEnabledWebhookTargets.all().map(normalizeTarget);
return targetsCache;
}
/**
* Build the HTTP request for a target + alert: resolved URL, provider-native
* serialized body, and headers (provider auth headers, plus custom headers and
* an optional HMAC-SHA256 signature for the generic family). Exported for tests.
*/
function buildRequest(target, alert) {
const url = resolveUrl(target);
if (!url) throw new Error(`no URL resolved for webhook type "${target.type}"`);
const payload = formatPayload(target.type, alert, target.config || {});
const body = JSON.stringify(payload);
const headers = {
"Content-Type": "application/json",
"User-Agent": "claude-code-agent-monitor/webhooks",
...resolveAuthHeaders(target),
};
if (isGenericFamily(target.type)) {
if (target.headers && typeof target.headers === "object") {
for (const [k, v] of Object.entries(target.headers)) {
if (typeof k !== "string" || typeof v !== "string") continue;
// Never let a custom header clobber Content-Type or the signature.
const lower = k.toLowerCase();
if (lower === "content-type" || lower === "x-webhook-signature") continue;
headers[k] = v;
}
}
if (target.secret) {
const ts = new Date().toISOString();
const sig = crypto.createHmac("sha256", target.secret).update(`${ts}.${body}`).digest("hex");
headers["X-Webhook-Timestamp"] = ts;
headers["X-Webhook-Signature"] = `sha256=${sig}`;
}
}
return { url, body, headers };
}
// ── Delivery ────────────────────────────────────────────────────────────────
function sleep(ms) {
return new Promise((resolve) => {
const t = setTimeout(resolve, ms);
if (t.unref) t.unref();
});
}
async function postOnce(url, body, headers) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
if (timer.unref) timer.unref();
try {
const res = await fetch(url, {
method: "POST",
headers,
body,
signal: controller.signal,
redirect: "follow",
});
// Read the response body — some providers (Splunk On-Call) signal failure
// in the body despite a 200, so deliver() may need to inspect it. Also
// frees the socket promptly. (Named distinctly from the `body` param.)
let responseBody = "";
try {
responseBody = await res.text();
} catch {
/* body read is best-effort */
}
return {
ok: res.status >= 200 && res.status < 300,
status: res.status,
error: null,
body: responseBody,
};
} catch (err) {
const timedOut = err?.name === "AbortError";
return {
ok: false,
status: null,
error: timedOut ? "timeout" : err?.message || "network error",
body: "",
};
} finally {
clearTimeout(timer);
}
}
function recordDelivery(target, alertId, { status, statusCode, attempts, error }) {
try {
stmts.insertWebhookDelivery.run(
target.id,
target.name,
target.type,
alertId == null ? null : alertId,
status,
statusCode == null ? null : statusCode,
attempts,
error == null ? null : truncate(error, 500)
);
stmts.pruneWebhookDeliveries.run();
} catch (err) {
console.warn("[WEBHOOK] delivery log write failed:", err?.message || err);
}
}
/**
* Deliver one alert to one target with bounded retry. Retries on transport
* errors, HTTP 429, and 5xx; gives up immediately on other 4xx (misconfigured
* URL / bad payload won't fix themselves). Always records the outcome and
* never throws. Returns `{ ok, status, attempts, error }`.
*/
async function deliver(target, alert) {
let built;
try {
built = buildRequest(target, alert);
} catch (err) {
recordDelivery(target, alert.id, {
status: "failed",
statusCode: null,
attempts: 0,
error: `request build failed: ${err?.message || err}`,
});
return { ok: false, status: null, attempts: 0, error: "request build failed" };
}
let attempts = 0;
let status = null;
let error = null;
const verifyResponse = PROVIDERS[target.type]?.verifyResponse;
while (attempts < MAX_ATTEMPTS) {
attempts += 1;
const res = await postOnce(built.url, built.body, built.headers);
status = res.status;
error = res.error;
if (res.ok) {
// Some providers (Splunk On-Call) return 200 even on rejection — let the
// provider veto a "successful" status by inspecting the response body.
const verdict = verifyResponse ? verifyResponse(res.body) : { ok: true };
if (verdict.ok) {
recordDelivery(target, alert.id, {
status: "success",
statusCode: status,
attempts,
error: null,
});
return { ok: true, status, attempts };
}
// A logical rejection won't fix on retry — fail immediately.
error = verdict.error || "provider reported failure";
break;
}
const retryable = status == null || status === 429 || status >= 500;
if (!retryable || attempts >= MAX_ATTEMPTS) break;
await sleep(RETRY_BASE_MS * attempts);
}
recordDelivery(target, alert.id, {
status: "failed",
statusCode: status,
attempts,
error: error || (status ? `HTTP ${status}` : "request failed"),
});
return {
ok: false,
status,
attempts,
error: error || (status ? `HTTP ${status}` : "request failed"),
};
}
/** A target receives an alert when it has no rule scope, or the alert's rule is in scope. */
function targetAppliesTo(target, alert) {
if (!Array.isArray(target.rule_ids) || target.rule_ids.length === 0) return true;
return target.rule_ids.includes(alert.rule_id);
}
/**
* Fan an alert out to every enabled, in-scope target. Returns a promise that
* settles when all deliveries finish (used by tests); callers in the alert
* path invoke it fire-and-forget. Never rejects.
*/
function dispatchAlert(alert) {
let targets;
try {
targets = loadEnabledTargets();
} catch (err) {
console.warn("[WEBHOOK] target load failed:", err?.message || err);
return Promise.resolve([]);
}
const applicable = targets.filter((t) => {
try {
return targetAppliesTo(t, alert);
} catch {
return false;
}
});
if (applicable.length === 0) return Promise.resolve([]);
return Promise.allSettled(applicable.map((t) => deliver(t, alert)));
}
/**
* Send a synthetic test alert to a single (already DB-loaded, un-redacted)
* target. Awaits the result so the route can report success/failure inline.
*/
function sendTest(target) {
const alert = {
id: null,
rule_id: null,
rule_name: "Webhook test",
rule_type: "test",
session_id: null,
agent_id: null,
message: `Test notification from Claude Code Agent Monitor to "${target.name}". If you can read this, delivery works.`,
details: { test: true, target: target.name, type: target.type },
triggered_at: new Date().toISOString(),
};
return deliver(target, alert);
}
module.exports = {
PROVIDERS,
WEBHOOK_TYPES,
invalidateWebhookCache,
loadEnabledTargets,
normalizeTarget,
formatPayload,
buildRequest,
deliver,
dispatchAlert,
sendTest,
targetAppliesTo,
};