refactor(web): split main.js into focused modules and shared constants

This commit is contained in:
Robert Wojciechowski
2026-02-23 04:38:04 +00:00
parent f89a0cc711
commit c1010dcd3c
8 changed files with 1142 additions and 1076 deletions
+46
View File
@@ -168,3 +168,49 @@ export const TIMEOUTS = {
LOG_REFRESH: 2000, // log auto-refresh interval
IMAGE_CACHE_CLEAR: 30000 // delay before clearing image cache
};
export const ENDPOINTS = {
// ComfyUI core
PROMPT: '/prompt',
INTERRUPT: '/interrupt',
UPLOAD_IMAGE: '/upload/image',
SYSTEM_INFO: '/system_stats',
// Distributed API
CONFIG: '/distributed/config',
UPDATE_WORKER: '/distributed/config/update_worker',
DELETE_WORKER: '/distributed/config/delete_worker',
UPDATE_SETTING: '/distributed/config/update_setting',
UPDATE_MASTER: '/distributed/config/update_master',
LAUNCH_WORKER: '/distributed/launch_worker',
STOP_WORKER: '/distributed/stop_worker',
MANAGED_WORKERS: '/distributed/managed_workers',
WORKER_LOG: '/distributed/worker_log',
CLEAR_LAUNCHING: '/distributed/worker/clear_launching',
PREPARE_JOB: '/distributed/prepare_job',
LOAD_IMAGE: '/distributed/load_image',
NETWORK_INFO: '/distributed/network_info',
CHECK_FILE: '/distributed/check_file',
CLEAR_MEMORY: '/distributed/clear_memory',
SYSTEM_INFO_DIST: '/distributed/system_info',
TUNNEL_START: '/distributed/tunnel/start',
TUNNEL_STOP: '/distributed/tunnel/stop',
TUNNEL_STATUS: '/distributed/tunnel/status',
};
export const NODE_CLASSES = {
DISTRIBUTED_COLLECTOR: 'DistributedCollector',
DISTRIBUTED_QUEUE: 'DistributedQueue',
DISTRIBUTED_SEED: 'DistributedSeed',
DISTRIBUTED_EMPTY_IMAGE: 'DistributedEmptyImage',
UPSCALE_DISTRIBUTED: 'UltimateSDUpscaleDistributed',
PREVIEW_IMAGE: 'PreviewImage',
};
export function generateUUID() {
if (crypto.randomUUID) return crypto.randomUUID();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
+14 -25
View File
@@ -1,6 +1,6 @@
import { api } from "../../scripts/api.js";
import { findNodesByClass, findImageReferences, hasUpstreamNode, pruneWorkflowForWorker, getCachedWorkerSystemInfo, findCollectorDownstreamNodes } from './workerUtils.js';
import { TIMEOUTS } from './constants.js';
import { TIMEOUTS, NODE_CLASSES, generateUUID } from './constants.js';
/**
* Convert paths in the API prompt to match the target platform's separator
@@ -58,9 +58,9 @@ function convertPathsForPlatform(apiPrompt, targetSeparator) {
export function setupInterceptor(extension) {
api.queuePrompt = async (number, prompt) => {
if (extension.isEnabled) {
const hasCollector = findNodesByClass(prompt.output, "DistributedCollector").length > 0;
const hasDistUpscale = findNodesByClass(prompt.output, "UltimateSDUpscaleDistributed").length > 0;
const hasDistQueue = findNodesByClass(prompt.output, "DistributedQueue").length > 0;
const hasCollector = findNodesByClass(prompt.output, NODE_CLASSES.DISTRIBUTED_COLLECTOR).length > 0;
const hasDistUpscale = findNodesByClass(prompt.output, NODE_CLASSES.UPSCALE_DISTRIBUTED).length > 0;
const hasDistQueue = findNodesByClass(prompt.output, NODE_CLASSES.DISTRIBUTED_QUEUE).length > 0;
if (hasCollector || hasDistUpscale) {
const result = await executeParallelDistributed(extension, prompt);
@@ -137,8 +137,8 @@ export async function executeParallelDistributed(extension, promptWrapper) {
}
// Find all distributed nodes in the workflow
const collectorNodes = findNodesByClass(promptWrapper.output, "DistributedCollector");
const upscaleNodes = findNodesByClass(promptWrapper.output, "UltimateSDUpscaleDistributed");
const collectorNodes = findNodesByClass(promptWrapper.output, NODE_CLASSES.DISTRIBUTED_COLLECTOR);
const upscaleNodes = findNodesByClass(promptWrapper.output, NODE_CLASSES.UPSCALE_DISTRIBUTED);
const allDistributedNodes = [...collectorNodes, ...upscaleNodes];
const wantsDelegate = Boolean(extension.config?.settings?.master_delegate_only);
@@ -322,7 +322,7 @@ function selectLeastBusyWorker(extension, statuses) {
function markSkipDispatch(promptObj) {
for (const node of Object.values(promptObj)) {
if (node && typeof node === 'object' && node.class_type === 'DistributedQueue') {
if (node && typeof node === 'object' && node.class_type === NODE_CLASSES.DISTRIBUTED_QUEUE) {
node.inputs = node.inputs || {};
node.inputs.skip_dispatch = true;
}
@@ -335,8 +335,8 @@ export async function prepareApiPromptForParticipant(extension, baseApiPrompt, p
const delegateMaster = Boolean(options.delegate_master);
// Find all distributed nodes once (before pruning)
let collectorNodes = findNodesByClass(jobApiPrompt, "DistributedCollector");
const upscaleNodes = findNodesByClass(jobApiPrompt, "UltimateSDUpscaleDistributed");
let collectorNodes = findNodesByClass(jobApiPrompt, NODE_CLASSES.DISTRIBUTED_COLLECTOR);
const upscaleNodes = findNodesByClass(jobApiPrompt, NODE_CLASSES.UPSCALE_DISTRIBUTED);
let allDistributedNodes = [...collectorNodes, ...upscaleNodes];
if (isMaster && delegateMaster) {
@@ -346,7 +346,7 @@ export async function prepareApiPromptForParticipant(extension, baseApiPrompt, p
extension.log("Delegate-only master mode requested but no collectors found in master prompt. Using full prompt.", "debug");
} else {
jobApiPrompt = prepareMasterDelegatePrompt(extension, jobApiPrompt, collectorNodes);
collectorNodes = findNodesByClass(jobApiPrompt, "DistributedCollector");
collectorNodes = findNodesByClass(jobApiPrompt, NODE_CLASSES.DISTRIBUTED_COLLECTOR);
allDistributedNodes = [...collectorNodes, ...upscaleNodes];
}
}
@@ -400,7 +400,7 @@ export async function prepareApiPromptForParticipant(extension, baseApiPrompt, p
}
// Handle Distributed seed nodes
const distributorNodes = findNodesByClass(jobApiPrompt, "DistributedSeed");
const distributorNodes = findNodesByClass(jobApiPrompt, NODE_CLASSES.DISTRIBUTED_SEED);
if (distributorNodes.length > 0) {
extension.log(`Found ${distributorNodes.length} seed node(s)`, "debug");
}
@@ -423,7 +423,7 @@ export async function prepareApiPromptForParticipant(extension, baseApiPrompt, p
const hasUpstreamDistributedUpscaler = hasUpstreamNode(
jobApiPrompt,
collector.id,
'UltimateSDUpscaleDistributed'
NODE_CLASSES.UPSCALE_DISTRIBUTED
);
if (hasUpstreamDistributedUpscaler) {
@@ -536,7 +536,7 @@ function prepareMasterDelegatePrompt(extension, apiPrompt, collectorNodes) {
const placeholderId = nextId();
prunedPrompt[placeholderId] = {
class_type: "DistributedEmptyImage",
class_type: NODE_CLASSES.DISTRIBUTED_EMPTY_IMAGE,
inputs: {
height: 64,
width: 64,
@@ -660,19 +660,8 @@ function buildWorkerWebSocketUrl(workerUrl) {
return url.toString();
}
function generateRequestId() {
if (crypto.randomUUID) {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
async function dispatchPromptViaWebSocket(extension, wsUrl, payload) {
const requestId = generateRequestId();
const requestId = generateUUID();
const message = {
type: 'dispatch_prompt',
request_id: requestId,
+30 -912
View File
File diff suppressed because it is too large Load Diff
+67 -132
View File
@@ -1,25 +1,57 @@
import { BUTTON_STYLES } from './constants.js';
export async function renderSidebarContent(extension, el) {
// Panel is being opened/rendered
extension.log("Panel opened", "debug");
if (!el) {
extension.log("No element provided to renderSidebarContent", "debug");
return;
}
// Prevent infinite recursion
if (extension._isRendering) {
extension.log("Already rendering, skipping", "debug");
return;
}
extension._isRendering = true;
function createCheckboxSetting(id, label, tooltip, checked, onChange) {
const group = document.createElement("div");
group.style.cssText = "grid-column: 1 / span 2; display: flex; align-items: center; gap: 8px;";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = id;
checkbox.checked = checked;
checkbox.onchange = onChange;
const lbl = document.createElement("label");
lbl.htmlFor = id;
lbl.textContent = label;
lbl.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;";
if (tooltip) lbl.title = tooltip;
group.appendChild(checkbox);
group.appendChild(lbl);
return group;
}
function createNumberSetting(id, label, tooltip, value, min, step, onChange) {
const group = document.createElement("div");
group.style.cssText = "grid-column: 1 / span 2; display: flex; align-items: center; gap: 6px;";
const lbl = document.createElement("label");
lbl.htmlFor = id;
lbl.textContent = label;
lbl.style.cssText = "font-size: 12px; color: #ccc;";
if (tooltip) lbl.title = tooltip;
const input = document.createElement("input");
input.type = "number";
input.id = id;
input.min = String(min);
input.step = String(step);
input.style.cssText = "width: 80px; padding: 2px 6px; background: #222; color: #ddd; border: 1px solid #333; border-radius: 3px;";
input.value = value;
input.onchange = onChange;
group.appendChild(lbl);
group.appendChild(input);
return group;
}
try {
// Store reference to the panel element
extension.panelElement = el;
// Show loading indicator
el.innerHTML = '';
const loadingDiv = document.createElement("div");
@@ -28,7 +60,6 @@ export async function renderSidebarContent(extension, el) {
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="40 40"/>
</svg>`;
el.appendChild(loadingDiv);
// Add rotation animation
const style = document.createElement('style');
style.textContent = `
@@ -39,93 +70,71 @@ export async function renderSidebarContent(extension, el) {
`;
document.head.appendChild(style);
loadingDiv.querySelector('svg').style.animation = 'rotate 1s linear infinite';
// Preload data outside render
await extension.loadConfig();
await extension.loadManagedWorkers();
await extension.refreshTunnelStatus();
extension.tunnelElements = {};
el.innerHTML = '';
// Create toolbar header to match ComfyUI style
const toolbar = document.createElement("div");
toolbar.className = "p-toolbar p-component border-x-0 border-t-0 rounded-none px-2 py-1 min-h-8";
toolbar.style.cssText = "border-bottom: 1px solid #444; background: transparent; display: flex; align-items: center;";
const toolbarStart = document.createElement("div");
toolbarStart.className = "p-toolbar-start";
toolbarStart.style.cssText = "display: flex; align-items: center;";
const titleSpan = document.createElement("span");
titleSpan.className = "text-xs 2xl:text-sm truncate";
titleSpan.textContent = "COMFYUI DISTRIBUTED";
titleSpan.title = "ComfyUI Distributed";
toolbarStart.appendChild(titleSpan);
toolbar.appendChild(toolbarStart);
const toolbarCenter = document.createElement("div");
toolbarCenter.className = "p-toolbar-center";
toolbar.appendChild(toolbarCenter);
const toolbarEnd = document.createElement("div");
toolbarEnd.className = "p-toolbar-end";
toolbar.appendChild(toolbarEnd);
el.appendChild(toolbar);
// Main container with adjusted padding
const container = document.createElement("div");
container.style.cssText = "padding: 15px; display: flex; flex-direction: column; height: calc(100% - 32px);";
// Detect master info on panel open (in case CUDA info wasn't available at startup)
extension.log(`Panel opened. CUDA device count: ${extension.cudaDeviceCount}, Workers: ${extension.config?.workers?.length || 0}`, "debug");
if (!extension.cudaDeviceCount) {
await extension.detectMasterIP();
}
// Now render with guaranteed up-to-date config
// Master Node Section
const masterDiv = extension.ui.renderEntityCard('master', extension.config?.master, extension);
container.appendChild(masterDiv);
// Workers Section (no heading)
const gpuSection = document.createElement("div");
gpuSection.style.cssText = "flex: 1; overflow-y: auto; margin-bottom: 15px;";
const gpuList = document.createElement("div");
const workers = extension.config?.workers || [];
// If no workers exist, show a full blueprint placeholder first
if (workers.length === 0) {
const blueprintDiv = extension.ui.renderEntityCard('blueprint', { onClick: () => extension.addNewWorker() }, extension);
gpuList.appendChild(blueprintDiv);
}
// Show existing workers
workers.forEach(worker => {
const gpuDiv = extension.ui.renderEntityCard('worker', worker, extension);
gpuList.appendChild(gpuDiv);
});
gpuSection.appendChild(gpuList);
// Only show the minimal "Add Worker" box if there are existing workers
if (workers.length > 0) {
const addWorkerDiv = extension.ui.renderEntityCard('add', { onClick: () => extension.addNewWorker() }, extension);
gpuSection.appendChild(addWorkerDiv);
}
container.appendChild(gpuSection);
const actionsSection = document.createElement("div");
actionsSection.style.cssText = "padding-top: 10px; margin-bottom: 15px; border-top: 1px solid #444;";
// Create a row for both buttons
const buttonRow = document.createElement("div");
buttonRow.style.cssText = "display: flex; gap: 8px;";
const clearMemButton = extension.ui.createButtonHelper(
"Clear Worker VRAM",
(e) => extension._handleClearMemory(e.target),
@@ -133,7 +142,6 @@ export async function renderSidebarContent(extension, el) {
);
clearMemButton.title = "Clear VRAM on all enabled worker GPUs (not master)";
clearMemButton.style.cssText = BUTTON_STYLES.base + " flex: 1;" + BUTTON_STYLES.clearMemory;
const interruptButton = extension.ui.createButtonHelper(
"Interrupt Workers",
(e) => extension._handleInterruptWorkers(e.target),
@@ -141,23 +149,18 @@ export async function renderSidebarContent(extension, el) {
);
interruptButton.title = "Cancel/interrupt execution on all enabled worker GPUs";
interruptButton.style.cssText = BUTTON_STYLES.base + " flex: 1;" + BUTTON_STYLES.interrupt;
buttonRow.appendChild(clearMemButton);
buttonRow.appendChild(interruptButton);
actionsSection.appendChild(buttonRow);
container.appendChild(actionsSection);
// Settings section
const settingsSection = document.createElement("div");
// Top separator only; spacing handled by the clickable toggle area for equal top/bottom spacing
settingsSection.style.cssText = "border-top: 1px solid #444; margin-bottom: 10px;";
// Settings header with toggle (full-area clickable between separators)
const settingsToggleArea = document.createElement("div");
// Equal spacing above header (to top separator) and below header (to bottom separator)
settingsToggleArea.style.cssText = "padding: 16.5px 0; cursor: pointer; user-select: none;";
const settingsHeader = document.createElement("div");
settingsHeader.style.cssText = "display: flex; align-items: center; justify-content: space-between;";
const workerSettingsTitle = document.createElement("h4");
@@ -172,19 +175,15 @@ export async function renderSidebarContent(extension, el) {
// Hover effect for toggle area
settingsToggleArea.onmouseover = () => { workerSettingsToggle.style.color = "#fff"; };
settingsToggleArea.onmouseout = () => { workerSettingsToggle.style.color = "#888"; };
// A small separator shown only when collapsed (to make the section boundary obvious)
const settingsSeparator = document.createElement("div");
// No margin so the bottom spacing is controlled by settingsToggleArea padding-bottom
settingsSeparator.style.cssText = "border-bottom: 1px solid #444; margin: 0;";
// Collapsible settings content
const settingsContent = document.createElement("div");
settingsContent.style.cssText = "max-height: 0; overflow: hidden; opacity: 0; transition: max-height 0.3s ease, opacity 0.3s ease;";
const settingsDiv = document.createElement("div");
settingsDiv.style.cssText = "display: grid; grid-template-columns: 1fr auto; row-gap: 10px; column-gap: 10px; padding-top: 10px; align-items: center;";
// Toggle functionality
let settingsExpanded = false;
settingsToggleArea.onclick = () => {
@@ -201,115 +200,51 @@ export async function renderSidebarContent(extension, el) {
settingsSeparator.style.display = "block";
}
};
// Debug mode setting
// Section: General
const generalLabel = document.createElement("div");
generalLabel.textContent = "GENERAL";
generalLabel.style.cssText = "grid-column: 1 / span 2; font-size: 11px; color: #888; letter-spacing: 0.06em; padding-top: 2px;";
const debugGroup = document.createElement("div");
debugGroup.style.cssText = "grid-column: 1 / span 2; display: flex; align-items: center; gap: 8px;";
const debugCheckbox = document.createElement("input");
debugCheckbox.type = "checkbox";
debugCheckbox.id = "setting-debug";
debugCheckbox.checked = extension.config?.settings?.debug || false;
debugCheckbox.onchange = (e) => extension._updateSetting('debug', e.target.checked);
const debugLabel = document.createElement("label");
debugLabel.htmlFor = "setting-debug";
debugLabel.textContent = "Debug Mode";
debugLabel.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;";
debugLabel.title = "Enable verbose logging in the browser console.";
debugGroup.appendChild(debugCheckbox);
debugGroup.appendChild(debugLabel);
// Auto-launch workers setting
const autoLaunchGroup = document.createElement("div");
autoLaunchGroup.style.cssText = "grid-column: 1 / span 2; display: flex; align-items: center; gap: 8px;";
const autoLaunchCheckbox = document.createElement("input");
autoLaunchCheckbox.type = "checkbox";
autoLaunchCheckbox.id = "setting-auto-launch";
autoLaunchCheckbox.checked = extension.config?.settings?.auto_launch_workers || false;
autoLaunchCheckbox.onchange = (e) => extension._updateSetting('auto_launch_workers', e.target.checked);
const autoLaunchLabel = document.createElement("label");
autoLaunchLabel.htmlFor = "setting-auto-launch";
autoLaunchLabel.textContent = "Auto-launch Local Workers on Startup";
autoLaunchLabel.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;";
autoLaunchLabel.title = "Start local worker processes automatically when the master starts.";
autoLaunchGroup.appendChild(autoLaunchCheckbox);
autoLaunchGroup.appendChild(autoLaunchLabel);
// Stop workers on exit setting (under General)
const stopOnExitGroup = document.createElement("div");
stopOnExitGroup.style.cssText = "grid-column: 1 / span 2; display: flex; align-items: center; gap: 8px;";
const stopOnExitCheckbox = document.createElement("input");
stopOnExitCheckbox.type = "checkbox";
stopOnExitCheckbox.id = "setting-stop-on-exit";
stopOnExitCheckbox.checked = extension.config?.settings?.stop_workers_on_master_exit !== false; // Default true
stopOnExitCheckbox.onchange = (e) => extension._updateSetting('stop_workers_on_master_exit', e.target.checked);
const stopOnExitLabel = document.createElement("label");
stopOnExitLabel.htmlFor = "setting-stop-on-exit";
stopOnExitLabel.textContent = "Stop Local Workers on Master Exit";
stopOnExitLabel.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;";
stopOnExitLabel.title = "Stop local worker processes automatically when the master exits.";
stopOnExitGroup.appendChild(stopOnExitCheckbox);
stopOnExitGroup.appendChild(stopOnExitLabel);
settingsDiv.appendChild(generalLabel);
settingsDiv.appendChild(debugGroup);
settingsDiv.appendChild(autoLaunchGroup);
settingsDiv.appendChild(stopOnExitGroup);
// Worker Timeout setting (seconds)
// Section: Timeouts
const timeoutsLabel = document.createElement("div");
timeoutsLabel.textContent = "TIMEOUTS";
timeoutsLabel.style.cssText = "grid-column: 1 / span 2; font-size: 11px; color: #888; letter-spacing: 0.06em; padding-top: 4px;";
const timeoutGroup = document.createElement("div");
timeoutGroup.style.cssText = "grid-column: 1 / span 2; display: flex; align-items: center; gap: 6px;";
const timeoutLabel = document.createElement("label");
timeoutLabel.htmlFor = "setting-worker-timeout";
timeoutLabel.textContent = "Worker Timeout";
timeoutLabel.style.cssText = "font-size: 12px; color: #ccc; cursor: default;";
timeoutLabel.title = "Seconds without a heartbeat before a worker is considered timed out and its tasks are requeued. Default 60. For WAN, consider 300–600.";
const timeoutInput = document.createElement("input");
timeoutInput.type = "number";
timeoutInput.id = "setting-worker-timeout";
timeoutInput.min = "10";
timeoutInput.step = "1";
timeoutInput.style.cssText = "width: 80px; padding: 2px 6px; background: #222; color: #ddd; border: 1px solid #333; border-radius: 3px;";
timeoutInput.value = (extension.config?.settings?.worker_timeout_seconds ?? 60);
timeoutInput.onchange = (e) => {
const v = parseInt(e.target.value, 10);
if (!Number.isFinite(v) || v <= 0) return;
extension._updateSetting('worker_timeout_seconds', v);
};
timeoutGroup.appendChild(timeoutLabel);
timeoutGroup.appendChild(timeoutInput);
settingsDiv.appendChild(generalLabel);
settingsDiv.appendChild(createCheckboxSetting(
"setting-debug", "Debug Mode",
"Enable verbose logging in the browser console.",
extension.config?.settings?.debug || false,
(e) => extension._updateSetting('debug', e.target.checked)
));
settingsDiv.appendChild(createCheckboxSetting(
"setting-auto-launch", "Auto-launch Local Workers on Startup",
"Start local worker processes automatically when the master starts.",
extension.config?.settings?.auto_launch_workers || false,
(e) => extension._updateSetting('auto_launch_workers', e.target.checked)
));
settingsDiv.appendChild(createCheckboxSetting(
"setting-stop-on-exit", "Stop Local Workers on Master Exit",
"Stop local worker processes automatically when the master exits.",
extension.config?.settings?.stop_workers_on_master_exit !== false,
(e) => extension._updateSetting('stop_workers_on_master_exit', e.target.checked)
));
settingsDiv.appendChild(timeoutsLabel);
settingsDiv.appendChild(timeoutGroup);
settingsDiv.appendChild(createNumberSetting(
"setting-worker-timeout", "Worker Timeout",
"Seconds without a heartbeat before a worker is considered timed out. Default 60.",
extension.config?.settings?.worker_timeout_seconds ?? 60,
10, 1,
(e) => {
const v = parseInt(e.target.value, 10);
if (!Number.isFinite(v) || v <= 0) return;
extension._updateSetting('worker_timeout_seconds', v);
}
));
settingsContent.appendChild(settingsDiv);
settingsSection.appendChild(settingsToggleArea);
settingsSection.appendChild(settingsSeparator);
settingsSection.appendChild(settingsContent);
container.appendChild(settingsSection);
el.appendChild(container);
// Start checking worker statuses immediately in parallel
setTimeout(() => extension.checkAllWorkerStatuses(), 0);
} finally {
+134
View File
@@ -0,0 +1,134 @@
export function updateTunnelUIElements(extension, isRunning, isStarting) {
void isRunning;
void isStarting;
const elements = extension.tunnelElements || {};
const status = (extension.tunnelStatus?.status || "stopped").toLowerCase();
const enableColor = "#665533"; // requested yellow/brown tone
const disableColor = "#7c4a4a"; // match worker delete button
const colors = {
running: disableColor,
starting: enableColor,
stopped: enableColor,
error: disableColor,
unknown: enableColor,
stopping: disableColor,
};
if (elements.button) {
elements.button.disabled = status === "starting" || status === "stopping";
if (status === "starting") {
elements.button.innerHTML = `<span class="tunnel-spinner"></span> Starting...`;
elements.button.style.backgroundColor = enableColor;
} else if (status === "running") {
elements.button.textContent = "Disable Cloudflare Tunnel";
elements.button.style.backgroundColor = disableColor;
} else if (status === "error") {
elements.button.textContent = "Retry Cloudflare Tunnel";
elements.button.style.backgroundColor = disableColor;
} else {
elements.button.textContent = "Enable Cloudflare Tunnel";
elements.button.style.backgroundColor = enableColor;
}
}
if (elements.status) {
elements.status.textContent = status.toUpperCase();
elements.status.style.backgroundColor = colors[status] || colors.stopped;
}
if (elements.url) {
const url = extension.tunnelStatus?.public_url;
if (url) {
elements.url.innerHTML = `<a href="${url}" target="_blank" style="color: #eee; text-decoration: none;">${url}</a>`;
} else {
elements.url.textContent = status === "starting" ? "Requesting public URL..." : "No tunnel active";
}
}
if (elements.copyBtn) {
const hasUrl = Boolean(extension.tunnelStatus?.public_url);
elements.copyBtn.disabled = !hasUrl;
elements.copyBtn.style.opacity = hasUrl ? "1" : "0.5";
}
}
export async function refreshTunnelStatus(extension) {
try {
const data = await extension.api.getTunnelStatus();
extension.tunnelStatus = data.tunnel || { status: "stopped" };
if (data.master_host !== undefined) {
extension._applyMasterHost(data.master_host);
}
return extension.tunnelStatus;
} catch (error) {
extension.tunnelStatus = { status: "error", last_error: error.message };
extension.log("Failed to fetch tunnel status: " + error.message, "error");
return extension.tunnelStatus;
} finally {
updateTunnelUIElements(extension);
}
}
export async function handleTunnelToggle(extension, button) {
const currentStatus = (extension.tunnelStatus?.status || "stopped").toLowerCase();
if (currentStatus === "starting" || currentStatus === "stopping") {
return;
}
const setStatus = (status) => {
extension.tunnelStatus = { ...(extension.tunnelStatus || {}), status };
updateTunnelUIElements(extension);
};
if (currentStatus === "running") {
setStatus("stopping");
try {
if (button) {
button.innerHTML = `<span class="tunnel-spinner"></span> Stopping...`;
button.disabled = true;
}
const data = await extension.api.stopTunnel();
extension.tunnelStatus = data.tunnel || { status: "stopped" };
if (data.master_host !== undefined) {
extension._applyMasterHost(data.master_host);
}
updateTunnelUIElements(extension);
extension.ui.showToast(extension.app, "info", "Cloudflare Tunnel Disabled", "Master address restored", 4000);
} catch (error) {
extension.tunnelStatus = { status: "error", last_error: error.message };
updateTunnelUIElements(extension);
extension.ui.showToast(extension.app, "error", "Failed to stop tunnel", error.message, 5000);
} finally {
if (button) {
button.disabled = false;
}
}
return;
}
// Start tunnel
setStatus("starting");
if (button) {
button.innerHTML = `<span class="tunnel-spinner"></span> Starting...`;
button.disabled = true;
}
try {
const data = await extension.api.startTunnel();
extension.tunnelStatus = data.tunnel || { status: "running" };
if (data.master_host !== undefined) {
extension._applyMasterHost(data.master_host);
}
updateTunnelUIElements(extension);
const url = data.tunnel?.public_url || data.master_host;
extension.ui.showToast(extension.app, "success", "Cloudflare Tunnel Ready", url || "Public URL created", 4500);
} catch (error) {
extension.tunnelStatus = { status: "error", last_error: error.message };
updateTunnelUIElements(extension);
extension.ui.showToast(extension.app, "error", "Failed to start tunnel", error.message, 5000);
} finally {
if (button) {
button.disabled = false;
}
}
}
+561
View File
@@ -0,0 +1,561 @@
import { BUTTON_STYLES, TIMEOUTS, STATUS_COLORS, ENDPOINTS } from './constants.js';
export async function checkAllWorkerStatuses(extension) {
// Don't continue if panel is closed
if (!extension.panelElement) {
return;
}
// Create new abort controller for this round of checks
extension.statusCheckAbortController = new AbortController();
// Check master status
checkMasterStatus(extension);
if (!extension.config || !extension.config.workers) {
return;
}
for (const worker of extension.config.workers) {
// Check status for enabled workers OR workers that are launching
if (worker.enabled || extension.state.isWorkerLaunching(worker.id)) {
checkWorkerStatus(extension, worker);
}
}
// Determine next interval based on current state
let isActive = extension.state.getMasterStatus() === "processing"; // Master is busy
// Check workers for activity
extension.config.workers.forEach((worker) => {
const ws = extension.state.getWorker(worker.id); // Get worker state
if (ws.launching || ws.status?.processing) {
// Launching or processing
isActive = true;
}
});
// Set next delay: 1s if active, 5s if idle
const nextInterval = isActive ? 1000 : 5000;
// Schedule the next check
extension.statusCheckTimeout = setTimeout(() => checkAllWorkerStatuses(extension), nextInterval);
}
export async function checkMasterStatus(extension) {
try {
const response = await fetch(`${window.location.origin}${ENDPOINTS.PROMPT}`, {
method: "GET",
signal: AbortSignal.timeout(TIMEOUTS.STATUS_CHECK),
});
if (response.ok) {
const data = await response.json();
const queueRemaining = data.exec_info?.queue_remaining || 0;
const isProcessing = queueRemaining > 0;
// Update master status in state
extension.state.setMasterStatus(isProcessing ? "processing" : "online");
// Update master status dot
const statusDot = document.getElementById("master-status");
if (statusDot) {
if (!extension.isMasterParticipating()) {
if (isProcessing) {
statusDot.style.backgroundColor = STATUS_COLORS.PROCESSING_YELLOW;
statusDot.title = `Orchestrating (${queueRemaining} in queue)`;
} else {
statusDot.style.backgroundColor = STATUS_COLORS.DISABLED_GRAY;
statusDot.title = "Master orchestrator only";
}
} else if (isProcessing) {
statusDot.style.backgroundColor = STATUS_COLORS.PROCESSING_YELLOW;
statusDot.title = `Processing (${queueRemaining} in queue)`;
} else {
statusDot.style.backgroundColor = STATUS_COLORS.ONLINE_GREEN;
statusDot.title = "Online";
}
}
}
} catch (error) {
// Master is always online (we're running on it), so keep it green
const statusDot = document.getElementById("master-status");
if (statusDot) {
statusDot.style.backgroundColor = extension.isMasterParticipating() ? STATUS_COLORS.ONLINE_GREEN : STATUS_COLORS.DISABLED_GRAY;
statusDot.title = extension.isMasterParticipating() ? "Online" : "Master orchestrator only";
}
}
}
// Helper to build worker URL
export function getWorkerUrl(extension, worker, endpoint = "") {
const parsed = extension._parseHostInput(worker.host || window.location.hostname);
const host = parsed.host || window.location.hostname;
const resolvedPort = parsed.port || worker.port;
// Cloud workers always use HTTPS
const isCloud = worker.type === "cloud";
// Detect if we're running on Runpod (for local workers on Runpod infrastructure)
const isRunpodProxy = host.endsWith(".proxy.runpod.net");
// For local workers on Runpod, construct the port-specific proxy URL
let finalHost = host;
if (!worker.host && isRunpodProxy) {
const match = host.match(/^(.*)\.proxy\.runpod\.net$/);
if (match) {
const podId = match[1];
const domain = "proxy.runpod.net";
finalHost = `${podId}-${worker.port}.${domain}`;
} else {
// Fallback or log error if no match (shouldn't happen)
console.error(`[Distributed] Failed to parse Runpod proxy host: ${host}`);
}
}
// Determine protocol: HTTPS for cloud, Runpod proxies, or port 443
const useHttps = isCloud || isRunpodProxy || resolvedPort === 443;
const protocol = useHttps ? "https" : "http";
// Only add port if non-standard
const defaultPort = useHttps ? 443 : 80;
const needsPort = !isRunpodProxy && resolvedPort !== defaultPort;
const portStr = needsPort ? `:${resolvedPort}` : "";
return `${protocol}://${finalHost}${portStr}${endpoint}`;
}
export async function checkWorkerStatus(extension, worker) {
// Assume caller ensured enabled; proceed with check
const url = getWorkerUrl(extension, worker, ENDPOINTS.PROMPT);
try {
// Combine timeout with abort controller signal
const timeoutSignal = AbortSignal.timeout(TIMEOUTS.STATUS_CHECK);
const signal = extension.statusCheckAbortController
? AbortSignal.any([timeoutSignal, extension.statusCheckAbortController.signal])
: timeoutSignal;
const response = await fetch(url, {
method: "GET",
mode: "cors",
signal,
});
if (response.ok) {
const data = await response.json();
const queueRemaining = data.exec_info?.queue_remaining || 0;
const isProcessing = queueRemaining > 0;
// Update status
extension.state.setWorkerStatus(worker.id, {
online: true,
processing: isProcessing,
queueCount: queueRemaining,
});
// Update status dot based on processing state
if (isProcessing) {
extension.ui.updateStatusDot(worker.id, "#f0ad4e", `Online - Processing (${queueRemaining} in queue)`, false);
} else {
extension.ui.updateStatusDot(worker.id, "#3ca03c", "Online - Idle", false);
}
// Clear launching state since worker is now online
if (extension.state.isWorkerLaunching(worker.id)) {
extension.state.setWorkerLaunching(worker.id, false);
clearLaunchingFlag(extension, worker.id);
}
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
// Don't process aborted requests
if (error.name === "AbortError") {
return;
}
// Worker is offline or unreachable
extension.state.setWorkerStatus(worker.id, {
online: false,
processing: false,
queueCount: 0,
});
// Check if worker is launching
if (extension.state.isWorkerLaunching(worker.id)) {
extension.ui.updateStatusDot(worker.id, "#f0ad4e", "Launching...", true);
} else if (worker.enabled) {
// Only update to red if not currently launching AND still enabled
extension.ui.updateStatusDot(worker.id, "#c04c4c", "Offline - Cannot connect", false);
}
// If disabled, don't update the dot (leave it gray)
extension.log(`Worker ${worker.id} status check failed: ${error.message}`, "debug");
}
// Update control buttons based on new status
updateWorkerControls(extension, worker.id);
}
export async function launchWorker(extension, workerId) {
const worker = extension.config.workers.find((w) => w.id === workerId);
// If worker is disabled, enable it first
if (!worker.enabled) {
await extension.updateWorkerEnabled(workerId, true);
// Update the checkbox UI
const checkbox = document.getElementById(`gpu-${workerId}`);
if (checkbox) {
checkbox.checked = true;
}
}
// Re-query button AFTER updateWorkerEnabled (which may re-render sidebar)
const launchBtn = document.querySelector(`#controls-${workerId} button`);
extension.ui.updateStatusDot(workerId, "#f0ad4e", "Launching...", true);
extension.state.setWorkerLaunching(workerId, true);
// Allow 90 seconds for worker to launch (model loading can take time)
setTimeout(() => {
extension.state.setWorkerLaunching(workerId, false);
}, TIMEOUTS.LAUNCH);
if (!launchBtn) {
return;
}
try {
// Disable button immediately
launchBtn.disabled = true;
const result = await extension.api.launchWorker(workerId);
if (result) {
extension.log(`Launched ${worker.name} (PID: ${result.pid})`, "info");
if (result.log_file) {
extension.log(`Log file: ${result.log_file}`, "debug");
}
extension.state.setWorkerManaged(workerId, {
pid: result.pid,
log_file: result.log_file,
started_at: Date.now(),
});
// Update controls immediately to hide launch button and show stop/log buttons
updateWorkerControls(extension, workerId);
setTimeout(() => checkWorkerStatus(extension, worker), TIMEOUTS.STATUS_CHECK);
}
} catch (error) {
// Check if worker was already running
if (error.message && error.message.includes("already running")) {
extension.log(`Worker ${worker.name} is already running`, "info");
updateWorkerControls(extension, workerId);
setTimeout(() => checkWorkerStatus(extension, worker), TIMEOUTS.STATUS_CHECK_DELAY);
} else {
extension.log(`Error launching worker: ${error.message || error}`, "error");
// Re-enable button on error
if (launchBtn) {
launchBtn.disabled = false;
}
}
}
}
export async function stopWorker(extension, workerId) {
const worker = extension.config.workers.find((w) => w.id === workerId);
const stopBtn = document.querySelectorAll(`#controls-${workerId} button`)[1];
// Provide immediate feedback
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = "Stopping...";
stopBtn.style.backgroundColor = "#666";
}
try {
const result = await extension.api.stopWorker(workerId);
if (result) {
extension.log(`Stopped worker: ${result.message}`, "info");
extension.state.setWorkerManaged(workerId, null);
// Immediately update status to offline
extension.ui.updateStatusDot(workerId, "#c04c4c", "Offline");
extension.state.setWorkerStatus(workerId, { online: false });
// Flash success feedback
if (stopBtn) {
stopBtn.style.backgroundColor = BUTTON_STYLES.success;
stopBtn.textContent = "Stopped!";
setTimeout(() => {
updateWorkerControls(extension, workerId);
}, TIMEOUTS.FLASH_SHORT);
}
// Verify status after a short delay
setTimeout(() => checkWorkerStatus(extension, worker), TIMEOUTS.STATUS_CHECK);
} else {
extension.log(`Failed to stop worker: ${result.message}`, "error");
// Flash error feedback
if (stopBtn) {
stopBtn.style.backgroundColor = BUTTON_STYLES.error;
stopBtn.textContent = result.message.includes("already stopped") ? "Not Running" : "Failed";
// If already stopped, update status immediately
if (result.message.includes("already stopped")) {
extension.ui.updateStatusDot(workerId, "#c04c4c", "Offline");
extension.state.setWorkerStatus(workerId, { online: false });
}
setTimeout(() => {
updateWorkerControls(extension, workerId);
}, TIMEOUTS.FLASH_MEDIUM);
}
}
} catch (error) {
extension.log(`Error stopping worker: ${error}`, "error");
// Reset button on error
if (stopBtn) {
stopBtn.style.backgroundColor = BUTTON_STYLES.error;
stopBtn.textContent = "Error";
setTimeout(() => {
updateWorkerControls(extension, workerId);
}, TIMEOUTS.FLASH_MEDIUM);
}
}
}
export async function clearLaunchingFlag(extension, workerId) {
try {
await extension.api.clearLaunchingFlag(workerId);
extension.log(`Cleared launching flag for worker ${workerId}`, "debug");
} catch (error) {
extension.log(`Error clearing launching flag: ${error.message || error}`, "error");
}
}
export async function loadManagedWorkers(extension) {
try {
const result = await extension.api.getManagedWorkers();
// Check for launching workers
for (const [workerId, info] of Object.entries(result.managed_workers)) {
extension.state.setWorkerManaged(workerId, info);
// If worker is marked as launching, add to launchingWorkers set
if (info.launching) {
extension.state.setWorkerLaunching(workerId, true);
extension.log(`Worker ${workerId} is in launching state`, "debug");
}
}
// Update UI for all workers
if (extension.config?.workers) {
extension.config.workers.forEach((w) => updateWorkerControls(extension, w.id));
}
} catch (error) {
extension.log(`Error loading managed workers: ${error}`, "error");
}
}
export function updateWorkerControls(extension, workerId) {
const controlsDiv = document.getElementById(`controls-${workerId}`);
if (!controlsDiv) {
return;
}
const worker = extension.config.workers.find((w) => w.id === workerId);
if (!worker) {
return;
}
// Skip button updates for remote workers
if (extension.isRemoteWorker(worker)) {
return;
}
// Ensure we check for string ID
const managedInfo = extension.state.getWorker(workerId).managed;
const status = extension.state.getWorkerStatus(workerId);
// Update button states - buttons are now inside a wrapper div
const launchBtn = document.getElementById(`launch-${workerId}`);
const stopBtn = document.getElementById(`stop-${workerId}`);
const logBtn = document.getElementById(`log-${workerId}`);
// Show log button immediately if we have log file info (even if worker is still starting)
if (managedInfo?.log_file && logBtn) {
logBtn.style.display = "";
} else if (logBtn && !managedInfo) {
logBtn.style.display = "none";
}
if (status?.online || managedInfo) {
// Worker is running or we just launched it
launchBtn.style.display = "none"; // Hide launch button when running
if (managedInfo) {
// Only show stop button if we manage this worker
stopBtn.style.display = "";
stopBtn.disabled = false;
stopBtn.textContent = "Stop";
stopBtn.style.backgroundColor = "#7c4a4a"; // Red when enabled
} else {
// Hide stop button for workers launched outside UI
stopBtn.style.display = "none";
}
} else {
// Worker is not running
launchBtn.style.display = ""; // Show launch button
launchBtn.disabled = false;
launchBtn.textContent = "Launch";
launchBtn.style.backgroundColor = "#4a7c4a"; // Always green
stopBtn.style.display = "none"; // Hide stop button when not running
}
}
export async function viewWorkerLog(extension, workerId) {
const managedInfo = extension.state.getWorker(workerId).managed;
if (!managedInfo?.log_file) {
return;
}
const logBtn = document.getElementById(`log-${workerId}`);
// Provide immediate feedback
if (logBtn) {
logBtn.disabled = true;
logBtn.textContent = "Loading...";
logBtn.style.backgroundColor = "#666";
}
try {
// Fetch log content
const data = await extension.api.getWorkerLog(workerId, 1000);
// Create modal dialog
extension.ui.showLogModal(extension, workerId, data);
// Restore button
if (logBtn) {
logBtn.disabled = false;
logBtn.textContent = "View Log";
logBtn.style.backgroundColor = "#685434"; // Keep the yellow color
}
} catch (error) {
extension.log("Error viewing log: " + error.message, "error");
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Error",
detail: `Failed to load log: ${error.message}`,
life: 5000,
});
// Flash error and restore button
if (logBtn) {
logBtn.style.backgroundColor = BUTTON_STYLES.error;
logBtn.textContent = "Error";
setTimeout(() => {
logBtn.disabled = false;
logBtn.textContent = "View Log";
logBtn.style.backgroundColor = "#685434"; // Keep the yellow color
}, TIMEOUTS.FLASH_LONG);
}
}
}
export async function refreshLog(extension, workerId, silent = false) {
const logContent = document.getElementById("distributed-log-content");
if (!logContent) {
return;
}
try {
const data = await extension.api.getWorkerLog(workerId, 1000);
// Update content
const shouldAutoScroll = logContent.scrollTop + logContent.clientHeight >= logContent.scrollHeight - 50;
logContent.textContent = data.content;
// Auto-scroll if was at bottom
if (shouldAutoScroll) {
logContent.scrollTop = logContent.scrollHeight;
}
// Only show toast if not in silent mode (manual refresh)
if (!silent) {
extension.app.extensionManager.toast.add({
severity: "success",
summary: "Log Refreshed",
detail: "Log content updated",
life: 2000,
});
}
} catch (error) {
// Only show error toast if not in silent mode
if (!silent) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Refresh Failed",
detail: error.message,
life: 3000,
});
}
}
}
export function startLogAutoRefresh(extension, workerId) {
// Stop any existing auto-refresh
stopLogAutoRefresh(extension);
// Refresh every 2 seconds
extension.logAutoRefreshInterval = setInterval(() => {
refreshLog(extension, workerId, true); // silent mode
}, TIMEOUTS.LOG_REFRESH);
}
export function stopLogAutoRefresh(extension) {
if (extension.logAutoRefreshInterval) {
clearInterval(extension.logAutoRefreshInterval);
extension.logAutoRefreshInterval = null;
}
}
export function toggleWorkerExpanded(extension, workerId) {
const settingsDiv = document.getElementById(`settings-${workerId}`);
const gpuDiv = settingsDiv?.closest('[style*="margin-bottom: 12px"]');
const settingsArrow = gpuDiv?.querySelector(".settings-arrow");
if (!settingsDiv) {
return;
}
if (extension.state.isWorkerExpanded(workerId)) {
extension.state.setWorkerExpanded(workerId, false);
settingsDiv.classList.remove("expanded");
if (settingsArrow) {
settingsArrow.style.transform = "rotate(0deg)";
}
// Animate padding to 0
settingsDiv.style.padding = "0 12px";
settingsDiv.style.marginTop = "0";
settingsDiv.style.marginBottom = "0";
} else {
extension.state.setWorkerExpanded(workerId, true);
settingsDiv.classList.add("expanded");
if (settingsArrow) {
settingsArrow.style.transform = "rotate(90deg)";
}
// Animate padding to full
settingsDiv.style.padding = "12px";
settingsDiv.style.marginTop = "8px";
settingsDiv.style.marginBottom = "8px";
}
}
+283
View File
@@ -0,0 +1,283 @@
import { renderSidebarContent } from './sidebarRenderer.js';
import { generateUUID } from './constants.js';
export function isRemoteWorker(extension, worker) {
// Check if explicitly marked as cloud worker
if (worker.type === "cloud") {
return true;
}
// Otherwise check by host (backward compatibility)
const parsed = extension._parseHostInput(worker.host || window.location.hostname);
const host = parsed.host || window.location.hostname;
return host !== "localhost" && host !== "127.0.0.1" && host !== window.location.hostname;
}
export function isCloudWorker(extension, worker) {
return worker.type === "cloud";
}
export async function saveWorkerSettings(extension, workerId) {
const worker = extension.config.workers.find((w) => w.id === workerId);
if (!worker) {
return;
}
// Get form values
const name = document.getElementById(`name-${workerId}`).value;
const workerType = document.getElementById(`worker-type-${workerId}`).value;
const isRemote = workerType === "remote" || workerType === "cloud";
const isCloud = workerType === "cloud";
const rawHost = isRemote ? document.getElementById(`host-${workerId}`).value : window.location.hostname;
const parsedHost = isRemote ? extension._parseHostInput(rawHost) : { host: window.location.hostname, port: null };
const host = isRemote ? parsedHost.host : window.location.hostname;
let port = parseInt(document.getElementById(`port-${workerId}`).value);
const cudaDevice = isRemote ? undefined : parseInt(document.getElementById(`cuda-${workerId}`).value);
const extraArgs = isRemote ? undefined : document.getElementById(`args-${workerId}`).value;
if (isRemote && Number.isFinite(parsedHost.port)) {
port = parsedHost.port;
}
// Validate
if (!name.trim()) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Validation Error",
detail: "Worker name is required",
life: 3000,
});
return;
}
if ((workerType === "remote" || workerType === "cloud") && !host.trim()) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Validation Error",
detail: "Host is required for remote workers",
life: 3000,
});
return;
}
if (!isCloud && (isNaN(port) || port < 1 || port > 65535)) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Validation Error",
detail: "Port must be between 1 and 65535",
life: 3000,
});
return;
}
// Check for port conflicts
// Remote workers can reuse ports, but local workers cannot share ports with each other or master
if (!isRemote) {
// Check if port conflicts with master
const masterPort = parseInt(window.location.port) || (window.location.protocol === "https:" ? 443 : 80);
if (port === masterPort) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Port Conflict",
detail: `Port ${port} is already in use by the master server`,
life: 3000,
});
return;
}
// Check if port conflicts with other local workers
const localPortConflict = extension.config.workers.some(
(w) => w.id !== workerId && w.port === port && !w.host // local workers have no host or host is null
);
if (localPortConflict) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Port Conflict",
detail: `Port ${port} is already in use by another local worker`,
life: 3000,
});
return;
}
} else {
// For remote workers, only check conflicts with other workers on the same host
const sameHostConflict = extension.config.workers.some((w) => w.id !== workerId && w.port === port && w.host === host.trim());
if (sameHostConflict) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Port Conflict",
detail: `Port ${port} is already in use by another worker on ${host}`,
life: 3000,
});
return;
}
}
try {
await extension.api.updateWorker(workerId, {
name: name.trim(),
type: workerType,
host: isRemote ? host.trim() : null,
port,
cuda_device: isRemote ? null : cudaDevice,
extra_args: isRemote ? null : extraArgs ? extraArgs.trim() : "",
});
// Update local config
worker.name = name.trim();
worker.type = workerType;
if (isRemote) {
worker.host = host.trim();
delete worker.cuda_device;
delete worker.extra_args;
} else {
delete worker.host;
worker.cuda_device = cudaDevice;
worker.extra_args = extraArgs ? extraArgs.trim() : "";
}
worker.port = port;
// Sync to state
extension.state.updateWorker(workerId, { enabled: worker.enabled });
extension.app.extensionManager.toast.add({
severity: "success",
summary: "Settings Saved",
detail: `Worker ${name} settings updated`,
life: 3000,
});
// Refresh the UI
if (extension.panelElement) {
renderSidebarContent(extension, extension.panelElement);
}
} catch (error) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Save Failed",
detail: error.message,
life: 5000,
});
}
}
export function cancelWorkerSettings(extension, workerId) {
// Collapse the settings panel
extension.toggleWorkerExpanded(workerId);
// Reset form values to original
const worker = extension.config.workers.find((w) => w.id === workerId);
if (worker) {
document.getElementById(`name-${workerId}`).value = worker.name;
document.getElementById(`host-${workerId}`).value = worker.host || "";
document.getElementById(`port-${workerId}`).value = worker.port;
document.getElementById(`cuda-${workerId}`).value = worker.cuda_device || 0;
document.getElementById(`args-${workerId}`).value = worker.extra_args || "";
// Reset remote checkbox
const remoteCheckbox = document.getElementById(`remote-${workerId}`);
if (remoteCheckbox) {
remoteCheckbox.checked = extension.isRemoteWorker(worker);
}
}
}
export async function deleteWorker(extension, workerId) {
const worker = extension.config.workers.find((w) => w.id === workerId);
if (!worker) {
return;
}
// Confirm deletion
if (!confirm(`Are you sure you want to delete worker "${worker.name}"?`)) {
return;
}
try {
await extension.api.deleteWorker(workerId);
// Remove from local config
const index = extension.config.workers.findIndex((w) => w.id === workerId);
if (index !== -1) {
extension.config.workers.splice(index, 1);
}
extension.app.extensionManager.toast.add({
severity: "success",
summary: "Worker Deleted",
detail: `Worker ${worker.name} has been removed`,
life: 3000,
});
// Refresh the UI
if (extension.panelElement) {
renderSidebarContent(extension, extension.panelElement);
}
} catch (error) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Delete Failed",
detail: error.message,
life: 5000,
});
}
}
export async function addNewWorker(extension) {
// Generate new worker ID using UUID (fallback for non-secure contexts)
const newId = generateUUID();
// Find next available port
const usedPorts = extension.config.workers.map((w) => w.port);
let nextPort = 8189;
while (usedPorts.includes(nextPort)) {
nextPort++;
}
// Create new worker object
const newWorker = {
id: newId,
name: `Worker ${extension.config.workers.length + 1}`,
port: nextPort,
cuda_device: extension.config.workers.length,
enabled: true, // Default to enabled for convenience
extra_args: "",
};
// Add to config
extension.config.workers.push(newWorker);
// Save immediately
try {
await extension.api.updateWorker(newId, {
name: newWorker.name,
port: newWorker.port,
cuda_device: newWorker.cuda_device,
extra_args: newWorker.extra_args,
enabled: newWorker.enabled,
});
// Sync to state
extension.state.updateWorker(newId, { enabled: true });
extension.app.extensionManager.toast.add({
severity: "success",
summary: "Worker Added",
detail: `New worker created on port ${nextPort}`,
life: 3000,
});
// Refresh UI and expand the new worker
extension.state.setWorkerExpanded(newId, true);
if (extension.panelElement) {
renderSidebarContent(extension, extension.panelElement);
}
} catch (error) {
extension.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to Add Worker",
detail: error.message,
life: 5000,
});
}
}
+7 -7
View File
@@ -1,4 +1,4 @@
import { BUTTON_STYLES, TIMEOUTS } from './constants.js';
import { BUTTON_STYLES, TIMEOUTS, ENDPOINTS, NODE_CLASSES } from './constants.js';
export async function handleWorkerOperation(extension, button, operation, successText, errorText) {
const originalText = button.textContent;
@@ -58,7 +58,7 @@ export async function handleWorkerOperation(extension, button, operation, succes
export async function handleInterruptWorkers(extension, button) {
return handleWorkerOperation(extension, button, {
name: "Interrupt",
endpoint: "/interrupt",
endpoint: ENDPOINTS.INTERRUPT,
loadingText: "Interrupting...",
onSuccess: () => setTimeout(() => extension.checkAllWorkerStatuses(), TIMEOUTS.POST_ACTION_DELAY)
}, "Interrupted!", "Error! See Console");
@@ -67,7 +67,7 @@ export async function handleInterruptWorkers(extension, button) {
export async function handleClearMemory(extension, button) {
return handleWorkerOperation(extension, button, {
name: "Clear memory",
endpoint: "/distributed/clear_memory",
endpoint: ENDPOINTS.CLEAR_MEMORY,
loadingText: "Clearing..."
}, "Success!", "Error! See Console");
}
@@ -205,8 +205,8 @@ export function findCollectorDownstreamNodes(apiPrompt, collectorIds) {
export function pruneWorkflowForWorker(extension, apiPrompt, distributedNodes = null) {
// Find all distributed nodes if not provided
if (!distributedNodes) {
const collectorNodes = findNodesByClass(apiPrompt, "DistributedCollector");
const upscaleNodes = findNodesByClass(apiPrompt, "UltimateSDUpscaleDistributed");
const collectorNodes = findNodesByClass(apiPrompt, NODE_CLASSES.DISTRIBUTED_COLLECTOR);
const upscaleNodes = findNodesByClass(apiPrompt, NODE_CLASSES.UPSCALE_DISTRIBUTED);
distributedNodes = [...collectorNodes, ...upscaleNodes];
}
@@ -263,7 +263,7 @@ export function pruneWorkflowForWorker(extension, apiPrompt, distributedNodes =
inputs: {
images: [distNodeId, 0] // Connect to first output of distributed node
},
class_type: "PreviewImage",
class_type: NODE_CLASSES.PREVIEW_IMAGE,
_meta: {
title: "Preview Image (auto-added)"
}
@@ -365,4 +365,4 @@ export async function getCachedWorkerSystemInfo(workerUrl) {
*/
export function clearSystemInfoCache() {
systemInfoCache.clear();
}
}