fix: keep disabled unused packages manageable
This commit is contained in:
@@ -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
|
- Summary bar with counts for each classification tier
|
||||||
- Sections for each tier, sorted from most actionable to least
|
- Sections for each tier, sorted from most actionable to least
|
||||||
- Expandable rows — click any package to see per-node execution counts and timestamps
|
- 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
|
### Disabling unused packages
|
||||||
|
|
||||||
When [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) is installed, the
|
The "Safe to Remove", "Consider Removing", and "Recently Unused" sections show
|
||||||
"Safe to Remove" and "Consider Removing" sections show a **Disable** button on each
|
a **Disable** button on each package, plus a **Disable all** button per section.
|
||||||
package, plus a **Disable all** button per section. Disabling:
|
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/`
|
- Uses ComfyUI Manager when it manages the package, otherwise moves it directly
|
||||||
- Is fully reversible — re-enable any package from ComfyUI Manager whenever you like
|
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
|
- Requires a ComfyUI restart to unload the package from the running session (a banner
|
||||||
with a **Restart ComfyUI** button appears after disabling)
|
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
|
> **Manager compatibility:** works with both the standalone
|
||||||
> [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) custom node and
|
> [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
|
> 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
|
> `--enable-manager-legacy-ui` (the extension detects the active manager and its
|
||||||
> API automatically). Without a reachable manager, all enable/disable/install
|
> API automatically). Without a reachable manager, Node Stats still supports
|
||||||
> actions are simply omitted.
|
> direct enable/disable; Manager-only workflow install and trial actions are
|
||||||
|
> unavailable.
|
||||||
|
|
||||||
### Workflow tab & temporary enable
|
### Workflow tab & temporary enable
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -78,7 +78,13 @@ routes = PromptServer.instance.routes
|
|||||||
@routes.get("/nodes-stats/packages")
|
@routes.get("/nodes-stats/packages")
|
||||||
async def get_package_stats(request):
|
async def get_package_stats(request):
|
||||||
try:
|
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)
|
return web.json_response(stats)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("nodes-stats: error getting package stats", exc_info=True)
|
logger.error("nodes-stats: error getting package stats", exc_info=True)
|
||||||
|
|||||||
+4
-2
@@ -21,8 +21,10 @@ const STATUS_META = {
|
|||||||
uninstalled: { label: "uninstalled", color: "#555", bg: "#1a1a1a", hover: "#252525", summaryBg: "#1a1a1a", summaryText: "#888" },
|
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).
|
// Tiers that may offer a "Disable" action. A package never used during the
|
||||||
const DISABLEABLE_TIERS = new Set(["safe_to_remove", "consider_removing"]);
|
// 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
|
// 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.
|
// workflow with missing/disabled nodes no longer pops the dialog every time.
|
||||||
|
|||||||
@@ -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
@@ -217,8 +217,14 @@ class UsageTracker:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def get_package_stats(self, mapper):
|
def get_package_stats(self, mapper, disabled_packages=()):
|
||||||
"""Aggregate per-package stats combining DB data with known nodes."""
|
"""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()
|
node_stats = self.get_node_stats()
|
||||||
|
|
||||||
# Build per-package data from DB
|
# Build per-package data from DB
|
||||||
@@ -263,13 +269,33 @@ class UsageTracker:
|
|||||||
}
|
}
|
||||||
packages[pkg]["total_nodes"] = total
|
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()
|
# 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():
|
for pkg, entry in packages.items():
|
||||||
if "total_nodes" not in entry:
|
if "total_nodes" not in entry:
|
||||||
entry["total_nodes"] = entry["used_nodes"]
|
entry["total_nodes"] = entry["used_nodes"]
|
||||||
entry["installed"] = pkg in installed_packages
|
entry["installed"] = pkg.lower() in installed_packages
|
||||||
|
|
||||||
# Classify packages by usage recency
|
# Classify packages by usage recency
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|||||||
Reference in New Issue
Block a user