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 neverUsed = data.filter( (p) => p.never_used && p.package !== "__builtin__" ); const used = data.filter( (p) => !p.never_used && p.package !== "__builtin__" ); let html = `

Node Package Stats

`; html += `
${neverUsed.length} never used
${used.length} used
`; if (neverUsed.length > 0) { html += `

Never Used — Safe to Remove

`; html += buildTable(neverUsed, true); } if (used.length > 0) { html += `

Used Packages

`; html += buildTable(used, false); } 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 buildTable(packages, isNeverUsed) { const bgColor = isNeverUsed ? "#2a1515" : "#151a15"; const hoverColor = isNeverUsed ? "#3a2020" : "#202a20"; 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; }