Files
JasonHoku-ComfyUI-Ultimate-…/resources/logic_standalone.js
T
Jason HokuandClaude Fable 5.1 660bdc17cf feat(dashboard): standalone page mode, own live websocket, open-standalone button
html_generator gains a standalone flag (USCG_STANDALONE) and escapes "</" in
the inlined manifest. New logic_standalone.js (loaded last) replaces every
parent-frame dependency: own ComfyUI websocket re-dispatching the bridge's
message shapes (session filter, id dedupe, backoff, resync on resume),
Fullscreen API, URL kept on the shown session, GENERATE NEW via
POST /config_tester/run, and an openStandalone() header button for the node
iframe. Lightbox gets modalNav/toggleModalFavorite/rejectModalItem helpers
and an action bar element (styled in the next commit).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 23:58:33 -08:00

171 lines
7.3 KiB
JavaScript

/**
* STANDALONE MODE (dashboard opened as a top-level page: /config_tester/dashboard)
* Everything that talks to the ComfyUI tab (postMessage bridge in web/dashboard.js) is replaced here:
* - live updates: own ComfyUI websocket, re-dispatched as the SAME message shapes the bridge sends,
* so logic_events.js renders them with no duplicated logic
* - "GENERATE NEW" in the lightbox queues through POST /config_tester/run
* - the node iframe's header button opens this page via openStandalone()
* Loaded LAST (after logic_init.js). All behaviour is inside `if (USCG_STANDALONE)`; the node iframe is untouched.
*/
// Header button (node iframe only): open this dashboard as its own page.
function openStandalone() {
var sess = (document.getElementById('session-input') || {}).value || '';
var base = document.baseURI;
if (!base || base.indexOf('http') !== 0) {
try { base = window.parent.location.href; } catch (e) { base = ''; }
}
var url;
try {
url = new URL('/config_tester/dashboard' + (sess ? '?session=' + encodeURIComponent(sess) : ''), base).href;
} catch (e) {
alert('Could not work out the dashboard URL: ' + e.message);
return;
}
// No `noopener` feature: it makes window.open() always return null, hiding blocked popups.
var w = window.open(url, '_blank');
if (w) {
try { w.opener = null; } catch (e) { /* cross-origin guard */ }
} else {
prompt('Open this URL in your browser', url);
}
}
// Lightbox "GENERATE NEW" without a ComfyUI graph: queue via the run API into the session being viewed.
async function uscgRunViaApi(btn, configOutput) {
var sess = window._uscgLoadedSession || (document.getElementById('session-input') || {}).value || '';
var original = btn ? btn.innerText : '';
var flash = function (text, ms) {
if (!btn) return;
btn.innerText = text;
setTimeout(function () { btn.innerText = original || 'GENERATE NEW'; }, ms);
};
try {
var body = { configs_json: configOutput, force: true };
if (sess) body.session_name = sess;
var r = await fetch('/config_tester/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
var j = null;
try { j = await r.json(); } catch (e) { /* non-JSON error body */ }
if (!r.ok) {
var msg = j && j.error ? (typeof j.error === 'string' ? j.error : (j.error.message || JSON.stringify(j.error))) : ('HTTP ' + r.status);
console.error('[Revise] /config_tester/run failed:', j);
flash('ERROR: ' + String(msg).slice(0, 60), 3000);
return;
}
console.log('[Revise] queued via /config_tester/run:', j.prompt_id);
flash('QUEUED!', 1000);
} catch (e) {
console.error('[Revise] run API error:', e);
flash('ERROR: ' + (e && e.message ? e.message : e), 3000);
}
}
if (USCG_STANDALONE) {
(function () {
document.body.classList.add('uscg-standalone');
var openBtn = document.getElementById('open-standalone-btn');
if (openBtn) openBtn.style.display = 'none';
var pack = document.getElementById('pack-workflow-checkbox');
if (pack) {
pack.checked = false;
pack.disabled = true;
pack.title = 'Needs the ComfyUI tab (open the dashboard from the node to pack the workflow)';
}
// The session this page currently shows ('' on the landing page).
var titleEl = document.getElementById('session-input');
window._uscgLoadedSession = (titleEl && titleEl.value) ? titleEl.value : '';
if (window._uscgLoadedSession) document.title = 'USCG - ' + window._uscgLoadedSession;
function knownIds() {
var s = new Set();
var items = (typeof fullManifest !== 'undefined' && fullManifest && fullManifest.items) || [];
for (var i = 0; i < items.length; i++) s.add(items[i].id);
return s;
}
function dispatch(type, data) {
window.postMessage({ type: type, data: data }, location.origin);
}
function onEvent(name, data) {
if (!data) return;
var isUpdate = (name === 'ultimate_grid.update' || name === 'ultimate_grid.update_data');
var isProgress = (name === 'ultimate_grid.progress');
if (!isUpdate && !isProgress) return;
// Landing page: follow a newly started session (mirrors the node bridge's "no session loaded" case).
if (!window._uscgLoadedSession) {
if (name === 'ultimate_grid.update' && data.session_name) {
loadSessionFromPicker(data.session_name);
}
return;
}
if (data.session_name !== window._uscgLoadedSession) return;
if (isProgress) { dispatch('progress_update', data); return; }
if (data.new_items && data.new_items.length) {
// processNewData does not dedupe: a resume reload plus a late event would double the cards.
var have = knownIds();
var fresh = data.new_items.filter(function (it) { return !have.has(it.id); });
if (!fresh.length) return;
var copy = {};
for (var k in data) copy[k] = data[k];
copy.new_items = fresh;
dispatch('update_data', copy);
} else if (data.manifest) {
dispatch('update_data', data);
}
}
// ---- websocket with backoff + resync on resume -------------------------------------
var ws = null, backoff = 1000, dropped = false, resyncTimer = null;
var clientId = 'uscg-dash-' + Math.random().toString(16).slice(2, 10) + Date.now().toString(16);
function resync() {
if (!window._uscgLoadedSession) return;
if (resyncTimer) return; // debounce: a reconnect and a visibility change must not both reload
resyncTimer = setTimeout(function () {
resyncTimer = null;
if (navigator.onLine === false) return;
loadSession();
}, 2000);
}
function connect() {
var url = (location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws?clientId=' + clientId;
try { ws = new WebSocket(url); } catch (e) { schedule(); return; }
ws.onopen = function () {
backoff = 1000;
if (dropped) { dropped = false; resync(); }
};
ws.onmessage = function (ev) {
if (typeof ev.data !== 'string') return; // binary preview frames
var msg;
try { msg = JSON.parse(ev.data); } catch (e) { return; }
if (msg && msg.type) onEvent(msg.type, msg.data);
};
ws.onclose = function () { dropped = true; schedule(); };
ws.onerror = function () { try { ws.close(); } catch (e) { /* already closed */ } };
}
function schedule() {
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, 30000);
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') resync();
});
connect();
})();
}