Files
olivv-cs-ComfyUI-FunPack/app/shell/packs.js
T
digital-garbage 1185e2201f core, app: a decade of races closed -- log routes, restarts, results, defaults
Two routes silently shared /api/log; a pack's "updates available" badge
outlived the update; nothing server-side stopped a restart mid-generation,
and the fixes for that had their own dead end, a lock scoped to the wrong
event loop, an unlocked status fetch, and a check that only ran once
instead of at the moment it mattered. A run's result was lost outright if
the project it belonged to wasn't the one open when it finished -- on a
project switch or a page reload. A brand-new project inherited the
previous one's width, then inherited the wrong "default" for it.

Found and confirmed via an adversarial pass over 9dcd94c..HEAD, one round
per fix.
2026-09-06 18:08:48 +03:00

167 lines
6.1 KiB
JavaScript

// Node packs: install, update, remove.
//
// A stand-in for ComfyUI-Manager's three operations, so somebody adding the one
// pack a workflow needs is not sent to another UI to do it. There is no
// catalogue and no curation -- the URL is theirs -- which keeps this honest
// about what it is: git, in a directory.
//
// Everything here can take minutes. Nothing pretends otherwise: the row that is
// working says so, and the rest of the list stays usable.
import { composer } from "../composer/composer.js";
const BASE = "/funpack/api/packs";
async function ask(method, path = "", body) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: body === undefined ? {} : { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(payload.detail || `HTTP ${res.status}`);
return payload;
}
/** What a pack's git state says, in words rather than two numbers. */
function saySince(pack, checked) {
if (!pack.git) return "not a git checkout — update and remove are not offered";
const news = checked && checked[pack.name];
if (!news) return `${pack.branch || "?"} · ${pack.commit || ""}`.trim();
if (!news.checked) return `${pack.branch || "?"} · could not check: ${news.reason}`;
if (news.behind) return `${news.behind} update${news.behind === 1 ? "" : "s"} available`;
return `${pack.branch || "?"} · up to date`;
}
export function open() {
let window_ = null;
let listing = null;
let checked = null;
let busy = null; // the pack currently being worked on, by name
// What went wrong, held rather than passed: every redraw would otherwise wipe
// it, and the one after a failure arrives immediately -- the message appeared
// and vanished in the same frame.
let problem = null;
const body = composer.region.stack({ gap: "sm", label: "Node packs" });
const run = async (name, work) => {
busy = name;
problem = null;
draw();
try {
await work();
// Stale news, not just a stale listing: an update that just succeeded must
// not go on claiming there is one available until "Check for updates" is
// pressed again.
if (checked) delete checked[name];
await load();
} catch (err) {
problem = err.message;
} finally {
busy = null;
draw();
}
};
function rows() {
const out = [];
if (problem) out.push(composer.banner.danger({ text: problem }));
const url = composer.input.md({ label: "Git URL",
placeholder: "https://github.com/owner/pack" });
out.push(composer.settingsRow.default({
label: "Install a pack",
hint: "Anything git can clone. ComfyUI has to be restarted before a new pack "
+ "loads — Settings ▸ Updates can do that.",
control: composer.button.md({
label: busy === "*" ? "Installing…" : "Install",
disabled: Boolean(busy),
onClick: () => {
const value = String(url.value || "").trim();
// Said here rather than sent: an empty URL is not a server's problem.
if (!value) { problem = "Paste a git URL first."; draw(); return; }
run("*", () => ask("POST", "/install", { url: value }));
},
}),
}));
out.push(composer.field.default({ label: "Git URL", control: url }));
if (!listing) {
out.push(composer.hint.default({ text: "Reading custom_nodes…" }));
return out;
}
out.push(composer.label.section({ text: `${listing.nodes.length} installed` }));
for (const pack of listing.nodes) {
const working = busy === pack.name;
out.push(composer.settingsRow.default({
label: pack.name + (pack.is_funpack ? " — this is FunPack" : ""),
hint: working ? "Working…" : saySince(pack, checked),
// Buttons, not a button GROUP: these are two actions, not a choice
// between two states. A radio group also leaves one of them looking
// chosen afterwards, which for "Remove" is an alarming thing to look at.
control: composer.toolbar.default({
label: pack.name,
items: [
...(pack.git && !pack.is_funpack ? [composer.button.sm({
label: "Update", disabled: Boolean(busy),
onClick: () => run(pack.name, () => ask("POST", "/update", { name: pack.name })),
})] : []),
...(pack.is_funpack ? [] : [composer.button.sm({
label: "Remove", tone: "danger", disabled: Boolean(busy),
onClick: () => run(pack.name, () => ask("POST", "/remove", { name: pack.name })),
})]),
],
}),
}));
}
return out;
}
function draw() {
body.set(rows());
if (window_) {
window_.setFooter({
note: listing ? listing.root : "",
actions: [
composer.button.md({ label: "Check for updates", disabled: Boolean(busy),
onClick: () => check() }),
composer.button.md({ label: "Close", tone: "primary",
onClick: () => window_.close("done") }),
],
});
}
}
async function load() {
try { listing = await ask("GET"); }
catch (err) { listing = null; problem = err.message; }
draw();
}
async function check() {
// Fetching every pack is network-bound and slow; the button says what it is
// doing rather than looking broken for twenty seconds.
busy = "*check*";
draw();
problem = null;
// `checked`, which is the key the server sends. It said `nodes` here for a
// while: the request succeeded, the JSON was valid, and every pack reported
// nothing to update no matter how far behind it was.
try { checked = (await ask("POST", "/check")).checked || null; }
catch (err) { problem = err.message; }
finally { busy = null; draw(); }
}
window_ = composer.modal.generic({
title: "Node packs",
subtitle: "Install, update and remove ComfyUI custom nodes.",
size: "lg",
body,
onClose: () => { window_ = null; },
});
load();
return window_;
}