Files
olivv-cs-ComfyUI-FunPack/app/legacy/log.js
T
digital-garbageandClaude Sonnet 5 29ac44ffcd feat: v4 UI port Phase A start -- module categories, legacy frontend served live
Two pieces of the "port v4's exact UI onto v5's backend" plan:

- core/contract.py: new optional ModuleSpec.category field (closed set:
  continuity, guidance, conditioning, sampling, post, system), validated in
  core/schema.py, threaded through core/registry.py's FIELDS and
  to_manifest(). Presentation grouping, independent of STAGE (execution
  order) -- a module can be stage="sampling" and category="guidance" at
  once. Only the 4 existing modules that actually render a settings panel
  (alg, sharpen, appearance, full_control) got a category assigned; the
  other 12 have none to categorize (no SETTINGS block) and stay
  uncategorised, which is valid.

- v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied
  verbatim into app/legacy/. core/routes.py's "/" route now serves
  legacy/index.html instead of the composer entry point; a <base> tag in
  that file resolves its own relative script/link tags against
  /funpack/app/legacy/ regardless of what URL it's served at, so every
  file moved as a unit with zero per-file path edits. Verified live: all
  61 assets load 200 OK, the real v4 onboarding screen renders pixel-for-
  pixel (it's the actual DOM/CSS, not a reimplementation), and several v4
  API calls (/api/media, /api/projects, /api/git/status) already succeed
  against v5's real backend by coincidence of shared route naming -- the
  rest 404/405 as expected, since api.js's ~90 functions haven't been
  rewired to v5's actual routes yet (next piece of this plan).

The old composer entry point (app/index.html, app/composer/**) stays in
the tree, unreferenced, until a later cleanup pass once nothing imports it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:58:51 +03:00

196 lines
6.9 KiB
JavaScript

// Menubar "Log" button + a collapsible panel showing ComfyUI's real backend log
// (tee'd stdout/stderr from the server). Polls only while open. Freezes during selection to preserve user interaction.
(function () {
const API = window.MovieEditorAPI;
const right = document.querySelector(".menubar-right");
if (!right) return;
const btn = document.createElement("button");
btn.className = "log-btn"; btn.textContent = "Log"; btn.title = "Show the ComfyUI backend log";
right.insertBefore(btn, right.firstChild);
const panel = document.createElement("div");
panel.className = "log-panel"; panel.hidden = true;
const head = document.createElement("div"); head.className = "log-head";
const title = document.createElement("span"); title.textContent = "ComfyUI log";
const actions = document.createElement("div"); actions.className = "log-actions";
head.append(title, actions);
const bodyEl = document.createElement("pre"); bodyEl.className = "log-body";
panel.append(head, bodyEl);
document.body.append(panel);
let timer = null, autoscroll = true, lastText = "", isFrozen = false, isManuallyPaused = false, pendingText = null;
// Set when a poll fails. Held so the panel keeps the last lines the server managed to send
// instead of replacing them with the error that means the server is gone.
let disconnected = false;
// Helper to check if a node is inside bodyEl (handles text nodes too).
function nodeInBody(node) {
return node === bodyEl || bodyEl.contains(node);
}
// Helper to check if user has a non-collapsed selection inside bodyEl.
function selectionInBody() {
const sel = window.getSelection();
if (sel.isCollapsed) return false;
return nodeInBody(sel.anchorNode) || nodeInBody(sel.focusNode);
}
async function refresh() {
if (isManuallyPaused) return;
try {
const r = await API.log(800);
const newText = (r.lines || []).join("\n");
// Skip DOM write if text hasn't changed — unless we are coming back from a
// disconnect, where the body still carries the "connection lost" note to clear.
if (newText === lastText && !disconnected) return;
if (disconnected) {
disconnected = false;
updateConnectionIndicator();
}
// If user is selecting, freeze updates and keep polling. textContent assignment
// destroys the text node tree, which nukes the selection mid-copy, so we defer the
// write until the selection is released or moves out of the log.
if (selectionInBody()) {
isFrozen = true;
pendingText = newText;
updateFreezeIndicator();
return;
}
// Update is safe to apply.
lastText = newText;
render(newText);
if (isFrozen) {
isFrozen = false;
pendingText = null;
updateFreezeIndicator();
}
} catch (e) {
// KEEP what we already have. The log going unavailable almost always means the backend
// just died — which is the one moment its last lines matter most, and replacing them
// with the error destroyed the evidence (and what Copy would have put on the
// clipboard). Say it is disconnected; do not wipe it.
if (!disconnected) {
disconnected = true;
render(lastText, "\n\n— connection lost (" + (e && e.message ? e.message : "backend "
+ "unreachable") + "). The lines above are the last the server sent. —");
updateConnectionIndicator();
}
}
}
// The body is the last good log plus an optional trailing note. `lastText` stays the log
// ALONE, so Copy hands over the log rather than the log plus our commentary.
function render(text, note) {
bodyEl.textContent = text + (note || "");
if (autoscroll) bodyEl.scrollTop = bodyEl.scrollHeight;
}
function updateConnectionIndicator() {
const existing = actions.querySelector(".log-disconnected");
if (disconnected) {
if (!existing) {
const chip = document.createElement("span");
chip.className = "log-freeze-indicator log-disconnected";
chip.textContent = "disconnected — log held";
actions.insertBefore(chip, actions.firstChild);
}
} else if (existing) {
existing.remove();
}
}
bodyEl.addEventListener("scroll", () => {
autoscroll = bodyEl.scrollHeight - bodyEl.scrollTop - bodyEl.clientHeight < 40;
});
function updateFreezeIndicator() {
const existingIndicator = actions.querySelector(".log-freeze-indicator");
if (isFrozen) {
if (!existingIndicator) {
const indicator = document.createElement("span");
indicator.className = "log-freeze-indicator";
indicator.textContent = "paused — selection held";
actions.insertBefore(indicator, actions.firstChild);
}
} else {
if (existingIndicator) existingIndicator.remove();
}
}
function open() {
panel.hidden = false;
btn.classList.add("active");
autoscroll = true;
isFrozen = false;
isManuallyPaused = false;
pendingText = null;
updatePauseButton();
updateFreezeIndicator();
updateConnectionIndicator();
refresh();
timer = setInterval(refresh, 1500);
}
function close() {
panel.hidden = true;
btn.classList.remove("active");
clearInterval(timer);
timer = null;
isFrozen = false;
isManuallyPaused = false;
pendingText = null;
updateFreezeIndicator();
}
btn.onclick = () => (panel.hidden ? open() : close());
// Helper to get the currently selected text within bodyEl.
function getSelectedText() {
const sel = window.getSelection();
if (sel.isCollapsed) return null;
if (!nodeInBody(sel.anchorNode) && !nodeInBody(sel.focusNode)) return null;
return sel.toString();
}
const copy = document.createElement("button");
copy.className = "btn ghost tiny";
copy.textContent = "Copy";
copy.onclick = () => {
try {
const selectedText = getSelectedText();
const textToCopy = selectedText || lastText;
navigator.clipboard.writeText(textToCopy);
copy.textContent = selectedText ? "Copied selection" : "Copied";
setTimeout(() => (copy.textContent = "Copy"), 1200);
} catch (_) {}
};
const pauseBtn = document.createElement("button");
pauseBtn.className = "btn ghost tiny";
pauseBtn.textContent = "⏸ Pause";
pauseBtn.onclick = () => {
isManuallyPaused = !isManuallyPaused;
updatePauseButton();
// Refetch rather than flushing pendingText: nothing was polled while paused, so the
// only text held is whatever a selection froze earlier and it may be minutes stale.
if (!isManuallyPaused) { pendingText = null; refresh(); }
};
function updatePauseButton() {
pauseBtn.textContent = isManuallyPaused ? "▶ Resume" : "⏸ Pause";
pauseBtn.classList.toggle("on", isManuallyPaused);
}
const x = document.createElement("button");
x.className = "btn ghost tiny";
x.textContent = "✕";
x.onclick = close;
actions.append(copy, pauseBtn, x);
})();