import { app } from "../../scripts/app.js"; // Bar chart with nodes icon const STATS_ICON = ` `; app.registerExtension({ name: "comfyui.nodes_stats", async setup() { try { const { ComfyButton } = await import( "../../scripts/ui/components/button.js" ); const btn = new ComfyButton({ icon: "bar-chart-2", content: "Node Stats", tooltip: "Show node and package usage statistics", action: () => showStatsDialog(), classList: "comfyui-button comfyui-menu-mobile-collapse", }); app.menu?.settingsGroup.element.before(btn.element); } catch (e) { console.log( "[nodes-stats] New menu API unavailable, falling back to legacy menu", e ); const btn = document.createElement("button"); btn.innerHTML = STATS_ICON; btn.title = "Node Stats"; btn.onclick = () => showStatsDialog(); btn.style.cssText = "display:flex;align-items:center;justify-content:center;padding:6px;background:none;border:none;cursor:pointer;color:var(--input-text,#ddd);"; 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"); 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"); } 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" ? "▶" : "▼"; } }); }); } 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" }, }; function buildTable(packages, status) { const { bg: bgColor, hover: hoverColor } = STATUS_COLORS[status] || STATUS_COLORS.used; let 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 += ``; if (hasNodes) { html += ``; } } html += `
Package Nodes Used Executions Last Used
${hasNodes ? "▶" : " "} ${escapeHtml(pkg.package)} ${pkg.total_nodes} ${pkg.used_nodes}/${pkg.total_nodes} ${pkg.total_executions} ${lastSeen}
`; return html; } function escapeHtml(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML; }