import { app } from "../../scripts/app.js";
// Bar chart with nodes icon
const STATS_ICON = ``;
app.registerExtension({
name: "comfyui.nodes_stats",
async setup() {
const btn = document.createElement("button");
btn.innerHTML = STATS_ICON;
btn.title = "Node Stats";
btn.className = "comfyui-button comfyui-menu-mobile-collapse";
btn.onclick = () => showStatsDialog();
btn.style.cssText =
"display:flex;align-items:center;justify-content:center;padding:6px;cursor:pointer;";
if (app.menu?.settingsGroup?.element) {
app.menu.settingsGroup.element.before(btn);
} else {
const menu = document.querySelector(".comfy-menu");
if (menu) {
menu.append(btn);
}
}
},
});
async function showStatsDialog() {
let data;
try {
const resp = await fetch("/nodes-stats/packages");
if (!resp.ok) {
alert("Failed to load node stats: HTTP " + resp.status);
return;
}
data = await resp.json();
if (!Array.isArray(data)) {
alert("Failed to load node stats: unexpected response format");
return;
}
} catch (e) {
alert("Failed to load node stats: " + e.message);
return;
}
// Remove existing dialog if any
const existing = document.getElementById("nodes-stats-dialog");
if (existing) existing.remove();
const overlay = document.createElement("div");
overlay.id = "nodes-stats-dialog";
overlay.style.cssText =
"position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center;";
overlay.addEventListener("click", (e) => {
if (e.target === overlay) overlay.remove();
});
const dialog = document.createElement("div");
dialog.style.cssText =
"background:#1e1e1e;color:#ddd;border-radius:8px;padding:24px;max-width:800px;width:90%;max-height:85vh;overflow-y:auto;font-family:monospace;font-size:13px;";
const custom = data.filter((p) => p.package !== "__builtin__");
const safeToRemove = custom.filter((p) => p.status === "safe_to_remove");
const considerRemoving = custom.filter((p) => p.status === "consider_removing");
const unusedNew = custom.filter((p) => p.status === "unused_new");
const used = custom.filter((p) => p.status === "used");
const uninstalled = custom.filter((p) => p.status === "uninstalled");
let html = `
Node Package Stats
`;
html += `
${safeToRemove.length}
safe to remove
${considerRemoving.length}
consider removing
${unusedNew.length}
unused <1 month
${used.length}
used
`;
if (safeToRemove.length > 0) {
html += sectionHeader("Safe to Remove", "Unused for 2+ months", "#e44");
html += buildTable(safeToRemove, "safe_to_remove");
}
if (considerRemoving.length > 0) {
html += sectionHeader("Consider Removing", "Unused for 1-2 months", "#e90");
html += buildTable(considerRemoving, "consider_removing");
}
if (unusedNew.length > 0) {
html += sectionHeader("Recently Unused", "Unused for less than 1 month", "#68f");
html += buildTable(unusedNew, "unused_new");
}
if (used.length > 0) {
html += sectionHeader("Used", "", "#4a4");
html += buildTable(used, "used");
}
if (uninstalled.length > 0) {
html += sectionHeader("Uninstalled", "Previously tracked, no longer installed", "#555");
html += buildTable(uninstalled, "uninstalled");
}
dialog.innerHTML = html;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
document
.getElementById("nodes-stats-close")
.addEventListener("click", () => overlay.remove());
// Toggle expandable rows
dialog.querySelectorAll(".pkg-row").forEach((row) => {
row.addEventListener("click", () => {
const detail = row.nextElementSibling;
if (detail && detail.classList.contains("pkg-detail")) {
detail.style.display =
detail.style.display === "none" ? "table-row" : "none";
const arrow = row.querySelector(".arrow");
if (arrow)
arrow.textContent = detail.style.display === "none" ? "▶" : "▼";
}
});
});
// Easter egg: click "used" badge 5 times to show podium
let eggClicks = 0;
let eggTimer = null;
const usedBadge = document.getElementById("nodes-stats-used-badge");
if (usedBadge) {
usedBadge.addEventListener("click", () => {
eggClicks++;
clearTimeout(eggTimer);
eggTimer = setTimeout(() => (eggClicks = 0), 1500);
if (eggClicks >= 5) {
eggClicks = 0;
const allNodes = custom
.flatMap((p) => p.nodes.map((n) => ({ ...n, pkg: p.package })))
.sort((a, b) => b.count - a.count);
showPodium(allNodes.slice(0, 3), overlay);
}
});
}
}
function sectionHeader(title, subtitle, color) {
let html = `${title}`;
if (subtitle) html += ` — ${subtitle}`;
html += `
`;
return html;
}
const STATUS_COLORS = {
safe_to_remove: { bg: "#2a1515", hover: "#3a2020" },
consider_removing: { bg: "#2a2215", hover: "#3a2e20" },
unused_new: { bg: "#1a1a25", hover: "#252530" },
used: { bg: "#151a15", hover: "#202a20" },
uninstalled: { bg: "#1a1a1a", hover: "#252525" },
};
function buildTable(packages, status) {
const { bg: bgColor, hover: hoverColor } = STATUS_COLORS[status] || STATUS_COLORS.used;
let html = `
|
Package |
Nodes |
Used |
Executions |
Last Used |
`;
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 (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;
}
// 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 += ``;
// 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;
}