feat: loading-screen status overlay (no more silent 'Comfy' splash)
Adds a small status line on ComfyUI's loading splash showing whether Tenaciousload is serving object_info from cache or building it (live node count + elapsed time, with a 'scanning model folders over the network' hint when the CIFS walk stalls progress). Removes itself on the app 'setup' hook. Backend tracks build progress (_build_state) and exposes GET /tenaciousload/status; frontend web/loading-status.js polls it and renders an unobtrusive overlay. Unit-tested progress tracking + status shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,10 @@ python main.py --listen --port 8188 --enable-compress-response-body
|
|||||||
negligible compared to the bytes saved over the network.
|
negligible compared to the bytes saved over the network.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
- **Loading status:** instead of ComfyUI's silent "Comfy" splash, a small status
|
||||||
|
line shows whether it's *serving from cache* or *building* (with node count +
|
||||||
|
elapsed time), so a long rebuild isn't a black screen with no feedback. It
|
||||||
|
removes itself once the app is ready. Status is also at `GET /tenaciousload/status`.
|
||||||
- The disk cache lives in `./cache/` (git-ignored). Delete it, or use the refresh
|
- The disk cache lives in `./cache/` (git-ignored). Delete it, or use the refresh
|
||||||
button, to force a rebuild.
|
button, to force a rebuild.
|
||||||
- An nginx reverse proxy can cache `object_info` at the HTTP layer too, but this
|
- An nginx reverse proxy can cache `object_info` at the HTTP layer too, but this
|
||||||
|
|||||||
+30
-2
@@ -412,6 +412,9 @@ def _check_node_signature():
|
|||||||
_node_info_fn = None
|
_node_info_fn = None
|
||||||
_node_info_resolved = False
|
_node_info_resolved = False
|
||||||
|
|
||||||
|
# Live build progress, surfaced at /tenaciousload/status for the loading overlay.
|
||||||
|
_build_state = {"building": False, "started": 0.0, "done": 0, "total": 0, "last_ms": 0, "last_bytes": 0}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_node_info_fn():
|
def _resolve_node_info_fn():
|
||||||
"""Pull ComfyUI's own `node_info` closure off the /object_info route, so the
|
"""Pull ComfyUI's own `node_info` closure off the /object_info route, so the
|
||||||
@@ -441,14 +444,23 @@ def _resolve_node_info_fn():
|
|||||||
def _build_object_info_bytes():
|
def _build_object_info_bytes():
|
||||||
"""Replicate ComfyUI's object_info build. Runs in a worker thread."""
|
"""Replicate ComfyUI's object_info build. Runs in a worker thread."""
|
||||||
import nodes
|
import nodes
|
||||||
|
keys = list(nodes.NODE_CLASS_MAPPINGS.keys())
|
||||||
|
_build_state.update(building=True, started=time.time(), done=0, total=len(keys))
|
||||||
out = {}
|
out = {}
|
||||||
|
try:
|
||||||
with folder_paths.cache_helper:
|
with folder_paths.cache_helper:
|
||||||
for x in list(nodes.NODE_CLASS_MAPPINGS.keys()):
|
for i, x in enumerate(keys):
|
||||||
try:
|
try:
|
||||||
out[x] = _node_info_fn(x)
|
out[x] = _node_info_fn(x)
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
log.error("Tenaciousload: node_info failed for '%s'", x, exc_info=True)
|
log.error("Tenaciousload: node_info failed for '%s'", x, exc_info=True)
|
||||||
return json.dumps(out).encode("utf-8")
|
_build_state["done"] = i + 1
|
||||||
|
raw = json.dumps(out).encode("utf-8")
|
||||||
|
_build_state["last_bytes"] = len(raw)
|
||||||
|
return raw
|
||||||
|
finally:
|
||||||
|
_build_state["building"] = False
|
||||||
|
_build_state["last_ms"] = int((time.time() - _build_state["started"]) * 1000)
|
||||||
|
|
||||||
|
|
||||||
async def _build_object_info_off_loop():
|
async def _build_object_info_off_loop():
|
||||||
@@ -533,6 +545,22 @@ _install_middleware()
|
|||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Refresh API
|
# Refresh API
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
@PromptServer.instance.routes.get("/tenaciousload/status")
|
||||||
|
async def _status(request):
|
||||||
|
st = {
|
||||||
|
"building": _build_state["building"],
|
||||||
|
"done": _build_state["done"],
|
||||||
|
"total": _build_state["total"],
|
||||||
|
"last_ms": _build_state["last_ms"],
|
||||||
|
"cached": _mem["raw"] is not None,
|
||||||
|
"cache_bytes": len(_mem["raw"]) if _mem["raw"] else 0,
|
||||||
|
"gz_bytes": len(_mem["gz"]) if _mem["gz"] else 0,
|
||||||
|
}
|
||||||
|
if _build_state["building"] and _build_state["started"]:
|
||||||
|
st["elapsed"] = round(time.time() - _build_state["started"], 1)
|
||||||
|
return web.json_response(st)
|
||||||
|
|
||||||
|
|
||||||
@PromptServer.instance.routes.post("/tenaciousload/refresh")
|
@PromptServer.instance.routes.post("/tenaciousload/refresh")
|
||||||
async def _refresh(request):
|
async def _refresh(request):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
|
|
||||||
|
// Shows a small status line on ComfyUI's "Comfy" loading splash so a long
|
||||||
|
// object_info build (or cache load) isn't a silent black screen.
|
||||||
|
|
||||||
|
const START = performance.now();
|
||||||
|
let overlay = null;
|
||||||
|
let ready = false;
|
||||||
|
let lastDone = -1;
|
||||||
|
let stallTicks = 0;
|
||||||
|
|
||||||
|
function injectStyle() {
|
||||||
|
if (document.getElementById("tl-loading-style")) return;
|
||||||
|
const s = document.createElement("style");
|
||||||
|
s.id = "tl-loading-style";
|
||||||
|
s.textContent = `
|
||||||
|
@keyframes tl-indet { 0%{left:-35%;width:35%} 50%{left:40%;width:45%} 100%{left:100%;width:35%} }
|
||||||
|
#tl-loading{position:fixed;left:0;right:0;top:57%;margin:0 auto;z-index:99999;width:340px;
|
||||||
|
text-align:center;font-family:system-ui,-apple-system,sans-serif;color:#a1a1aa;pointer-events:none;
|
||||||
|
transition:opacity .35s ease}
|
||||||
|
#tl-loading .tl-msg{font-size:12.5px;letter-spacing:.3px;line-height:1.5}
|
||||||
|
#tl-loading .tl-sub{font-size:11px;color:#71717a;margin-top:2px}
|
||||||
|
#tl-loading .tl-track{position:relative;margin:11px auto 0;width:240px;height:4px;
|
||||||
|
background:#27272a;border-radius:3px;overflow:hidden}
|
||||||
|
#tl-loading .tl-bar{position:absolute;top:0;height:100%;border-radius:3px;
|
||||||
|
background:linear-gradient(90deg,#60a5fa,#a78bfa,#ff9cf9)}
|
||||||
|
#tl-loading .tl-bar.indet{animation:tl-indet 1.15s ease-in-out infinite}`;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureOverlay() {
|
||||||
|
if (overlay || ready) return;
|
||||||
|
injectStyle();
|
||||||
|
overlay = document.createElement("div");
|
||||||
|
overlay.id = "tl-loading";
|
||||||
|
overlay.innerHTML =
|
||||||
|
'<div class="tl-msg" id="tl-msg">Tenaciousload</div>' +
|
||||||
|
'<div class="tl-sub" id="tl-sub"></div>' +
|
||||||
|
'<div class="tl-track"><div class="tl-bar indet" id="tl-bar" style="left:0;width:35%"></div></div>';
|
||||||
|
(document.body || document.documentElement).appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(s) {
|
||||||
|
s = Math.max(0, Math.round(s));
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
return m ? `${m}:${String(s % 60).padStart(2, "0")}` : `${s}s`;
|
||||||
|
}
|
||||||
|
const mb = (b) => (b / 1048576).toFixed(1) + " MB";
|
||||||
|
|
||||||
|
async function tick() {
|
||||||
|
if (ready) return;
|
||||||
|
if (!overlay && performance.now() - START > 700) ensureOverlay();
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
let st = null;
|
||||||
|
try { st = await (await api.fetchApi("/tenaciousload/status")).json(); } catch (e) { /* ignore */ }
|
||||||
|
const msg = overlay.querySelector("#tl-msg");
|
||||||
|
const sub = overlay.querySelector("#tl-sub");
|
||||||
|
const bar = overlay.querySelector("#tl-bar");
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
if (st && st.building) {
|
||||||
|
const pct = st.total ? Math.round((100 * st.done) / st.total) : 0;
|
||||||
|
msg.textContent = `Building node definitions — ${st.done}/${st.total} (${pct}%)`;
|
||||||
|
// detect a stall (the model-folder walk) and explain it
|
||||||
|
stallTicks = st.done === lastDone ? stallTicks + 1 : 0;
|
||||||
|
lastDone = st.done;
|
||||||
|
sub.textContent = stallTicks >= 3
|
||||||
|
? `scanning model folders over the network… · ${fmt(st.elapsed || 0)}`
|
||||||
|
: `first build / refresh · ${fmt(st.elapsed || 0)}`;
|
||||||
|
bar.classList.add("indet");
|
||||||
|
bar.style.width = "35%";
|
||||||
|
} else {
|
||||||
|
msg.textContent = "Loading node definitions…";
|
||||||
|
sub.textContent = st && st.cached ? `from cache · ${mb(st.gz_bytes || st.cache_bytes || 0)} gzipped` : "";
|
||||||
|
bar.classList.remove("indet");
|
||||||
|
bar.style.left = "0";
|
||||||
|
bar.style.width = "90%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = setInterval(tick, 600);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Tenaciousload.LoadingStatus",
|
||||||
|
// setup() fires once the app is ready (object_info loaded, nodes registered).
|
||||||
|
async setup() {
|
||||||
|
ready = true;
|
||||||
|
clearInterval(poll);
|
||||||
|
if (overlay) {
|
||||||
|
overlay.style.opacity = "0";
|
||||||
|
setTimeout(() => overlay && overlay.remove(), 350);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// hard safety: never let the overlay linger
|
||||||
|
setTimeout(() => { if (overlay && !ready) { clearInterval(poll); overlay.remove(); } }, 15 * 60 * 1000);
|
||||||
Reference in New Issue
Block a user