diff --git a/web/apiClient.js b/web/apiClient.js index 061428d..b4fceaa 100644 --- a/web/apiClient.js +++ b/web/apiClient.js @@ -1,7 +1,9 @@ +import { TIMEOUTS } from './constants.js'; + export function createApiClient(baseUrl) { - const request = async (endpoint, options = {}, retries = 3) => { + const request = async (endpoint, options = {}, retries = TIMEOUTS.MAX_RETRIES) => { let lastError; - let delay = 1000; // Initial delay for exponential backoff + let delay = TIMEOUTS.RETRY_DELAY; // Initial delay for exponential backoff for (let attempt = 0; attempt < retries; attempt++) { try { @@ -114,7 +116,7 @@ export function createApiClient(baseUrl) { }, // Status checking (with timeout) - async checkStatus(url, timeout = 5000) { + async checkStatus(url, timeout = TIMEOUTS.DEFAULT_FETCH) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); diff --git a/web/constants.js b/web/constants.js index 223807d..2b3f0ad 100644 --- a/web/constants.js +++ b/web/constants.js @@ -5,7 +5,6 @@ export const BUTTON_STYLES = { error: "background-color: #c04c4c;", launch: "background-color: #4a7c4a;", stop: "background-color: #7c4a4a;", - grey: "background-color: #555;", }; export const STATUS_COLORS = { @@ -15,6 +14,18 @@ export const STATUS_COLORS = { PROCESSING_YELLOW: "#f0ad4e" }; +export const UI_COLORS = { + MUTED_TEXT: "#888", + SECONDARY_TEXT: "#ccc", + BORDER_LIGHT: "#555", + BORDER_DARK: "#444", + BORDER_DARKER: "#3a3a3a", + BACKGROUND_DARK: "#2a2a2a", + BACKGROUND_DARKER: "#1e1e1e", + ICON_COLOR: "#666", + ACCENT_COLOR: "#777" +}; + export const PULSE_ANIMATION_CSS = ` @keyframes pulse { 0% { @@ -73,11 +84,32 @@ export const UI_STYLES = { formGroup: "display: flex; flex-direction: column; gap: 5px;", formLabel: "font-size: 12px; color: #ccc; font-weight: 500;", formInput: "padding: 6px 10px; background: #2a2a2a; border: 1px solid #444; color: white; font-size: 12px; border-radius: 4px; transition: border-color 0.2s;", + + // Card styles + cardBase: "margin-bottom: 12px; border-radius: 6px; overflow: hidden; display: flex;", workerCard: "margin-bottom: 12px; border-radius: 6px; overflow: hidden; display: flex; background: #2a2a2a;", + cardBlueprint: "border: 2px dashed #555; cursor: pointer; transition: all 0.2s ease; background: rgba(255, 255, 255, 0.02);", + cardAdd: "border: 1px dashed #444; cursor: pointer; transition: all 0.2s ease; background: transparent;", + + // Column styles + columnBase: "display: flex; align-items: center; justify-content: center;", checkboxColumn: "flex: 0 0 44px; display: flex; align-items: center; justify-content: center; border-right: 1px solid #3a3a3a; cursor: default; background: rgba(0,0,0,0.1);", contentColumn: "flex: 1; display: flex; flex-direction: column; transition: background-color 0.2s ease;", - infoRow: "display: flex; align-items: center; padding: 12px; cursor: pointer;", + iconColumn: "width: 44px; flex-shrink: 0; font-size: 20px; color: #666;", + + // Row and content styles + infoRow: "display: flex; align-items: center; padding: 12px; cursor: pointer; min-height: 64px;", workerContent: "display: flex; align-items: center; gap: 10px; flex: 1;", + + // Form and controls styles + buttonGroup: "display: flex; gap: 4px; margin-top: 10px;", + settingsForm: "display: flex; flex-direction: column; gap: 10px;", + checkboxGroup: "display: flex; align-items: center; gap: 8px; margin: 5px 0;", + formLabelClickable: "font-size: 12px; color: #ccc; cursor: pointer;", + settingsToggle: "display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer; user-select: none;", + controlsWrapper: "display: flex; gap: 6px; align-items: stretch; width: 100%;", + + // Existing styles settingsArrow: "font-size: 12px; color: #888; transition: all 0.2s ease; margin-left: auto; padding: 4px;", infoBox: "background-color: #333; color: #999; padding: 5px 14px; border-radius: 4px; font-size: 11px; text-align: center; flex: 1; font-weight: 500;", workerSettings: "margin: 0 12px; padding: 0 12px; background: #1e1e1e; border-radius: 4px; border: 1px solid #2a2a2a;" @@ -85,8 +117,22 @@ export const UI_STYLES = { export const TIMEOUTS = { DEFAULT_FETCH: 5000, // ms for general API calls - STATUS_CHECK: 2000, // ms for status checks + STATUS_CHECK: 500, // ms for status checks LAUNCH: 90000, // ms for worker launch (longer for model loading) RETRY_DELAY: 1000, // initial delay for exponential backoff - MAX_RETRIES: 3 // max retry attempts + MAX_RETRIES: 3, // max retry attempts + + // UI feedback delays + BUTTON_RESET: 3000, // button text/state reset after actions + FLASH_SHORT: 1000, // brief success feedback + FLASH_MEDIUM: 1500, // medium error feedback + FLASH_LONG: 2000, // longer error feedback + + // Operational delays + POST_ACTION_DELAY: 500, // delay after operations before status checks + STATUS_CHECK_DELAY: 100, // brief delay before status checks + + // Background tasks + LOG_REFRESH: 2000, // log auto-refresh interval + IMAGE_CACHE_CLEAR: 30000 // delay before clearing image cache }; \ No newline at end of file diff --git a/web/executionUtils.js b/web/executionUtils.js index d26fcba..9960de4 100644 --- a/web/executionUtils.js +++ b/web/executionUtils.js @@ -11,7 +11,7 @@ export function setupInterceptor(extension) { if (hasCollector || hasDistUpscale) { const result = await executeParallelDistributed(extension, prompt); // Check status after dispatching jobs - setTimeout(() => extension.checkAllWorkerStatuses(), 500); + setTimeout(() => extension.checkAllWorkerStatuses(), TIMEOUTS.POST_ACTION_DELAY); return result; } } @@ -27,8 +27,13 @@ export async function executeParallelDistributed(extension, promptWrapper) { // Pre-flight health check on all enabled workers const activeWorkers = await performPreflightCheck(extension, enabledWorkers); + // Case: Enabled workers but all offline if (activeWorkers.length === 0 && enabledWorkers.length > 0) { - extension.log("No active workers found. Running on master only."); + extension.log("No active workers found. All enabled workers are offline."); + if (extension.ui?.showToast) { + extension.ui.showToast(extension.app, "error", "All Workers Offline", + `${enabledWorkers.length} worker(s) enabled but all are offline or unreachable. Check worker connections and try again.`, 5000); + } // Fall back to master-only execution return extension.originalQueuePrompt(0, promptWrapper); } @@ -336,7 +341,7 @@ export async function loadImagesForWorker(extension, imageReferences) { extension.log(`Clearing image cache (${extension._imageCache.size} images)`, "debug"); extension._imageCache.clear(); } - }, 30000); // Clear after 30 seconds + }, TIMEOUTS.IMAGE_CACHE_CLEAR); // Clear after 30 seconds return images; } diff --git a/web/main.js b/web/main.js index c0310c5..e643d58 100644 --- a/web/main.js +++ b/web/main.js @@ -16,6 +16,7 @@ class DistributedExtension { this.statusCheckInterval = null; this.logAutoRefreshInterval = null; this.masterSettingsExpanded = false; + this.app = app; // Store app reference for toast notifications // Initialize centralized state this.state = createStateManager(); @@ -109,7 +110,7 @@ class DistributedExtension { if (enabled) { // Enabled: Start with checking state and trigger check this.ui.updateStatusDot(workerId, STATUS_COLORS.OFFLINE_RED, "Checking status...", true); - setTimeout(() => this.checkWorkerStatus(worker), 100); + setTimeout(() => this.checkWorkerStatus(worker), TIMEOUTS.STATUS_CHECK_DELAY); } else { // Disabled: Set to gray this.ui.updateStatusDot(workerId, STATUS_COLORS.DISABLED_GRAY, "Disabled", false); @@ -192,11 +193,9 @@ class DistributedExtension { } startStatusChecking() { - // Start checking every 2 seconds for more responsive updates - // Don't check immediately since panel might not be open yet this.statusCheckInterval = setInterval(() => { this.checkAllWorkerStatuses(); - }, 2000); + }, TIMEOUTS.STATUS_CHECK); } async checkAllWorkerStatuses() { @@ -356,7 +355,7 @@ class DistributedExtension { // Allow 90 seconds for worker to launch (model loading can take time) setTimeout(() => { this.state.setWorkerLaunching(workerId, false); - }, 90000); + }, TIMEOUTS.LAUNCH); if (!launchBtn) return; @@ -379,14 +378,14 @@ class DistributedExtension { // Update controls immediately to hide launch button and show stop/log buttons this.updateWorkerControls(workerId); - setTimeout(() => this.checkWorkerStatus(worker), 2000); + setTimeout(() => this.checkWorkerStatus(worker), TIMEOUTS.STATUS_CHECK); } } catch (error) { // Check if worker was already running if (error.message && error.message.includes("already running")) { this.log(`Worker ${worker.name} is already running`, "info"); this.updateWorkerControls(workerId); - setTimeout(() => this.checkWorkerStatus(worker), 100); + setTimeout(() => this.checkWorkerStatus(worker), TIMEOUTS.STATUS_CHECK_DELAY); } else { this.log(`Error launching worker: ${error.message || error}`, "error"); @@ -425,11 +424,11 @@ class DistributedExtension { stopBtn.textContent = "Stopped!"; setTimeout(() => { this.updateWorkerControls(workerId); - }, 1000); + }, TIMEOUTS.FLASH_SHORT); } // Verify status after a short delay - setTimeout(() => this.checkWorkerStatus(worker), 2000); + setTimeout(() => this.checkWorkerStatus(worker), TIMEOUTS.STATUS_CHECK); } else { this.log(`Failed to stop worker: ${result.message}`, "error"); @@ -446,7 +445,7 @@ class DistributedExtension { setTimeout(() => { this.updateWorkerControls(workerId); - }, 1500); + }, TIMEOUTS.FLASH_MEDIUM); } } } catch (error) { @@ -458,7 +457,7 @@ class DistributedExtension { stopBtn.textContent = "Error"; setTimeout(() => { this.updateWorkerControls(workerId); - }, 1500); + }, TIMEOUTS.FLASH_MEDIUM); } } } @@ -473,7 +472,7 @@ class DistributedExtension { } // Generic async button action handler - async handleAsyncButtonAction(button, action, successText, errorText, resetDelay = 3000) { + async handleAsyncButtonAction(button, action, successText, errorText, resetDelay = TIMEOUTS.BUTTON_RESET) { const originalText = button.textContent; const originalStyle = button.style.cssText; button.disabled = true; @@ -638,7 +637,7 @@ class DistributedExtension { logBtn.disabled = false; logBtn.textContent = "View Log"; logBtn.style.backgroundColor = "#685434"; // Keep the yellow color - }, 2000); + }, TIMEOUTS.FLASH_LONG); } } } @@ -1121,7 +1120,7 @@ class DistributedExtension { // Refresh every 2 seconds this.logAutoRefreshInterval = setInterval(() => { this.refreshLog(workerId, true); // silent mode - }, 2000); + }, TIMEOUTS.LOG_REFRESH); } stopLogAutoRefresh() { diff --git a/web/ui.js b/web/ui.js index 87bbbd9..d8a2fd6 100644 --- a/web/ui.js +++ b/web/ui.js @@ -1,4 +1,4 @@ -import { BUTTON_STYLES, UI_STYLES, STATUS_COLORS } from './constants.js'; +import { BUTTON_STYLES, UI_STYLES, STATUS_COLORS, UI_COLORS, TIMEOUTS } from './constants.js'; const cardConfigs = { master: { @@ -19,7 +19,7 @@ const cardConfigs = { const cudaDevice = extension.config?.master?.cuda_device ?? extension.masterCudaDevice; const cudaInfo = cudaDevice !== undefined ? `CUDA ${cudaDevice} • ` : ''; const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80'); - return `${data?.name || extension.config?.master?.name || "Master"}
${cudaInfo}Port ${port}`; + return `${data?.name || extension.config?.master?.name || "Master"}
${cudaInfo}Port ${port}`; }, controls: { type: 'info', @@ -50,10 +50,10 @@ const cardConfigs = { infoText: (data, extension) => { const isRemote = extension.isRemoteWorker(data); if (isRemote) { - return `${data.name}
${data.host}:${data.port}`; + return `${data.name}
${data.host}:${data.port}`; } else { const cudaInfo = data.cuda_device !== undefined ? `CUDA ${data.cuda_device} • ` : ''; - return `${data.name}
${cudaInfo}Port ${data.port}`; + return `${data.name}
${cudaInfo}Port ${data.port}`; } }, controls: { @@ -73,17 +73,17 @@ const cardConfigs = { type: 'icon', content: '+', width: 42, - style: 'border-right: 2px dashed #555; color: #777; font-size: 24px; font-weight: 500;' + style: `border-right: 2px dashed ${UI_COLORS.BORDER_LIGHT}; color: ${UI_COLORS.ACCENT_COLOR}; font-size: 24px; font-weight: 500;` }, statusDot: { color: 'transparent', - border: '1px solid #555' + border: `1px solid ${UI_COLORS.BORDER_LIGHT}` }, - infoText: () => `Add New Worker
[CUDA] • [Port]`, + infoText: () => `Add New Worker
[CUDA] • [Port]`, controls: { type: 'ghost', text: 'Configure', - style: 'border: 1px solid #444; background: transparent; color: #555;' + style: `border: 1px solid ${UI_COLORS.BORDER_DARK}; background: transparent; color: ${UI_COLORS.BORDER_LIGHT};` }, hover: 'placeholder', expand: false, @@ -94,13 +94,13 @@ const cardConfigs = { type: 'icon', content: '+', width: 43, - style: 'border-right: 1px dashed #444; color: #555; font-size: 18px;' + style: `border-right: 1px dashed ${UI_COLORS.BORDER_DARK}; color: ${UI_COLORS.BORDER_LIGHT}; font-size: 18px;` }, statusDot: { color: 'transparent', - border: '1px solid #555' + border: `1px solid ${UI_COLORS.BORDER_LIGHT}` }, - infoText: () => `Add New Worker`, + infoText: () => `Add New Worker`, controls: null, hover: 'placeholder', expand: false, @@ -134,7 +134,7 @@ export class DistributedUI { createButtonGroup(buttons, style = "") { const group = document.createElement("div"); - group.style.cssText = "display: flex; gap: 4px; margin-top: 10px;" + style; + group.style.cssText = this.styles.buttonGroup + style; buttons.forEach(button => group.appendChild(button)); return group; } @@ -207,20 +207,19 @@ export class DistributedUI { createCard(type = 'worker', options = {}) { const card = document.createElement("div"); - const baseStyle = "margin-bottom: 12px; border-radius: 6px; overflow: hidden; display: flex;"; switch(type) { case 'master': case 'worker': - card.style.cssText = baseStyle + "background: #2a2a2a;"; + card.style.cssText = this.styles.workerCard; break; case 'blueprint': - card.style.cssText = baseStyle + "border: 2px dashed #555; cursor: pointer; transition: all 0.2s ease; background: rgba(255, 255, 255, 0.02);"; + card.style.cssText = this.styles.cardBase + this.styles.cardBlueprint; if (options.onClick) card.onclick = options.onClick; if (options.title) card.title = options.title; break; case 'add': - card.style.cssText = baseStyle + "border: 1px dashed #444; cursor: pointer; transition: all 0.2s ease; background: transparent;"; + card.style.cssText = this.styles.cardBase + this.styles.cardAdd; if (options.onClick) card.onclick = options.onClick; if (options.title) card.title = options.title; break; @@ -238,18 +237,17 @@ export class DistributedUI { createCardColumn(type = 'checkbox', options = {}) { const column = document.createElement("div"); - const baseStyle = "display: flex; align-items: center; justify-content: center;"; switch(type) { case 'checkbox': - column.style.cssText = baseStyle + "flex: 0 0 44px; border-right: 1px solid #3a3a3a; cursor: default; background: rgba(0,0,0,0.1);"; + column.style.cssText = this.styles.checkboxColumn; if (options.title) column.title = options.title; break; case 'icon': - column.style.cssText = baseStyle + "width: 44px; flex-shrink: 0; font-size: 20px; color: #666;"; + column.style.cssText = this.styles.columnBase + this.styles.iconColumn; break; case 'content': - column.style.cssText = "flex: 1; display: flex; flex-direction: column; transition: background-color 0.2s ease;"; + column.style.cssText = this.styles.contentColumn; break; } @@ -258,25 +256,25 @@ export class DistributedUI { createInfoRow(options = {}) { const row = document.createElement("div"); - row.style.cssText = "display: flex; align-items: center; padding: 12px; cursor: pointer; min-height: 64px;"; + row.style.cssText = this.styles.infoRow; if (options.onClick) row.onclick = options.onClick; return row; } createWorkerContent() { const content = document.createElement("div"); - content.style.cssText = "display: flex; align-items: center; gap: 10px; flex: 1;"; + content.style.cssText = this.styles.workerContent; return content; } createSettingsForm(fields = [], options = {}) { const form = document.createElement("div"); - form.style.cssText = "display: flex; flex-direction: column; gap: 10px;"; + form.style.cssText = this.styles.settingsForm; fields.forEach(field => { if (field.type === 'checkbox') { const group = document.createElement("div"); - group.style.cssText = "display: flex; align-items: center; gap: 8px; margin: 5px 0;"; + group.style.cssText = this.styles.checkboxGroup; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; @@ -287,7 +285,7 @@ export class DistributedUI { const label = document.createElement("label"); label.htmlFor = field.id; label.textContent = field.label; - label.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;"; + label.style.cssText = this.styles.formLabelClickable; group.appendChild(checkbox); group.appendChild(label); @@ -613,7 +611,7 @@ export class DistributedUI { createSettingsToggle() { const settingsRow = document.createElement("div"); - settingsRow.style.cssText = "display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer; user-select: none;"; + settingsRow.style.cssText = this.styles.settingsToggle; const settingsTitle = document.createElement("h4"); settingsTitle.textContent = "Settings"; @@ -726,7 +724,7 @@ export class DistributedUI { // Always create a wrapper div for consistent layout const controlsWrapper = document.createElement("div"); - controlsWrapper.style.cssText = "display: flex; gap: 6px; align-items: stretch; width: 100%;"; + controlsWrapper.style.cssText = this.styles.controlsWrapper; if (config.dynamic && data) { if (isRemote) { @@ -835,7 +833,7 @@ export class DistributedUI { this.updateMasterDisplay(extension); saveBtn.textContent = "Saved!"; - setTimeout(() => { saveBtn.textContent = "Save"; }, 2000); + setTimeout(() => { saveBtn.textContent = "Save"; }, TIMEOUTS.FLASH_LONG); }, "background-color: #4a7c4a;"); saveBtn.style.cssText += " padding: 6px 12px; font-size: 12px;"; diff --git a/web/workerUtils.js b/web/workerUtils.js index 122d59e..745758a 100644 --- a/web/workerUtils.js +++ b/web/workerUtils.js @@ -1,4 +1,4 @@ -import { BUTTON_STYLES } from './constants.js'; +import { BUTTON_STYLES, TIMEOUTS } from './constants.js'; export async function handleWorkerOperation(extension, button, operation, successText, errorText) { const originalText = button.textContent; @@ -20,7 +20,7 @@ export async function handleWorkerOperation(extension, button, operation, succes button.textContent = originalText; button.style.cssText = originalStyle; button.disabled = false; - }, 3000); + }, TIMEOUTS.BUTTON_RESET); return; } @@ -49,7 +49,7 @@ export async function handleWorkerOperation(extension, button, operation, succes setTimeout(() => { button.textContent = originalText; button.style.cssText = originalStyle; - }, 3000); + }, TIMEOUTS.BUTTON_RESET); } finally { button.disabled = false; } @@ -60,7 +60,7 @@ export async function handleInterruptWorkers(extension, button) { name: "Interrupt", endpoint: "/interrupt", loadingText: "Interrupting...", - onSuccess: () => setTimeout(() => extension.checkAllWorkerStatuses(), 500) + onSuccess: () => setTimeout(() => extension.checkAllWorkerStatuses(), TIMEOUTS.POST_ACTION_DELAY) }, "Interrupted!", "Error! See Console"); }