feat: native disable/enable fallback and package whitelist; bump to 1.8.0
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
Publish to Comfy registry / Publish Custom Node to registry (push) Waiting to run
Disable/enable no longer require ComfyUI Manager: - New pack_fs.py moves packs in/out of custom_nodes/.disabled/ (no import, delete, or re-clone). Fallback for hand-cloned packs, loose single-file nodes, or when Manager is absent. enable strips the @version suffix so packs restore as clean, importable dir names. - Routes: native-disable, native-enable, disabled-packs. - Frontend routes each disable per-pack (Manager queue vs native move), and shows an Enable button on recoverable packs in the Uninstalled tier. The restart banner degrades to a manual-restart notice when no Manager exists. Whitelist (packages-only): a star toggle protects a pack — pulled into its own pinned group, no Disable button, skipped by the 7-day trial auto-disable. - New whitelist_packages table; whitelisted flag on package stats. - Routes: whitelist, whitelist/add, whitelist/remove. Tests: test_pack_fs.py, test_whitelist.py (60 passing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+268
-52
@@ -119,6 +119,10 @@ function unresolvedNodeTypes() {
|
||||
// Latest workflow scan, shared so showStatsDialog can render the Workflow tab.
|
||||
let _lastWorkflowScan = { disabled: [], missing: [] };
|
||||
|
||||
// Lowercased names of packs currently in custom_nodes/.disabled/ — the packs the
|
||||
// extension can natively re-enable. Refreshed each time the dialog opens.
|
||||
let _disabledPacksSet = new Set();
|
||||
|
||||
async function onWorkflowLoaded() {
|
||||
const types = unresolvedNodeTypes();
|
||||
_lastWorkflowScan = await classifyUnresolved(types);
|
||||
@@ -133,12 +137,14 @@ async function onWorkflowLoaded() {
|
||||
async function showStatsDialog(initialTab = "nodes") {
|
||||
let data, modelData, managerInfo, trials = [];
|
||||
try {
|
||||
const [pkgResp, modelResp, mgr, trialsResp] = await Promise.all([
|
||||
const [pkgResp, modelResp, mgr, trialsResp, disabledPacks] = await Promise.all([
|
||||
fetch("/nodes-stats/packages"),
|
||||
fetch("/nodes-stats/models"),
|
||||
fetchManagerInfo(),
|
||||
fetch("/nodes-stats/trials").catch(() => null),
|
||||
fetchDisabledPacks(),
|
||||
]);
|
||||
_disabledPacksSet = disabledPacks;
|
||||
if (!pkgResp.ok) { alert("Failed to load node stats: HTTP " + pkgResp.status); return; }
|
||||
if (!modelResp.ok) { alert("Failed to load model stats: HTTP " + modelResp.status); return; }
|
||||
data = await pkgResp.json();
|
||||
@@ -248,6 +254,8 @@ async function showStatsDialog(initialTab = "nodes") {
|
||||
|
||||
wireDisableButtons(dialog, managerInfo);
|
||||
wireWorkflowButtons(dialog);
|
||||
wireWhitelistButtons(dialog);
|
||||
wireNativeEnableButtons(dialog);
|
||||
|
||||
switchTab(TABS.includes(initialTab) ? initialTab : "nodes");
|
||||
|
||||
@@ -285,6 +293,7 @@ function dialogStyle() {
|
||||
#nodes-stats-dialog .ns-btn:hover:not(:disabled){background:#3a2020;border-color:#e44;color:#fff;}
|
||||
#nodes-stats-dialog .ns-btn:disabled{opacity:0.5;cursor:default;}
|
||||
#nodes-stats-dialog .ns-disable-all-btn{border-color:#a33;color:#e88;}
|
||||
#nodes-stats-dialog .ns-enable-native-btn:hover:not(:disabled){background:#203a20;border-color:#4a4;color:#fff;}
|
||||
${rows}
|
||||
</style>`;
|
||||
}
|
||||
@@ -306,7 +315,11 @@ function summaryBar(items) {
|
||||
}
|
||||
|
||||
function buildNodesTabContent(custom, managerInfo) {
|
||||
const byStatus = (s) => custom.filter((p) => p.status === s);
|
||||
// Whitelisted packs are pulled out of their usage tiers into a pinned group
|
||||
// and never offered for disable. Everything else classifies as usual.
|
||||
const whitelisted = custom.filter((p) => p.whitelisted);
|
||||
const rest = custom.filter((p) => !p.whitelisted);
|
||||
const byStatus = (s) => rest.filter((p) => p.status === s);
|
||||
const safeToRemove = byStatus("safe_to_remove");
|
||||
const considerRemoving = byStatus("consider_removing");
|
||||
const unusedNew = byStatus("unused_new");
|
||||
@@ -320,6 +333,7 @@ function buildNodesTabContent(custom, managerInfo) {
|
||||
{ count: used.length, status: "used", label: "used", id: "nodes-stats-used-badge" },
|
||||
]);
|
||||
|
||||
html += renderWhitelistSection(whitelisted, managerInfo);
|
||||
html += renderSection("Safe to Remove", "Unused for 2+ months", "safe_to_remove", safeToRemove, managerInfo);
|
||||
html += renderSection("Consider Removing", "Unused for 1-2 months", "consider_removing", considerRemoving, managerInfo);
|
||||
html += renderSection("Recently Unused", "Unused for less than 1 month", "unused_new", unusedNew, managerInfo);
|
||||
@@ -333,8 +347,12 @@ function renderSection(title, subtitle, status, packages, managerInfo) {
|
||||
if (packages.length === 0) return "";
|
||||
|
||||
const color = STATUS_META[status].color;
|
||||
const withActions = !!managerInfo && DISABLEABLE_TIERS.has(status);
|
||||
const eligible = withActions
|
||||
// Disable actions no longer require Manager: an installed pack Manager doesn't
|
||||
// cover is disabled natively (moved into custom_nodes/.disabled by us).
|
||||
// The Uninstalled tier gets an Enable action for packs recoverable from
|
||||
// .disabled/ (also native, so it works with or without Manager).
|
||||
const withActions = DISABLEABLE_TIERS.has(status) || sectionHasEnable(status, packages);
|
||||
const eligible = DISABLEABLE_TIERS.has(status)
|
||||
? packages.filter((p) => isDisableEligible(p, managerInfo)).map((p) => p.package)
|
||||
: [];
|
||||
|
||||
@@ -352,12 +370,48 @@ function renderSection(title, subtitle, status, packages, managerInfo) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// A package can be disabled only if ComfyUI Manager knows it (by directory
|
||||
// name) and it is currently active (any state other than already-disabled).
|
||||
// How a package can be disabled: "manager" when ComfyUI Manager knows it (by
|
||||
// directory name) and it's currently active; "native" when it's installed but
|
||||
// Manager doesn't cover it (hand-cloned repo, loose file, or Manager absent) —
|
||||
// the extension moves it into custom_nodes/.disabled itself; null when it can't
|
||||
// be disabled (uninstalled, or whitelisted/protected).
|
||||
function disableMode(pkg, managerInfo) {
|
||||
if (pkg.whitelisted || !pkg.installed) return null;
|
||||
const info = managerInfo && managerInfo[pkg.package];
|
||||
if (info) {
|
||||
// Manager knows this pack: offer disable only while it's still active.
|
||||
return info.state && info.state !== "disabled" ? "manager" : null;
|
||||
}
|
||||
// Manager doesn't cover it (hand-cloned, loose file, or Manager absent):
|
||||
// the extension moves it into custom_nodes/.disabled itself.
|
||||
return "native";
|
||||
}
|
||||
|
||||
function isDisableEligible(pkg, managerInfo) {
|
||||
if (!managerInfo || !pkg.installed) return false;
|
||||
const info = managerInfo[pkg.package];
|
||||
return !!(info && info.state && info.state !== "disabled");
|
||||
return disableMode(pkg, managerInfo) !== null;
|
||||
}
|
||||
|
||||
// An uninstalled pack can be natively re-enabled iff its source is still sitting
|
||||
// in custom_nodes/.disabled/. (Whitelisted packs live in their own group.)
|
||||
function nativeEnableEligible(pkg) {
|
||||
return pkg.status === "uninstalled" && _disabledPacksSet.has(pkg.package.toLowerCase());
|
||||
}
|
||||
|
||||
function sectionHasEnable(status, packages) {
|
||||
return status === "uninstalled" && packages.some(nativeEnableEligible);
|
||||
}
|
||||
|
||||
const WHITELIST_COLOR = "#e5c04b";
|
||||
|
||||
// Pinned group of protected packs. They keep their own row background (mixed
|
||||
// statuses) but never show a Disable action — the star toggles protection off.
|
||||
function renderWhitelistSection(packages, managerInfo) {
|
||||
if (packages.length === 0) return "";
|
||||
let html = `<div style="display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin:16px 0 8px;">
|
||||
<h3 style="color:${WHITELIST_COLOR};margin:0;font-size:14px;">★ Whitelist <span style="color:#666;font-size:12px;font-weight:normal;">— protected, never disabled</span></h3>
|
||||
</div>`;
|
||||
html += buildTable(packages, null, false, managerInfo);
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildModelsTabContent(modelData) {
|
||||
@@ -462,6 +516,15 @@ function sectionHeader(title, subtitle, color) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// Clickable whitelist toggle rendered before a package name. Filled gold star
|
||||
// when protected, hollow grey when not. Wired by wireWhitelistButtons.
|
||||
function whitelistStar(pkg) {
|
||||
const on = !!pkg.whitelisted;
|
||||
return `<span class="ns-wl-btn" data-pkg="${escapeAttr(pkg.package)}" data-on="${on ? 1 : 0}" ` +
|
||||
`title="${on ? "Remove from whitelist" : "Add to whitelist (protect from disable)"}" ` +
|
||||
`style="cursor:pointer;margin-right:7px;color:${on ? WHITELIST_COLOR : "#555"};">${on ? "★" : "☆"}</span>`;
|
||||
}
|
||||
|
||||
function buildTable(packages, status, withActions, managerInfo) {
|
||||
const colspan = withActions ? 7 : 6;
|
||||
|
||||
@@ -480,19 +543,23 @@ function buildTable(packages, status, withActions, managerInfo) {
|
||||
const hasNodes = pkg.nodes && pkg.nodes.length > 0;
|
||||
const lastSeen = pkg.last_seen ? new Date(pkg.last_seen).toLocaleDateString() : "—";
|
||||
|
||||
html += `<tr class="pkg-row ns-row-${status}" style="cursor:${hasNodes ? "pointer" : "default"};border-bottom:1px solid #222;">
|
||||
html += `<tr class="pkg-row ns-row-${pkg.status}" style="cursor:${hasNodes ? "pointer" : "default"};border-bottom:1px solid #222;">
|
||||
<td style="padding:6px 8px;width:20px;"><span class="arrow" style="color:#666;">${hasNodes ? "▶" : " "}</span></td>
|
||||
<td style="padding:6px 8px;color:#fff;">${escapeHtml(pkg.package)}</td>
|
||||
<td style="padding:6px 8px;color:#fff;">${whitelistStar(pkg)}${escapeHtml(pkg.package)}</td>
|
||||
<td style="padding:6px 8px;text-align:right;">${pkg.total_nodes}</td>
|
||||
<td style="padding:6px 8px;text-align:right;">${pkg.used_nodes}/${pkg.total_nodes}</td>
|
||||
<td style="padding:6px 8px;text-align:right;">${pkg.total_executions}</td>
|
||||
<td style="padding:6px 8px;color:#888;">${lastSeen}</td>`;
|
||||
|
||||
if (withActions) {
|
||||
const eligible = isDisableEligible(pkg, managerInfo);
|
||||
const cell = eligible
|
||||
? `<button class="ns-btn ns-disable-btn" data-pkg="${escapeAttr(pkg.package)}">Disable</button>`
|
||||
: `<span style="color:#555;">—</span>`;
|
||||
let cell;
|
||||
if (nativeEnableEligible(pkg)) {
|
||||
cell = `<button class="ns-btn ns-enable-native-btn" data-pkg="${escapeAttr(pkg.package)}" style="border-color:#3a6;color:#8d8;">Enable</button>`;
|
||||
} else if (isDisableEligible(pkg, managerInfo)) {
|
||||
cell = `<button class="ns-btn ns-disable-btn" data-pkg="${escapeAttr(pkg.package)}">Disable</button>`;
|
||||
} else {
|
||||
cell = `<span style="color:#555;">—</span>`;
|
||||
}
|
||||
html += `<td class="ns-action-cell" data-pkg="${escapeAttr(pkg.package)}" style="padding:6px 8px;text-align:right;">${cell}</td>`;
|
||||
}
|
||||
html += `</tr>`;
|
||||
@@ -799,8 +866,8 @@ function disablePayload(dirName, info) {
|
||||
}
|
||||
|
||||
function wireDisableButtons(dialog, managerInfo) {
|
||||
if (!managerInfo) return;
|
||||
|
||||
// No early return on a missing Manager: native-disable buttons are rendered
|
||||
// for installed packs Manager doesn't cover, and must work with no Manager.
|
||||
dialog.querySelectorAll(".ns-disable-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -831,48 +898,156 @@ function wireWorkflowButtons(dialog) {
|
||||
b.addEventListener("click", (e) => { e.stopPropagation(); handleTrialInstall(b.dataset.pkg, dialog, false); }));
|
||||
}
|
||||
|
||||
async function handleDisable(pkgNames, dialog, managerInfo) {
|
||||
// Only act on packages Manager still reports as active (guards against
|
||||
// double-clicks and stale buttons after a partial batch).
|
||||
pkgNames = pkgNames.filter((n) => managerInfo[n] && managerInfo[n].state !== "disabled");
|
||||
if (pkgNames.length === 0) return;
|
||||
// Star toggles on package rows: add/remove the pack from the whitelist, then
|
||||
// re-render the Nodes tab so the pack moves in/out of the pinned group.
|
||||
function wireWhitelistButtons(dialog) {
|
||||
dialog.querySelectorAll(".ns-wl-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleWhitelist(btn.dataset.pkg, btn.dataset.on !== "1");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const what = pkgNames.length === 1 ? `"${pkgNames[0]}"` : `${pkgNames.length} packages`;
|
||||
async function toggleWhitelist(pkg, add) {
|
||||
try {
|
||||
const r = await fetch(add ? "/nodes-stats/whitelist/add" : "/nodes-stats/whitelist/remove", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ package: pkg }),
|
||||
});
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
} catch (e) {
|
||||
notify("Whitelist update failed: " + e.message, "error");
|
||||
return;
|
||||
}
|
||||
showStatsDialog("nodes"); // re-fetch + rebuild so the pack regroups
|
||||
}
|
||||
|
||||
async function fetchWhitelist() {
|
||||
try {
|
||||
const r = await fetch("/nodes-stats/whitelist");
|
||||
if (r.ok) return new Set(await r.json());
|
||||
} catch { /* treat as empty */ }
|
||||
return new Set();
|
||||
}
|
||||
|
||||
// Enable buttons on recoverable uninstalled packs: move the pack out of
|
||||
// custom_nodes/.disabled via the native route. Restart is left to the user.
|
||||
function wireNativeEnableButtons(dialog) {
|
||||
dialog.querySelectorAll(".ns-enable-native-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
handleNativeEnable(btn.dataset.pkg, btn, dialog);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleNativeEnable(pkg, btn, dialog) {
|
||||
if (!confirm(
|
||||
`Enable "${pkg}"?\n\nIt will be moved out of custom_nodes/.disabled and ` +
|
||||
`load on the next ComfyUI restart.`
|
||||
)) return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const r = await fetch("/nodes-stats/native-enable", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ package: pkg }),
|
||||
});
|
||||
if (!r.ok) {
|
||||
let msg = "HTTP " + r.status;
|
||||
try { const j = await r.json(); if (j && j.message) msg = j.message; } catch { /* keep status */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
_disabledPacksSet.delete(pkg.toLowerCase());
|
||||
const cell = dialog.querySelector(`.ns-action-cell[data-pkg="${cssEscape(pkg)}"]`);
|
||||
if (cell) cell.innerHTML = `<span style="color:#6a6;font-size:11px;">✓ enabled · restart</span>`;
|
||||
showRestartBanner(dialog);
|
||||
notify(`Enabled ${pkg}. Restart ComfyUI to load it.`, "success");
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
notify("Failed to enable: " + e.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Lowercased set of pack names in custom_nodes/.disabled/ (native re-enable
|
||||
// candidates). Lowercased so lookups match package names case-insensitively.
|
||||
async function fetchDisabledPacks() {
|
||||
try {
|
||||
const r = await fetch("/nodes-stats/disabled-packs");
|
||||
if (r.ok) return new Set((await r.json()).map((n) => n.toLowerCase()));
|
||||
} catch { /* treat as empty */ }
|
||||
return new Set();
|
||||
}
|
||||
|
||||
async function handleDisable(pkgNames, dialog, managerInfo) {
|
||||
// Partition into packages Manager can disable (still reported active) vs. the
|
||||
// rest, which we disable natively by moving them into custom_nodes/.disabled.
|
||||
// The eligible lists already excluded whitelisted/uninstalled packs.
|
||||
const managerNames = pkgNames.filter(
|
||||
(n) => managerInfo && managerInfo[n] && managerInfo[n].state && managerInfo[n].state !== "disabled"
|
||||
);
|
||||
const nativeNames = pkgNames.filter((n) => !managerNames.includes(n));
|
||||
if (managerNames.length === 0 && nativeNames.length === 0) return;
|
||||
|
||||
const total = managerNames.length + nativeNames.length;
|
||||
const what = total === 1 ? `"${managerNames[0] || nativeNames[0]}"` : `${total} packages`;
|
||||
const via = managerNames.length && nativeNames.length
|
||||
? "ComfyUI Manager / directly"
|
||||
: managerNames.length ? "ComfyUI Manager" : "the extension";
|
||||
const confirmMsg =
|
||||
`Disable ${what} via ComfyUI Manager?\n\n` +
|
||||
`Disable ${what} via ${via}?\n\n` +
|
||||
`They will be moved to custom_nodes/.disabled and a ComfyUI restart is ` +
|
||||
`required to take effect. You can re-enable them anytime from ComfyUI Manager.`;
|
||||
`required to take effect. You can re-enable them anytime.`;
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
setDisableButtonsBusy(dialog, true);
|
||||
const succeeded = [];
|
||||
const failed = [];
|
||||
try {
|
||||
const pre = await mgrFetch("/manager/queue/status").then((r) => (r && r.ok ? r.json() : null)).catch(() => null);
|
||||
if (pre && pre.is_processing) {
|
||||
notify("ComfyUI Manager is busy. Please try again in a moment.", "warn");
|
||||
setDisableButtonsBusy(dialog, false);
|
||||
return;
|
||||
if (managerNames.length) {
|
||||
const pre = await mgrFetch("/manager/queue/status").then((r) => (r && r.ok ? r.json() : null)).catch(() => null);
|
||||
if (pre && pre.is_processing) {
|
||||
notify("ComfyUI Manager is busy. Please try again in a moment.", "warn");
|
||||
failed.push(...managerNames);
|
||||
} else {
|
||||
try {
|
||||
await runManagerDisable(managerNames.map((n) => disablePayload(n, managerInfo[n])));
|
||||
// Reconcile against Manager's actual state: disabled only if no longer
|
||||
// reported as active on disk.
|
||||
const after = await fetchManagerInfo();
|
||||
const isStillActive = (n) => after && after[n] && after[n].state !== "disabled";
|
||||
for (const n of managerNames) {
|
||||
if (after ? !isStillActive(n) : true) {
|
||||
succeeded.push(n);
|
||||
if (managerInfo[n]) managerInfo[n].state = "disabled";
|
||||
} else {
|
||||
failed.push(n);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
failed.push(...managerNames);
|
||||
notify("ComfyUI Manager disable failed: " + e.message, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payloads = pkgNames.map((n) => disablePayload(n, managerInfo[n]));
|
||||
await runManagerDisable(payloads);
|
||||
if (nativeNames.length) {
|
||||
const res = await runNativeDisable(nativeNames);
|
||||
succeeded.push(...res.succeeded);
|
||||
failed.push(...res.failed);
|
||||
}
|
||||
|
||||
// Reconcile against Manager's actual state: a package is considered
|
||||
// disabled only if it's no longer reported as active on disk.
|
||||
const after = await fetchManagerInfo();
|
||||
const isStillActive = (n) => after && after[n] && after[n].state !== "disabled";
|
||||
const succeeded = after ? pkgNames.filter((n) => !isStillActive(n)) : pkgNames;
|
||||
const failed = pkgNames.filter((n) => !succeeded.includes(n));
|
||||
|
||||
succeeded.forEach((n) => { if (managerInfo[n]) managerInfo[n].state = "disabled"; });
|
||||
markPackagesDisabled(dialog, succeeded);
|
||||
updateBulkButtons(dialog, managerInfo);
|
||||
updateBulkButtons(dialog, new Set(succeeded));
|
||||
|
||||
if (succeeded.length > 0) {
|
||||
showRestartBanner(dialog);
|
||||
notify(`Disabled ${succeeded.length} package${succeeded.length !== 1 ? "s" : ""}. Restart ComfyUI to apply.`, "success");
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
notify(`ComfyUI Manager could not disable: ${failed.join(", ")}`, "error");
|
||||
notify(`Could not disable: ${failed.join(", ")}`, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
notify("Failed to disable: " + e.message, "error");
|
||||
@@ -881,6 +1056,26 @@ async function handleDisable(pkgNames, dialog, managerInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// Disable packs by moving each into custom_nodes/.disabled via the extension's
|
||||
// own route (no Manager). Returns { succeeded, failed } name lists.
|
||||
async function runNativeDisable(pkgNames) {
|
||||
const succeeded = [];
|
||||
const failed = [];
|
||||
for (const n of pkgNames) {
|
||||
try {
|
||||
const r = await fetch("/nodes-stats/native-disable", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ package: n }),
|
||||
});
|
||||
(r.ok ? succeeded : failed).push(n);
|
||||
} catch {
|
||||
failed.push(n);
|
||||
}
|
||||
}
|
||||
return { succeeded, failed };
|
||||
}
|
||||
|
||||
// v4-legacy: queue a batch of operations ({install:[...]}, {disable:[...]}, ...)
|
||||
// through the single endpoint that also starts the worker, then wait for it to
|
||||
// finish. A reset first clears any stale queue (harmless if empty).
|
||||
@@ -1391,26 +1586,39 @@ async function processExpiredTrials() {
|
||||
if (r.ok) trials = await r.json();
|
||||
} catch { return; }
|
||||
|
||||
const expired = trials.filter((t) => t.expired);
|
||||
let expired = trials.filter((t) => t.expired);
|
||||
if (!expired.length) return;
|
||||
|
||||
// Whitelisted packs are protected: never auto-disable them. Clear their trial
|
||||
// row so they stop being tracked as temporary (they're now keepers).
|
||||
const whitelist = await fetchWhitelist();
|
||||
const protectedExpired = expired.filter((t) => whitelist.has(t.package));
|
||||
for (const t of protectedExpired) await stopTrial(t.package);
|
||||
expired = expired.filter((t) => !whitelist.has(t.package));
|
||||
if (!expired.length) return;
|
||||
|
||||
const mgr = await fetchManagerInfo();
|
||||
if (!mgr) return; // Manager unavailable — leave rows for a later session
|
||||
|
||||
// Don't reset Manager's queue out from under an in-progress operation
|
||||
// (e.g. startup install work); the expired rows persist and retry next session.
|
||||
if (await managerIsBusy()) return;
|
||||
if (mgr && await managerIsBusy()) return;
|
||||
|
||||
const done = [];
|
||||
for (const t of expired) {
|
||||
const info = mgr[t.package];
|
||||
if (!info || info.state === "disabled") {
|
||||
const info = mgr && mgr[t.package];
|
||||
if (info && info.state === "disabled") {
|
||||
await stopTrial(t.package);
|
||||
done.push(t.package);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await runManagerDisable([disablePayload(t.package, info)]);
|
||||
if (info) {
|
||||
await runManagerDisable([disablePayload(t.package, info)]);
|
||||
} else {
|
||||
// Manager absent or doesn't know this pack — disable it natively.
|
||||
const res = await runNativeDisable([t.package]);
|
||||
if (!res.succeeded.includes(t.package)) throw new Error("native disable failed");
|
||||
}
|
||||
await stopTrial(t.package);
|
||||
done.push(t.package);
|
||||
} catch { /* keep the row; retry next session */ }
|
||||
@@ -1454,13 +1662,13 @@ function markPackagesDisabled(dialog, pkgNames) {
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute "Disable all (N)" counts after a batch; hide buttons with nothing
|
||||
// left to disable.
|
||||
function updateBulkButtons(dialog, managerInfo) {
|
||||
// Recompute "Disable all (N)" counts after a batch, dropping packages that were
|
||||
// just disabled; hide buttons with nothing left. Mode-agnostic (Manager+native).
|
||||
function updateBulkButtons(dialog, succeededSet) {
|
||||
dialog.querySelectorAll(".ns-disable-all-btn").forEach((btn) => {
|
||||
let names = [];
|
||||
try { names = JSON.parse(btn.dataset.pkgs); } catch { names = []; }
|
||||
const remaining = names.filter((n) => managerInfo[n] && managerInfo[n].state !== "disabled");
|
||||
const remaining = names.filter((n) => !succeededSet.has(n));
|
||||
if (remaining.length === 0) {
|
||||
btn.style.display = "none";
|
||||
} else {
|
||||
@@ -1492,6 +1700,14 @@ function showRestartBanner(dialog) {
|
||||
}
|
||||
|
||||
async function rebootComfy() {
|
||||
// The reboot endpoint is provided by ComfyUI Manager. Without it we can't
|
||||
// restart the server ourselves — tell the user to do it manually (this is the
|
||||
// native-only disable path).
|
||||
const api = await detectManagerApi();
|
||||
if (!api) {
|
||||
notify("No ComfyUI Manager to trigger a restart — please restart ComfyUI manually to apply the changes.", "warn");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Restart ComfyUI now? The server will go down briefly and the page will reconnect.")) return;
|
||||
notify("Restarting ComfyUI…", "info");
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user