Files
Claude-Code-Monitor/server/update-scheduler.js
T
nntrivi2001 4f84d2d7e2 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.
2026-07-30 13:49:16 +07:00

96 lines
3.2 KiB
JavaScript

/**
* @file Periodic git upstream check and WebSocket broadcast when update availability changes.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { getUpdatesStatus } = require("./lib/update-check");
function isUpdateCheckDisabled() {
const v = process.env.DASHBOARD_UPDATE_CHECK;
return v === "0" || v === "false" || v === "off";
}
function intervalMs() {
const n = Number.parseInt(process.env.DASHBOARD_UPDATE_CHECK_INTERVAL_MS || "", 10);
if (Number.isFinite(n) && n >= 60_000) return n;
return 5 * 60 * 1000;
}
function startUpdateScheduler({ broadcast }) {
if (isUpdateCheckDisabled()) return { stop: () => {} };
let lastFingerprint = "";
let lastHadUpdate = false;
let stopped = false;
let initialTimer = null;
let intervalTimer = null;
async function tick() {
if (stopped) return;
try {
const status = await getUpdatesStatus();
// Fingerprint must change whenever anything user-visible changes,
// including the *shape* of manual_command (e.g. user switches branch
// or adds an upstream remote — same remote_sha / commits_behind, but
// different command). Including manual_command here covers situation
// transitions automatically.
const fp = JSON.stringify({
a: Boolean(status.update_available),
r: status.remote_sha || null,
b: status.commits_behind || 0,
e: status.fetch_error || null,
c: status.manual_command || null,
});
const changed = fp !== lastFingerprint;
lastFingerprint = fp;
if (changed) {
broadcast("update_status", status);
}
const becameAvailable = status.update_available && !lastHadUpdate;
lastHadUpdate = Boolean(status.update_available);
if (becameAvailable) {
const line = "━".repeat(52);
console.log(`\n${line}`);
console.log(" Agent Dashboard: upstream update available");
console.log(` ${status.message || ""}`);
if (status.situation_note) {
console.log(` ${status.situation_note}`);
}
if (status.manual_command) {
console.log(` Run: ${status.manual_command}`);
// Only suggest restart when the command actually rewrites the
// working tree. fetch-only situations (feature_branch /
// detached_head) don't need a restart.
const updatesWorkingTree =
status.situation === "tracking_canonical" ||
status.situation === "fork_or_diverged_tracking";
if (updatesWorkingTree) {
console.log(" Then restart the dashboard the same way you started it.");
}
}
console.log(`${line}\n`);
}
} catch {
// Non-fatal — never block the server on update checks
}
}
initialTimer = setTimeout(() => {
tick();
}, 8_000);
if (typeof initialTimer.unref === "function") initialTimer.unref();
intervalTimer = setInterval(tick, intervalMs());
if (typeof intervalTimer.unref === "function") intervalTimer.unref();
return {
stop: () => {
stopped = true;
if (initialTimer) clearTimeout(initialTimer);
if (intervalTimer) clearInterval(intervalTimer);
},
};
}
module.exports = { startUpdateScheduler };