From 17a27ed5b216bce8ff0fc2d6f8695f679f17c231 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 22 Feb 2026 13:58:12 +0100 Subject: [PATCH] Add 4-tier package classification by usage recency Packages are now classified as: - Used: actively used - Recently Unused (<1 month): too early to judge - Consider Removing (1-2 months unused): deletion suggestion - Safe to Remove (2+ months unused): confident removal candidate Never-used packages are classified based on how long tracking has been active. Summary bar and sections are color-coded. Co-Authored-By: Claude Opus 4.6 --- js/nodes_stats.js | 70 +++++++++++++++++++++++++++++++++-------------- tracker.py | 42 ++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/js/nodes_stats.js b/js/nodes_stats.js index 169e57b..a8dc341 100644 --- a/js/nodes_stats.js +++ b/js/nodes_stats.js @@ -83,37 +83,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 neverUsed = data.filter( - (p) => p.never_used && p.package !== "__builtin__" - ); - const used = data.filter( - (p) => !p.never_used && p.package !== "__builtin__" - ); + 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 += `
-
- ${neverUsed.length} - never used + html += `
+
+ ${safeToRemove.length} + safe to remove
-
- ${used.length} +
+ ${considerRemoving.length} + consider removing +
+
+ ${unusedNew.length} + unused <1 month +
+
+ ${used.length} used
`; - if (neverUsed.length > 0) { - html += `

Never Used — Safe to Remove

`; - html += buildTable(neverUsed, true); + 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 += `

Used Packages

`; - html += buildTable(used, false); + html += sectionHeader("Used", "", "#4a4"); + html += buildTable(used, "used"); } dialog.innerHTML = html; @@ -139,9 +156,22 @@ async function showStatsDialog() { }); } -function buildTable(packages, isNeverUsed) { - const bgColor = isNeverUsed ? "#2a1515" : "#151a15"; - const hoverColor = isNeverUsed ? "#3a2020" : "#202a20"; +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 = ` diff --git a/tracker.py b/tracker.py index e392d91..ae727af 100644 --- a/tracker.py +++ b/tracker.py @@ -3,7 +3,7 @@ import logging import os import sqlite3 import threading -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta logger = logging.getLogger(__name__) @@ -129,17 +129,53 @@ class UsageTracker: "last_seen": None, } packages[pkg]["total_nodes"] = total - packages[pkg]["never_used"] = packages[pkg]["total_executions"] == 0 # For packages that came only from DB (e.g. uninstalled), fill total_nodes for pkg, entry in packages.items(): if "total_nodes" not in entry: entry["total_nodes"] = entry["used_nodes"] - entry["never_used"] = False + + # Classify packages by usage recency + now = datetime.now(timezone.utc) + one_month_ago = (now - timedelta(days=30)).isoformat() + two_months_ago = (now - timedelta(days=60)).isoformat() + tracking_start = self._get_first_prompt_time() + + for entry in packages.values(): + if entry["total_executions"] > 0: + # Used packages: classify by last_seen recency + if entry["last_seen"] < two_months_ago: + entry["status"] = "safe_to_remove" + elif entry["last_seen"] < one_month_ago: + entry["status"] = "consider_removing" + else: + entry["status"] = "used" + else: + # Never-used packages: classify by how long we've been tracking + if tracking_start is None: + entry["status"] = "unused_new" + elif tracking_start < two_months_ago: + entry["status"] = "safe_to_remove" + elif tracking_start < one_month_ago: + entry["status"] = "consider_removing" + else: + entry["status"] = "unused_new" result = sorted(packages.values(), key=lambda p: p["total_executions"]) return result + def _get_first_prompt_time(self): + """Return the timestamp of the earliest recorded prompt, or None.""" + with self._lock: + conn = self._connect() + try: + row = conn.execute( + "SELECT MIN(timestamp) FROM prompt_log" + ).fetchone() + return row[0] if row and row[0] else None + finally: + conn.close() + def reset(self): """Clear all tracked data.""" with self._lock: