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
+387
View File
@@ -0,0 +1,387 @@
/**
* @file Express router for universal webhook targets across 14 providers
* (Slack, Discord, Teams, Google Chat, Mattermost, Rocket.Chat, Telegram,
* PagerDuty, Opsgenie, Splunk On-Call, Zapier, Make, n8n, Pipedream, generic).
* Provides target CRUD, a synchronous "send test" probe, a per-target delivery
* log, and redacted provider metadata for the UI. Secrets are never returned —
* URLs are masked and secret config / header values are redacted in every
* response. Delivery + provider definitions live in server/lib/.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { Router } = require("express");
const { v4: uuidv4 } = require("uuid");
const { stmts } = require("../db");
const { invalidateWebhookCache, normalizeTarget, sendTest } = require("../lib/webhooks");
const {
PROVIDERS,
WEBHOOK_TYPES,
isGenericFamily,
resolveUrl,
urlRequired,
publicProviders,
} = require("../lib/webhook-providers");
const router = Router();
// ── Serialization (redacted) ──────────────────────────────────────────────
// Reveal the host + last 4 chars so a user can recognize which webhook this is
// without exposing any embedded secret token.
function maskUrl(url) {
if (!url) return "…";
try {
const u = new URL(url);
const tail = url.length > 4 ? url.slice(-4) : "";
return `${u.protocol}//${u.host}/…${tail}`;
} catch {
return "…";
}
}
// Custom header values can carry auth tokens — return only the keys, masked.
function redactHeaders(headers) {
if (!headers || typeof headers !== "object") return null;
const keys = Object.keys(headers);
if (keys.length === 0) return null;
const out = {};
for (const k of keys) out[k] = "••••";
return out;
}
// Mask provider config fields flagged secret (routing keys, api keys, tokens);
// show the rest (chat_id, region, severity, …).
function redactConfig(type, config) {
if (!config || typeof config !== "object") return null;
const fields = PROVIDERS[type]?.fields || [];
const secretKeys = new Set(fields.filter((f) => f.secret).map((f) => f.key));
const out = {};
for (const [k, v] of Object.entries(config)) out[k] = secretKeys.has(k) ? "••••" : v;
return Object.keys(out).length ? out : null;
}
function serializeTarget(row) {
const t = normalizeTarget(row);
let last = null;
try {
last = stmts.lastWebhookDeliveryForTarget.get(t.id) || null;
} catch {
/* delivery log read is best-effort */
}
return {
id: t.id,
name: t.name,
type: t.type,
enabled: t.enabled,
url_preview: maskUrl(resolveUrl(t)),
has_secret: !!t.secret,
headers: isGenericFamily(t.type) ? redactHeaders(t.headers) : null,
config: redactConfig(t.type, t.config),
rule_ids: t.rule_ids && t.rule_ids.length ? t.rule_ids : null,
created_at: t.created_at,
updated_at: t.updated_at,
last_delivery: last
? {
status: last.status,
status_code: last.status_code,
attempts: last.attempts,
error: last.error,
created_at: last.created_at,
}
: null,
};
}
// ── Validation ────────────────────────────────────────────────────────────
function bad(res, message) {
return res.status(400).json({ error: { code: "INVALID_INPUT", message } });
}
function validateUrl(url, type) {
if (typeof url !== "string" || !url.trim()) return { ok: false, error: "url is required" };
let u;
try {
u = new URL(url.trim());
} catch {
return { ok: false, error: "url must be a valid URL" };
}
if (u.protocol !== "https:" && u.protocol !== "http:") {
return { ok: false, error: "url must use http or https" };
}
// Most providers' endpoints are https-only; only the generic family with
// https:false (generic, n8n) permits http (for local/self-hosted testing).
const allowHttp = PROVIDERS[type]?.https === false;
if (!allowHttp && u.protocol !== "https:") {
return { ok: false, error: `${type} webhook URL must use https` };
}
return { ok: true, url: url.trim() };
}
// Validate (and normalize) provider config, merging supplied values over a base
// (the existing config on PATCH) so a single field can change without re-sending
// secrets. Returns { ok, value } where value is the full config object or null.
function validateConfig(type, input, base = {}) {
const fields = PROVIDERS[type]?.fields || [];
if (input != null && (typeof input !== "object" || Array.isArray(input))) {
return { ok: false, error: "config must be an object" };
}
const supplied = input || {};
const out = {};
for (const f of fields) {
// Effective value: a non-empty supplied value wins, else fall back to base.
let v = supplied[f.key];
if (v == null || v === "") v = base[f.key];
if (v == null || v === "") {
if (f.default != null) {
out[f.key] = f.default;
continue;
}
if (f.required) return { ok: false, error: `${f.label} is required` };
continue;
}
if (f.type === "enum") {
if (!f.options.includes(v)) {
return { ok: false, error: `${f.label} must be one of: ${f.options.join(", ")}` };
}
} else {
if (typeof v !== "string") return { ok: false, error: `${f.label} must be a string` };
v = v.trim();
if (!v) {
if (f.required) return { ok: false, error: `${f.label} is required` };
continue;
}
}
out[f.key] = v;
}
return { ok: true, value: Object.keys(out).length ? out : null };
}
function validateHeaders(headers) {
if (headers == null) return { ok: true, value: null };
if (typeof headers !== "object" || Array.isArray(headers)) {
return { ok: false, error: "headers must be an object of string values" };
}
const out = {};
for (const [k, v] of Object.entries(headers)) {
if (typeof k !== "string" || !k.trim()) {
return { ok: false, error: "header names must be non-empty strings" };
}
if (typeof v !== "string") return { ok: false, error: `header "${k}" value must be a string` };
out[k] = v;
}
return { ok: true, value: Object.keys(out).length ? out : null };
}
function validateRuleIds(ruleIds) {
if (ruleIds == null) return { ok: true, value: null };
if (!Array.isArray(ruleIds)) return { ok: false, error: "rule_ids must be an array" };
for (const id of ruleIds) {
if (typeof id !== "string" || !id.trim()) {
return { ok: false, error: "rule_ids must be non-empty strings" };
}
}
return { ok: true, value: ruleIds.length ? ruleIds : null };
}
// ── Routes ──────────────────────────────────────────────────────────────────
// GET /api/webhooks/providers — redacted provider catalog for the UI
router.get("/providers", (_req, res) => {
res.json({ providers: publicProviders() });
});
// GET /api/webhooks — list targets (redacted)
router.get("/", (_req, res) => {
res.json({ targets: stmts.listWebhookTargets.all().map(serializeTarget) });
});
// POST /api/webhooks — create a target
router.post("/", (req, res) => {
const { name, type, url, enabled, secret, headers, rule_ids, config } = req.body || {};
if (!name || typeof name !== "string" || !name.trim()) return bad(res, "name is required");
if (!WEBHOOK_TYPES.includes(type)) {
return bad(res, `type must be one of: ${WEBHOOK_TYPES.join(", ")}`);
}
// URL: required for some providers, derived/defaulted for others (Telegram,
// Opsgenie, PagerDuty). Stored as "" when not user-supplied.
let storedUrl = "";
if (urlRequired(type)) {
const u = validateUrl(url, type);
if (!u.ok) return bad(res, u.error);
storedUrl = u.url;
} else if (url != null && String(url).trim()) {
const u = validateUrl(url, type);
if (!u.ok) return bad(res, u.error);
storedUrl = u.url;
}
const cfg = validateConfig(type, config);
if (!cfg.ok) return bad(res, cfg.error);
// secret + custom headers only apply to the generic family.
const generic = isGenericFamily(type);
const h = validateHeaders(generic ? headers : null);
if (!h.ok) return bad(res, h.error);
const r = validateRuleIds(rule_ids);
if (!r.ok) return bad(res, r.error);
let sec = null;
if (generic && secret != null) {
if (typeof secret !== "string") return bad(res, "secret must be a string");
sec = secret.trim() || null;
}
const id = uuidv4();
stmts.insertWebhookTarget.run(
id,
name.trim(),
type,
storedUrl,
enabled === false ? 0 : 1,
sec,
h.value ? JSON.stringify(h.value) : null,
r.value ? JSON.stringify(r.value) : null,
cfg.value ? JSON.stringify(cfg.value) : null
);
invalidateWebhookCache();
res.status(201).json({ target: serializeTarget(stmts.getWebhookTarget.get(id)) });
});
// PATCH /api/webhooks/:id — partial update. url/secret/headers/rule_ids/config
// are only changed when their key is present in the body (omit = leave as-is).
router.patch("/:id", (req, res) => {
const existing = stmts.getWebhookTarget.get(req.params.id);
if (!existing) {
return res
.status(404)
.json({ error: { code: "NOT_FOUND", message: "Webhook target not found" } });
}
const body = req.body || {};
const { name, url, enabled, secret, headers, rule_ids, config } = body;
const generic = isGenericFamily(existing.type);
if (name != null && (typeof name !== "string" || !name.trim())) {
return bad(res, "name must be a non-empty string");
}
let urlVal = null;
if (url != null) {
const u = validateUrl(url, existing.type);
if (!u.ok) return bad(res, u.error);
urlVal = u.url;
}
// config: merge supplied fields over the existing config, then re-validate,
// so e.g. region can change without re-sending the api_key.
let configSet = 0;
let configVal = null;
if ("config" in body) {
let base = {};
try {
base = existing.config ? JSON.parse(existing.config) : {};
} catch {
base = {};
}
const cfg = validateConfig(existing.type, config, base);
if (!cfg.ok) return bad(res, cfg.error);
configSet = 1;
configVal = cfg.value ? JSON.stringify(cfg.value) : null;
}
let secretSet = 0;
let secretVal = null;
if ("secret" in body && generic) {
if (secret !== null && typeof secret !== "string") {
return bad(res, "secret must be a string or null");
}
secretSet = 1;
secretVal = secret ? String(secret).trim() || null : null;
}
let headersSet = 0;
let headersVal = null;
if ("headers" in body && generic) {
const h = validateHeaders(headers);
if (!h.ok) return bad(res, h.error);
headersSet = 1;
headersVal = h.value ? JSON.stringify(h.value) : null;
}
let ruleSet = 0;
let ruleVal = null;
if ("rule_ids" in body) {
const r = validateRuleIds(rule_ids);
if (!r.ok) return bad(res, r.error);
ruleSet = 1;
ruleVal = r.value ? JSON.stringify(r.value) : null;
}
stmts.updateWebhookTarget.run(
name != null ? name.trim() : null,
urlVal,
enabled == null ? null : enabled ? 1 : 0,
secretSet,
secretVal,
headersSet,
headersVal,
ruleSet,
ruleVal,
configSet,
configVal,
req.params.id
);
invalidateWebhookCache();
res.json({ target: serializeTarget(stmts.getWebhookTarget.get(req.params.id)) });
});
// DELETE /api/webhooks/:id — delete a target (its delivery log cascades away)
router.delete("/:id", (req, res) => {
const existing = stmts.getWebhookTarget.get(req.params.id);
if (!existing) {
return res
.status(404)
.json({ error: { code: "NOT_FOUND", message: "Webhook target not found" } });
}
stmts.deleteWebhookTarget.run(req.params.id);
invalidateWebhookCache();
res.json({ ok: true });
});
// POST /api/webhooks/:id/test — send a synthetic alert and report the result.
// Always 200 (the request itself succeeded); `ok` carries the delivery result.
router.post("/:id/test", async (req, res) => {
const row = stmts.getWebhookTarget.get(req.params.id);
if (!row) {
return res
.status(404)
.json({ error: { code: "NOT_FOUND", message: "Webhook target not found" } });
}
const result = await sendTest(normalizeTarget(row));
res.json({
ok: result.ok,
status: result.status ?? null,
attempts: result.attempts,
error: result.error || null,
});
});
// GET /api/webhooks/:id/deliveries — recent delivery log for a target
router.get("/:id/deliveries", (req, res) => {
const row = stmts.getWebhookTarget.get(req.params.id);
if (!row) {
return res
.status(404)
.json({ error: { code: "NOT_FOUND", message: "Webhook target not found" } });
}
const limit = Math.max(1, Math.min(parseInt(req.query.limit, 10) || 20, 200));
const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
const deliveries = stmts.listWebhookDeliveriesForTarget.all(req.params.id, limit, offset);
res.json({ deliveries, limit, offset });
});
module.exports = router;
module.exports.WEBHOOK_TYPES = WEBHOOK_TYPES;