+
+ ${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: