fix: keep disabled unused packages manageable

This commit is contained in:
2026-07-29 19:30:54 +02:00
parent 6d433ba371
commit c43c8dde79
5 changed files with 100 additions and 17 deletions
+13 -9
View File
@@ -67,27 +67,31 @@ Click the **Node Stats** button (bar chart icon) in the ComfyUI top menu bar. A
- Summary bar with counts for each classification tier
- Sections for each tier, sorted from most actionable to least
- Expandable rows — click any package to see per-node execution counts and timestamps
- **Disable** buttons on the "Safe to Remove" and "Consider Removing" tiers (see below)
- **Disable** buttons on the "Safe to Remove", "Consider Removing", and "Recently Unused" tiers (see below)
### Disabling unused packages
When [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) is installed, the
"Safe to Remove" and "Consider Removing" sections show a **Disable** button on each
package, plus a **Disable all** button per section. Disabling:
The "Safe to Remove", "Consider Removing", and "Recently Unused" sections show
a **Disable** button on each package, plus a **Disable all** button per section.
The latter is useful for a package you installed but never used. Disabling:
- Hands off to ComfyUI Manager, which moves the package into `custom_nodes/.disabled/`
- Is fully reversible — re-enable any package from ComfyUI Manager whenever you like
- Uses ComfyUI Manager when it manages the package, otherwise moves it directly
into `custom_nodes/.disabled/`
- Is fully reversible — the *Uninstalled* section offers **Enable** for every
package found in `.disabled/`, even if it was never run
- Requires a ComfyUI restart to unload the package from the running session (a banner
with a **Restart ComfyUI** button appears after disabling)
If ComfyUI Manager is not installed, the disable buttons are hidden and stats work as before.
ComfyUI Manager is optional for these actions; packages it does not manage are
disabled and re-enabled directly by Node Stats.
> **Manager compatibility:** works with both the standalone
> [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) custom node and
> ComfyUI core's built-in manager. For the built-in manager, launch ComfyUI with
> `--enable-manager-legacy-ui` (the extension detects the active manager and its
> API automatically). Without a reachable manager, all enable/disable/install
> actions are simply omitted.
> API automatically). Without a reachable manager, Node Stats still supports
> direct enable/disable; Manager-only workflow install and trial actions are
> unavailable.
### Workflow tab & temporary enable
+7 -1
View File
@@ -78,7 +78,13 @@ routes = PromptServer.instance.routes
@routes.get("/nodes-stats/packages")
async def get_package_stats(request):
try:
stats = tracker.get_package_stats(mapper)
# Include entries parked in custom_nodes/.disabled even when they were
# never executed. They are otherwise absent from both the DB and the
# live node mapper, leaving no way to re-enable them in the UI.
disabled = await asyncio.get_event_loop().run_in_executor(
None, list_disabled_packs
)
stats = tracker.get_package_stats(mapper, disabled)
return web.json_response(stats)
except Exception:
logger.error("nodes-stats: error getting package stats", exc_info=True)
+4 -2
View File
@@ -21,8 +21,10 @@ const STATUS_META = {
uninstalled: { label: "uninstalled", color: "#555", bg: "#1a1a1a", hover: "#252525", summaryBg: "#1a1a1a", summaryText: "#888" },
};
// Tiers that may offer a "Disable" action (when ComfyUI Manager is available).
const DISABLEABLE_TIERS = new Set(["safe_to_remove", "consider_removing"]);
// Tiers that may offer a "Disable" action. A package never used during the
// first month is safe to disable manually too; waiting a month would otherwise
// strand common one-off installs such as VAE Decode PlusPlus.
const DISABLEABLE_TIERS = new Set(["safe_to_remove", "consider_removing", "unused_new"]);
// Setting id for the auto-open-on-load behavior. Defaults to off so loading a
// workflow with missing/disabled nodes no longer pops the dialog every time.
+45
View File
@@ -0,0 +1,45 @@
from tracker import UsageTracker
class _Mapper:
def __init__(self, mapping=()):
self.mapping = dict(mapping)
def get_package(self, class_type):
return self.mapping.get(class_type, "__unknown__")
def get_all_packages(self):
return set(self.mapping.values()) - {"__builtin__"}
def test_never_used_disabled_package_stays_visible_for_enable(tmp_path):
tracker = UsageTracker(db_path=str(tmp_path / "test.db"))
stats = tracker.get_package_stats(
_Mapper(), disabled_packages={"VAE-Decode-PlusPlus"}
)
assert stats == [{
"package": "VAE-Decode-PlusPlus",
"total_executions": 0,
"used_nodes": 0,
"nodes": [],
"last_seen": None,
"total_nodes": 0,
"installed": False,
"status": "uninstalled",
"whitelisted": False,
}]
def test_disabled_entry_does_not_override_an_active_package(tmp_path):
tracker = UsageTracker(db_path=str(tmp_path / "test.db"))
stats = tracker.get_package_stats(
_Mapper({"Node": "VAE-Decode-PlusPlus"}),
disabled_packages={"vae-decode-plusplus"},
)
assert len(stats) == 1
assert stats[0]["package"] == "VAE-Decode-PlusPlus"
assert stats[0]["installed"] is True
+31 -5
View File
@@ -217,8 +217,14 @@ class UsageTracker:
finally:
conn.close()
def get_package_stats(self, mapper):
"""Aggregate per-package stats combining DB data with known nodes."""
def get_package_stats(self, mapper, disabled_packages=()):
"""Aggregate package stats from usage, active nodes, and disabled packs.
``disabled_packages`` is the set of directory names currently parked in
``custom_nodes/.disabled``. Those packages are no longer loaded, so
they cannot appear in the node mapper. Including them here keeps a
never-used package visible long enough for the UI to offer Enable.
"""
node_stats = self.get_node_stats()
# Build per-package data from DB
@@ -263,13 +269,33 @@ class UsageTracker:
}
packages[pkg]["total_nodes"] = total
# Packages only in DB (not in mapper) are uninstalled/disabled
# Packages only in DB (not in mapper) are uninstalled/disabled.
# Also add disabled-on-disk packages that have no recorded executions;
# without this, a package disabled before first use disappears from the
# UI and cannot be re-enabled there.
active_package_keys = {pkg.lower() for pkg in node_counts}
existing_package_keys = {pkg.lower() for pkg in packages}
for pkg in disabled_packages:
if not isinstance(pkg, str) or not pkg or pkg.lower() in active_package_keys:
continue
if pkg.lower() in existing_package_keys:
continue
packages[pkg] = {
"package": pkg,
"total_executions": 0,
"used_nodes": 0,
"nodes": [],
"last_seen": None,
"total_nodes": 0,
}
existing_package_keys.add(pkg.lower())
# node_counts already includes all packages from mapper + get_all_packages()
installed_packages = set(node_counts.keys())
installed_packages = active_package_keys
for pkg, entry in packages.items():
if "total_nodes" not in entry:
entry["total_nodes"] = entry["used_nodes"]
entry["installed"] = pkg in installed_packages
entry["installed"] = pkg.lower() in installed_packages
# Classify packages by usage recency
now = datetime.now(timezone.utc)