`;
for (const it of items) {
const m = STATUS_META[it.status];
const idAttr = it.id ? ` id="${it.id}"` : "";
const cursor = it.id ? "cursor:default;user-select:none;" : "";
html += `
${it.count}${it.label}
`;
}
html += `
`;
return html;
}
function buildNodesTabContent(custom, managerInfo) {
const byStatus = (s) => custom.filter((p) => p.status === s);
const safeToRemove = byStatus("safe_to_remove");
const considerRemoving = byStatus("consider_removing");
const unusedNew = byStatus("unused_new");
const used = byStatus("used");
const uninstalled = byStatus("uninstalled");
let html = summaryBar([
{ count: safeToRemove.length, status: "safe_to_remove", label: "safe to remove" },
{ count: considerRemoving.length, status: "consider_removing", label: "consider removing" },
{ count: unusedNew.length, status: "unused_new", label: "unused <1 month" },
{ count: used.length, status: "used", label: "used", id: "nodes-stats-used-badge" },
]);
html += renderSection("Safe to Remove", "Unused for 2+ months", "safe_to_remove", safeToRemove, managerInfo);
html += renderSection("Consider Removing", "Unused for 1-2 months", "consider_removing", considerRemoving, managerInfo);
html += renderSection("Recently Unused", "Unused for less than 1 month", "unused_new", unusedNew, managerInfo);
html += renderSection("Used", "", "used", used, managerInfo);
html += renderSection("Uninstalled", "Previously tracked, no longer installed", "uninstalled", uninstalled, managerInfo);
return html;
}
function renderSection(title, subtitle, status, packages, managerInfo) {
if (packages.length === 0) return "";
const color = STATUS_META[status].color;
const withActions = !!managerInfo && DISABLEABLE_TIERS.has(status);
const eligible = withActions
? packages.filter((p) => isDisableEligible(p, managerInfo)).map((p) => p.package)
: [];
let action = "";
if (eligible.length > 0) {
action = ``;
}
let html = `
${escapeHtml(title)}`;
if (subtitle) html += ` — ${escapeHtml(subtitle)}`;
html += `
${action}
`;
html += buildTable(packages, status, withActions, managerInfo);
return html;
}
// A package can be disabled only if ComfyUI Manager knows it (by directory
// name) and it is currently enabled (active on disk).
function isDisableEligible(pkg, managerInfo) {
if (!managerInfo || !pkg.installed) return false;
const info = managerInfo[pkg.package];
return !!(info && info.enabled);
}
function buildModelsTabContent(modelData) {
const allModels = modelData.flatMap((g) => g.models);
const count = (s) => allModels.filter((m) => m.status === s).length;
let html = summaryBar([
{ count: count("safe_to_remove"), status: "safe_to_remove", label: "safe to remove" },
{ count: count("consider_removing"), status: "consider_removing", label: "consider removing" },
{ count: count("unused_new"), status: "unused_new", label: "unused <1 month" },
{ count: count("used"), status: "used", label: "used" },
]);
if (allModels.length === 0) {
html += `
No models tracked yet. Run a workflow to start.
`;
return html;
}
for (const group of modelData) {
if (group.models.length === 0) continue;
const title = group.model_type.charAt(0).toUpperCase() + group.model_type.slice(1).replace(/_/g, " ");
html += sectionHeader(title, `${group.models.length} model${group.models.length !== 1 ? "s" : ""}`, "#4a4");
html += buildModelTable(group.models);
}
return html;
}
function buildModelTable(models) {
let html = `
Model
Executions
Last Used
Status
`;
for (const m of models) {
const meta = STATUS_META[m.status] || STATUS_META.used;
const lastSeen = m.last_seen ? new Date(m.last_seen).toLocaleDateString() : "—";
html += `
${escapeHtml(m.model_name)}
${m.count}
${lastSeen}
${meta.label}
`;
}
html += `
`;
return html;
}
function sectionHeader(title, subtitle, color) {
let html = `
${escapeHtml(title)}`;
if (subtitle) html += ` — ${escapeHtml(subtitle)}`;
html += `
`;
return html;
}
function buildTable(packages, status, withActions, managerInfo) {
const colspan = withActions ? 7 : 6;
let html = `
Package
Nodes
Used
Executions
Last Used
`;
if (withActions) html += `
`;
html += `
`;
for (const pkg of packages) {
const hasNodes = pkg.nodes && pkg.nodes.length > 0;
const lastSeen = pkg.last_seen ? new Date(pkg.last_seen).toLocaleDateString() : "—";
html += `
${hasNodes ? "▶" : " "}
${escapeHtml(pkg.package)}
${pkg.total_nodes}
${pkg.used_nodes}/${pkg.total_nodes}
${pkg.total_executions}
${lastSeen}
`;
if (withActions) {
const eligible = isDisableEligible(pkg, managerInfo);
const cell = eligible
? ``
: `—`;
html += `
${cell}
`;
}
html += `
`;
if (hasNodes) {
html += `
`;
for (const node of pkg.nodes) {
const nLastSeen = node.last_seen ? new Date(node.last_seen).toLocaleDateString() : "—";
html += `
${escapeHtml(node.class_type)}
${node.count}
${nLastSeen}
`;
}
html += `
`;
}
}
html += `
`;
return html;
}
// ---------------------------------------------------------------------------
// ComfyUI Manager integration: disable unused node packages
// ---------------------------------------------------------------------------
// Map of installed packages from ComfyUI Manager:
// { : { ver, cnr_id, aux_id, enabled }, ... }
// Returns null when the Manager is not installed/reachable, in which case the
// disable UI is omitted entirely.
async function fetchManagerInfo() {
try {
const resp = await fetch("/customnode/installed");
if (!resp.ok) return null;
const data = await resp.json();
return data && typeof data === "object" ? data : null;
} catch {
return null;
}
}
// Build the payload ComfyUI Manager's /manager/queue/disable expects. CNR
// (registry) packages are keyed by their cnr_id; everything else is treated as
// "unknown" and keyed by directory name.
function disablePayload(dirName, info) {
if (info && info.cnr_id && info.ver && info.ver !== "unknown") {
return { id: info.cnr_id, version: info.ver, ui_id: dirName };
}
return { id: dirName, version: "unknown", files: [dirName], ui_id: dirName };
}
function wireDisableButtons(dialog, managerInfo) {
if (!managerInfo) return;
dialog.querySelectorAll(".ns-disable-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
handleDisable([btn.dataset.pkg], dialog, managerInfo);
});
});
dialog.querySelectorAll(".ns-disable-all-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
let names = [];
try { names = JSON.parse(btn.dataset.pkgs); } catch { names = []; }
handleDisable(names, dialog, managerInfo);
});
});
}
async function handleDisable(pkgNames, dialog, managerInfo) {
// Only act on packages Manager still reports as enabled (guards against
// double-clicks and stale buttons after a partial batch).
pkgNames = pkgNames.filter((n) => managerInfo[n] && managerInfo[n].enabled);
if (pkgNames.length === 0) return;
const what = pkgNames.length === 1 ? `"${pkgNames[0]}"` : `${pkgNames.length} packages`;
const confirmMsg =
`Disable ${what} via ComfyUI Manager?\n\n` +
`They will be moved to custom_nodes/.disabled and a ComfyUI restart is ` +
`required to take effect. You can re-enable them anytime from ComfyUI Manager.`;
if (!confirm(confirmMsg)) return;
setDisableButtonsBusy(dialog, true);
try {
const pre = await fetch("/manager/queue/status").then((r) => (r.ok ? r.json() : null)).catch(() => null);
if (pre && pre.is_processing) {
notify("ComfyUI Manager is busy. Please try again in a moment.", "warn");
setDisableButtonsBusy(dialog, false);
return;
}
const payloads = pkgNames.map((n) => disablePayload(n, managerInfo[n]));
await runManagerDisable(payloads);
// Reconcile against Manager's actual state: a package is considered
// disabled only if it's no longer reported as enabled on disk.
const after = await fetchManagerInfo();
const isStillEnabled = (n) => after && after[n] && after[n].enabled;
const succeeded = after ? pkgNames.filter((n) => !isStillEnabled(n)) : pkgNames;
const failed = pkgNames.filter((n) => !succeeded.includes(n));
succeeded.forEach((n) => { if (managerInfo[n]) managerInfo[n].enabled = false; });
markPackagesDisabled(dialog, succeeded);
updateBulkButtons(dialog, managerInfo);
if (succeeded.length > 0) {
showRestartBanner(dialog);
notify(`Disabled ${succeeded.length} package${succeeded.length !== 1 ? "s" : ""}. Restart ComfyUI to apply.`, "success");
}
if (failed.length > 0) {
notify(`ComfyUI Manager could not disable: ${failed.join(", ")}`, "error");
}
} catch (e) {
notify("Failed to disable: " + e.message, "error");
} finally {
setDisableButtonsBusy(dialog, false);
}
}
// Queue the disable tasks and run them, then wait for the Manager worker to
// finish. /manager/queue/start returns 201 if a worker is already running.
async function runManagerDisable(payloads) {
await fetch("/manager/queue/reset", { method: "POST", headers: { "Content-Type": "application/json" } });
for (const payload of payloads) {
const r = await fetch("/manager/queue/disable", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`disable request failed (HTTP ${r.status})`);
}
const start = await fetch("/manager/queue/start", { method: "POST", headers: { "Content-Type": "application/json" } });
if (!start.ok && start.status !== 201) throw new Error(`queue start failed (HTTP ${start.status})`);
await waitForQueue();
}
async function waitForQueue(timeoutMs = 60000) {
const deadline = Date.now() + timeoutMs;
await sleep(300);
while (Date.now() < deadline) {
let st = null;
try {
const r = await fetch("/manager/queue/status");
if (r.ok) st = await r.json();
} catch { /* transient; retry */ }
if (st && !st.is_processing && st.in_progress_count === 0) return;
await sleep(500);
}
throw new Error("timed out waiting for ComfyUI Manager");
}
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
function setDisableButtonsBusy(dialog, busy) {
dialog.querySelectorAll(".ns-disable-btn, .ns-disable-all-btn").forEach((b) => {
b.disabled = busy;
});
}
function markPackagesDisabled(dialog, pkgNames) {
for (const name of pkgNames) {
const cell = dialog.querySelector(`.ns-action-cell[data-pkg="${cssEscape(name)}"]`);
if (cell) {
cell.innerHTML = `✓ disabled · restart`;
cell.closest("tr")?.classList.add("ns-disabled-row");
}
}
}
// Recompute "Disable all (N)" counts after a batch; hide buttons with nothing
// left to disable.
function updateBulkButtons(dialog, managerInfo) {
dialog.querySelectorAll(".ns-disable-all-btn").forEach((btn) => {
let names = [];
try { names = JSON.parse(btn.dataset.pkgs); } catch { names = []; }
const remaining = names.filter((n) => managerInfo[n] && managerInfo[n].enabled);
if (remaining.length === 0) {
btn.style.display = "none";
} else {
btn.dataset.pkgs = JSON.stringify(remaining);
btn.textContent = `Disable all (${remaining.length})`;
}
});
}
function showRestartBanner(dialog) {
if (dialog.querySelector("#ns-restart-banner")) return;
const banner = document.createElement("div");
banner.id = "ns-restart-banner";
banner.style.cssText =
"display:flex;align-items:center;justify-content:space-between;gap:12px;background:#2a2215;border:1px solid #a83;border-radius:4px;padding:10px 14px;margin-bottom:16px;";
banner.innerHTML =
`Changes applied on disk. Restart ComfyUI to unload disabled packages.`;
const tabs = dialog.querySelector("#ns-tabs");
tabs ? tabs.before(banner) : dialog.prepend(banner);
banner.querySelector("#ns-restart-btn").addEventListener("click", rebootComfy);
banner.querySelector("#ns-restart-dismiss").addEventListener("click", () => banner.remove());
}
async function rebootComfy() {
if (!confirm("Restart ComfyUI now? The server will go down briefly and the page will reconnect.")) return;
notify("Restarting ComfyUI…", "info");
try {
await fetch("/manager/reboot", { method: "POST", headers: { "Content-Type": "application/json" } });
} catch {
// The reboot tears down the connection, so a network error here is expected.
}
}
function notify(detail, severity) {
try {
const toast = app?.extensionManager?.toast;
if (toast && typeof toast.add === "function") {
toast.add({ severity: severity === "warn" ? "warn" : severity, summary: "Node Stats", detail, life: 5000 });
return;
}
} catch { /* fall through to console/alert */ }
if (severity === "error") alert(detail);
else console.log("[Node Stats] " + detail);
}
// ---------------------------------------------------------------------------
// Easter egg
// ---------------------------------------------------------------------------
// Internal: builds celebratory overlay for top contributors
function showPodium(top3, overlay) {
const existing = document.getElementById("nodes-stats-podium");
if (existing) { existing.remove(); return; }
const colors = ["#FFD700", "#C0C0C0", "#CD7F32"];
const heights = [160, 120, 90];
const order = [1, 0, 2];
// SVG characters: champion with cape, cool runner-up, happy bronze
const characters = [
// Gold: flexing champion with crown and cape
``,
// Silver: sunglasses dude, arms crossed
``,
// Bronze: happy little guy waving
``,
];
const podium = document.createElement("div");
podium.id = "nodes-stats-podium";
podium.style.cssText =
"position:absolute;top:0;left:0;width:100%;height:100%;background:radial-gradient(ellipse at center,#1a1a2e 0%,#0a0a12 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px;z-index:1;cursor:pointer;overflow:hidden;";
podium.addEventListener("click", () => podium.remove());
// Sparkle particles
let sparkles = "";
for (let i = 0; i < 20; i++) {
const x = Math.random() * 100;
const y = Math.random() * 60;
const d = (1 + Math.random() * 2).toFixed(1);
const o = (0.3 + Math.random() * 0.7).toFixed(2);
sparkles += ``;
}
let html = ``;
html += sparkles;
// Trophy title
html += `
Hall of Fame
`;
// Podium blocks
html += `
`;
for (const i of order) {
const node = top3[i];
if (!node) continue;
const isGold = i === 0;
const w = isGold ? 170 : 140;
const floatDelay = [0, 0.3, 0.6][i];
html += `
${characters[i]}
${escapeHtml(node.class_type)}
${escapeHtml(node.pkg)}
${i + 1}${["st","nd","rd"][i]}
${node.count.toLocaleString()}x
`;
}
html += `
`;
html += `
click to dismiss
`;
podium.innerHTML = html;
overlay.querySelector("div").style.position = "relative";
overlay.querySelector("div").appendChild(podium);
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// Escape a value for use inside a double-quoted HTML attribute.
function escapeAttr(str) {
return escapeHtml(str).replace(/"/g, """);
}
// Escape a string for use in a CSS attribute selector.
function cssEscape(str) {
return window.CSS && CSS.escape ? CSS.escape(str) : String(str).replace(/["\\]/g, "\\$&");
}