diff --git a/README.md b/README.md index e4874fd..6588a36 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/__init__.py b/__init__.py index 968b203..cf66a7e 100644 --- a/__init__.py +++ b/__init__.py @@ -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) diff --git a/js/nodes_stats.js b/js/nodes_stats.js index 7b66d6d..6a0d7a6 100644 --- a/js/nodes_stats.js +++ b/js/nodes_stats.js @@ -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. diff --git a/tests/test_disabled_package_stats.py b/tests/test_disabled_package_stats.py new file mode 100644 index 0000000..408014b --- /dev/null +++ b/tests/test_disabled_package_stats.py @@ -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 diff --git a/tracker.py b/tracker.py index c09c04d..ba77ca1 100644 --- a/tracker.py +++ b/tracker.py @@ -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)