4f84d2d7e2
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.
96 lines
3.2 KiB
JavaScript
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 };
|