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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h2 style="margin:0;color:#fff;font-size:18px;">Node Package Stats</h2>
|
||||
<button id="nodes-stats-close" style="background:none;border:none;color:#888;font-size:20px;cursor:pointer;">×</button>
|
||||
</div>`;
|
||||
|
||||
html += `<div style="display:flex;gap:16px;margin-bottom:20px;">
|
||||
<div style="background:#3a1a1a;padding:8px 16px;border-radius:4px;border-left:3px solid #e44;">
|
||||
<span style="font-size:22px;font-weight:bold;color:#e44;">${neverUsed.length}</span>
|
||||
<span style="color:#c99;margin-left:6px;">never used</span>
|
||||
html += `<div style="display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap;">
|
||||
<div style="background:#3a1a1a;padding:8px 14px;border-radius:4px;border-left:3px solid #e44;">
|
||||
<span style="font-size:20px;font-weight:bold;color:#e44;">${safeToRemove.length}</span>
|
||||
<span style="color:#c99;margin-left:6px;">safe to remove</span>
|
||||
</div>
|
||||
<div style="background:#1a2a1a;padding:8px 16px;border-radius:4px;border-left:3px solid #4a4;">
|
||||
<span style="font-size:22px;font-weight:bold;color:#4a4;">${used.length}</span>
|
||||
<div style="background:#2a2215;padding:8px 14px;border-radius:4px;border-left:3px solid #e90;">
|
||||
<span style="font-size:20px;font-weight:bold;color:#e90;">${considerRemoving.length}</span>
|
||||
<span style="color:#ca8;margin-left:6px;">consider removing</span>
|
||||
</div>
|
||||
<div style="background:#1a1a2a;padding:8px 14px;border-radius:4px;border-left:3px solid #68f;">
|
||||
<span style="font-size:20px;font-weight:bold;color:#68f;">${unusedNew.length}</span>
|
||||
<span style="color:#99b;margin-left:6px;">unused <1 month</span>
|
||||
</div>
|
||||
<div style="background:#1a2a1a;padding:8px 14px;border-radius:4px;border-left:3px solid #4a4;">
|
||||
<span style="font-size:20px;font-weight:bold;color:#4a4;">${used.length}</span>
|
||||
<span style="color:#9c9;margin-left:6px;">used</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (neverUsed.length > 0) {
|
||||
html += `<h3 style="color:#e44;margin:12px 0 8px;font-size:14px;">Never Used — Safe to Remove</h3>`;
|
||||
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 += `<h3 style="color:#4a4;margin:16px 0 8px;font-size:14px;">Used Packages</h3>`;
|
||||
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 = `<h3 style="color:${color};margin:16px 0 8px;font-size:14px;">${title}`;
|
||||
if (subtitle) html += ` <span style="color:#666;font-size:12px;font-weight:normal;">— ${subtitle}</span>`;
|
||||
html += `</h3>`;
|
||||
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 = `<table style="width:100%;border-collapse:collapse;margin-bottom:12px;">
|
||||
<thead><tr style="color:#888;text-align:left;border-bottom:1px solid #333;">
|
||||
|
||||
42
tracker.py
42
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:
|
||||
|
||||
Reference in New Issue
Block a user