diff --git a/js/nodes_stats.js b/js/nodes_stats.js
index 0c066e5..3bd3a3c 100644
--- a/js/nodes_stats.js
+++ b/js/nodes_stats.js
@@ -34,23 +34,27 @@ app.registerExtension({
});
async function showStatsDialog() {
- let data;
+ let data, modelData;
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");
+ const [pkgResp, modelResp] = await Promise.all([
+ fetch("/nodes-stats/packages"),
+ fetch("/nodes-stats/models"),
+ ]);
+ if (!pkgResp.ok) { alert("Failed to load node stats: HTTP " + pkgResp.status); return; }
+ if (!modelResp.ok) { alert("Failed to load model stats: HTTP " + modelResp.status); return; }
+ data = await pkgResp.json();
+ modelData = await modelResp.json();
+ if (!Array.isArray(data) || !Array.isArray(modelData)) {
+ alert("Failed to load stats: unexpected response format");
return;
}
} catch (e) {
- alert("Failed to load node stats: " + e.message);
+ alert("Failed to load stats: " + e.message);
return;
}
+ const custom = data.filter((p) => p.package !== "__builtin__");
+
// Remove existing dialog if any
const existing = document.getElementById("nodes-stats-dialog");
if (existing) existing.remove();
@@ -67,66 +71,54 @@ async function showStatsDialog() {
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 = `
-
- ${safeToRemove.length}
- safe to remove
-
-
- ${considerRemoving.length}
- consider removing
-
-
- ${unusedNew.length}
- unused <1 month
-
-
- ${used.length}
- used
-
+ // Tab switcher — wired via addEventListener after insertion, no onclick globals
+ html += `
+
+
+
`;
- if (safeToRemove.length > 0) {
- html += sectionHeader("Safe to Remove", "Unused for 2+ months", "#e44");
- html += buildTable(safeToRemove, "safe_to_remove");
- }
+ // Nodes tab content (existing content, wrapped)
+ html += `
`;
+ html += buildNodesTabContent(custom);
+ html += `
`;
- 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");
- }
+ // Models tab content
+ html += `
`;
+ html += buildModelsTabContent(modelData);
+ html += `
`;
dialog.innerHTML = html;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
+ // Tab switch — local function, no window pollution
+ function switchTab(tab) {
+ dialog.querySelector("#ns-content-nodes").style.display = tab === "nodes" ? "" : "none";
+ dialog.querySelector("#ns-content-models").style.display = tab === "models" ? "" : "none";
+ const nodeBtn = dialog.querySelector("#ns-tab-nodes");
+ const modelBtn = dialog.querySelector("#ns-tab-models");
+ nodeBtn.style.borderBottomColor = tab === "nodes" ? "#4a4" : "transparent";
+ nodeBtn.style.color = tab === "nodes" ? "#4a4" : "#888";
+ nodeBtn.style.fontWeight = tab === "nodes" ? "bold" : "normal";
+ modelBtn.style.borderBottomColor = tab === "models" ? "#4a4" : "transparent";
+ modelBtn.style.color = tab === "models" ? "#4a4" : "#888";
+ modelBtn.style.fontWeight = tab === "models" ? "bold" : "normal";
+ }
+ dialog.querySelector("#ns-tab-nodes").addEventListener("click", () => switchTab("nodes"));
+ dialog.querySelector("#ns-tab-models").addEventListener("click", () => switchTab("models"));
+
document
.getElementById("nodes-stats-close")
.addEventListener("click", () => overlay.remove());
@@ -165,6 +157,116 @@ async function showStatsDialog() {
}
}
+function buildNodesTabContent(custom) {
+ 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 = `
+
+ ${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") + buildTable(safeToRemove, "safe_to_remove");
+ if (considerRemoving.length > 0) html += sectionHeader("Consider Removing", "Unused for 1-2 months", "#e90") + buildTable(considerRemoving, "consider_removing");
+ if (unusedNew.length > 0) html += sectionHeader("Recently Unused", "Unused for less than 1 month", "#68f") + buildTable(unusedNew, "unused_new");
+ if (used.length > 0) html += sectionHeader("Used", "", "#4a4") + buildTable(used, "used");
+ if (uninstalled.length > 0) html += sectionHeader("Uninstalled", "Previously tracked, no longer installed", "#555") + buildTable(uninstalled, "uninstalled");
+
+ return html;
+}
+
+function buildModelsTabContent(modelData) {
+ // Flatten for summary counts
+ const allModels = modelData.flatMap((g) => g.models);
+ const safeCount = allModels.filter((m) => m.status === "safe_to_remove").length;
+ const considerCount = allModels.filter((m) => m.status === "consider_removing").length;
+ const unusedNewCount = allModels.filter((m) => m.status === "unused_new").length;
+ const usedCount = allModels.filter((m) => m.status === "used").length;
+
+ let html = `
+
+ ${safeCount}
+ safe to remove
+
+
+ ${considerCount}
+ consider removing
+
+
+ ${unusedNewCount}
+ unused <1 month
+
+
+ ${usedCount}
+ 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 { bg, hover } = STATUS_COLORS[m.status] || STATUS_COLORS.used;
+ const lastSeen = m.last_seen ? new Date(m.last_seen).toLocaleDateString() : "—";
+ const statusLabel = {
+ safe_to_remove: { text: "safe to remove", color: "#e44" },
+ consider_removing: { text: "consider removing", color: "#e90" },
+ unused_new: { text: "unused <1mo", color: "#68f" },
+ used: { text: "used", color: "#4a4" },
+ uninstalled: { text: "uninstalled", color: "#555" },
+ }[m.status] || { text: m.status, color: "#888" };
+
+ html += `
+ | ${escapeHtml(m.model_name)} |
+ ${m.count} |
+ ${lastSeen} |
+ ${statusLabel.text} |
+
`;
+ }
+
+ html += `
`;
+ return html;
+}
+
function sectionHeader(title, subtitle, color) {
let html = `
${title}`;
if (subtitle) html += ` — ${subtitle}`;