From bf5c8683a77c42d3a10194f152a48adf5b148e7b Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 9 Apr 2026 18:11:52 +0200 Subject: [PATCH] feat: add Models tab with per-type usage stats --- js/nodes_stats.js | 216 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 159 insertions(+), 57 deletions(-) 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 = `

Node Package Stats

`; - 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 += ``; 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 = ` + + + + + + `; + + 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 += ` + + + + + `; + } + + html += `
ModelExecutionsLast UsedStatus
${escapeHtml(m.model_name)}${m.count}${lastSeen}${statusLabel.text}
`; + return html; +} + function sectionHeader(title, subtitle, color) { let html = `

${title}`; if (subtitle) html += ` — ${subtitle}`;