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,132 @@
|
||||
/**
|
||||
* @file Handles web push notifications using the `web-push` library, including generating/loading VAPID keys, sending notifications to all subscribed clients, and cleaning up invalid subscriptions. It provides a function to retrieve the public VAPID key for client registration and a function to broadcast notifications to all subscribers stored in the database.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const webpush = require("web-push");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { getDataDir } = require("./claude-home");
|
||||
|
||||
// Lives in the shared data dir alongside the SQLite DB (see getDataDir), so the
|
||||
// web app and the native apps reuse one set of VAPID keys.
|
||||
const KEYS_PATH = path.join(getDataDir(), "vapid-keys.json");
|
||||
|
||||
function loadOrCreateVapidKeys() {
|
||||
if (fs.existsSync(KEYS_PATH)) {
|
||||
return JSON.parse(fs.readFileSync(KEYS_PATH, "utf8"));
|
||||
}
|
||||
const keys = webpush.generateVAPIDKeys();
|
||||
fs.mkdirSync(path.dirname(KEYS_PATH), { recursive: true });
|
||||
fs.writeFileSync(KEYS_PATH, JSON.stringify(keys, null, 2));
|
||||
return keys;
|
||||
}
|
||||
|
||||
const vapidKeys = loadOrCreateVapidKeys();
|
||||
|
||||
webpush.setVapidDetails(
|
||||
"https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor",
|
||||
vapidKeys.publicKey,
|
||||
vapidKeys.privateKey
|
||||
);
|
||||
|
||||
function getPublicKey() {
|
||||
return vapidKeys.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a native OS notification when this process is the Electron main process
|
||||
* (i.e. the desktop app embeds the server in-process). Web Push is unreliable
|
||||
* inside Electron — Chromium-in-Electron ships without Firebase Cloud
|
||||
* Messaging credentials, so `pushManager.subscribe()` in the renderer either
|
||||
* fails or returns an endpoint that nothing can ever deliver to, leaving the
|
||||
* `push_subscriptions` table empty. Calling Electron's main-process
|
||||
* Notification API directly side-steps the push service entirely.
|
||||
*
|
||||
* Returns true when a notification was actually shown.
|
||||
*
|
||||
* @param {string} title
|
||||
* @param {string} body
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function showNativeNotificationIfElectron(title, body) {
|
||||
if (!process.versions || !process.versions.electron) return false;
|
||||
try {
|
||||
// `require("electron")` only resolves inside the Electron runtime; in a
|
||||
// plain `node server/index.js` host it throws and we fall through.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { Notification: ElectronNotification } = require("electron");
|
||||
if (!ElectronNotification) return false;
|
||||
if (
|
||||
typeof ElectronNotification.isSupported === "function" &&
|
||||
!ElectronNotification.isSupported()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
new ElectronNotification({ title, body, silent: false }).show();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a notification to every reachable surface:
|
||||
* - A native Electron notification when hosted inside the desktop app.
|
||||
* - A Web Push delivery to every subscribed browser endpoint.
|
||||
*
|
||||
* Both legs run unconditionally so whichever surface the user is on receives
|
||||
* the alert. Under `npm start` the native leg is a no-op; under the desktop
|
||||
* app the Web Push leg is typically a no-op (no FCM credentials in Electron,
|
||||
* so `push_subscriptions` is empty).
|
||||
*
|
||||
* Returns `{ native, pushed, failed }` so the caller can surface what actually
|
||||
* happened in its API response — silent failures stop looking like success.
|
||||
*/
|
||||
async function sendPushToAll(db, title, body) {
|
||||
const native = showNativeNotificationIfElectron(title, body);
|
||||
|
||||
const subscriptions = db.prepare("SELECT * FROM push_subscriptions").all();
|
||||
if (subscriptions.length === 0) {
|
||||
return { native, pushed: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title,
|
||||
body,
|
||||
icon: "https://raw.githubusercontent.com/Smartgift-AI/Claude-Code-Monitor/main/client/public/favicon.ico",
|
||||
badge:
|
||||
"https://raw.githubusercontent.com/Smartgift-AI/Claude-Code-Monitor/main/client/public/favicon.ico",
|
||||
silent: false,
|
||||
sound: "default",
|
||||
});
|
||||
const results = await Promise.allSettled(
|
||||
subscriptions.map((sub) =>
|
||||
webpush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
payload
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Remove subscriptions that are gone (HTTP 410); count what landed.
|
||||
let pushed = 0;
|
||||
let failed = 0;
|
||||
for (let index = 0; index < results.length; index++) {
|
||||
const result = results[index];
|
||||
if (result.status === "fulfilled") {
|
||||
pushed++;
|
||||
} else {
|
||||
failed++;
|
||||
if (result.reason?.statusCode === 410) {
|
||||
db.prepare("DELETE FROM push_subscriptions WHERE endpoint = ?").run(
|
||||
subscriptions[index].endpoint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { native, pushed, failed };
|
||||
}
|
||||
|
||||
module.exports = { getPublicKey, sendPushToAll, showNativeNotificationIfElectron };
|
||||
Reference in New Issue
Block a user