import { BUTTON_STYLES, UI_STYLES, STATUS_COLORS, UI_COLORS, TIMEOUTS } from './constants.js'; const cardConfigs = { master: { checkbox: { enabled: false, checked: true, disabled: true, opacity: 0.6, title: "Master node is always enabled" }, statusDot: { color: STATUS_COLORS.ONLINE_GREEN, title: 'Online', id: 'master-status', dynamic: true }, infoText: (data, extension) => { 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}`; }, controls: { type: 'info', text: 'Master', style: "background-color: #333; color: #999;" }, settings: { formType: 'master', id: 'master-settings', expandedTracker: 'masterSettingsExpanded' }, hover: true, expand: true, border: 'solid' }, worker: { checkbox: { enabled: true, title: "Enable/disable this worker" }, statusDot: { dynamic: true, initialColor: (data) => data.enabled ? STATUS_COLORS.OFFLINE_RED : STATUS_COLORS.DISABLED_GRAY, initialTitle: (data) => data.enabled ? "Checking status..." : "Disabled", pulsing: (data) => data.enabled, id: (data) => `status-${data.id}` }, infoText: (data, extension) => { const isRemote = extension.isRemoteWorker(data); const isCloud = data.type === 'cloud'; if (isCloud) { // For cloud workers, don't show port (it's always 443) return `${data.name}
${data.host}`; } else if (isRemote) { 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}`; } }, controls: { dynamic: true }, settings: { formType: 'worker', id: (data) => `settings-${data.id}`, expandedId: (data) => data?.id }, hover: true, expand: true, border: 'solid' }, blueprint: { checkbox: { type: 'icon', content: '+', width: 42, 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 ${UI_COLORS.BORDER_LIGHT}` }, infoText: () => `Add New Worker
[CUDA] • [Port]`, controls: { type: 'ghost', text: 'Configure', style: `border: 1px solid ${UI_COLORS.BORDER_DARK}; background: transparent; color: ${UI_COLORS.BORDER_LIGHT};` }, hover: 'placeholder', expand: false, border: 'dashed' }, add: { checkbox: { type: 'icon', content: '+', width: 43, style: `border-right: 1px dashed ${UI_COLORS.BORDER_DARK}; color: ${UI_COLORS.BORDER_LIGHT}; font-size: 18px;` }, statusDot: { color: 'transparent', border: `1px solid ${UI_COLORS.BORDER_LIGHT}` }, infoText: () => `Add New Worker`, controls: null, hover: 'placeholder', expand: false, border: 'dashed', minHeight: '48px' } }; export class DistributedUI { constructor() { // UI element styles this.styles = UI_STYLES; } createStatusDot(id, color = "#666", title = "Status") { const dot = document.createElement("span"); if (id) dot.id = id; dot.style.cssText = this.styles.statusDot + ` background-color: ${color};`; dot.title = title; return dot; } createButton(text, onClick, customStyle = "") { const button = document.createElement("button"); button.textContent = text; button.className = "distributed-button"; button.style.cssText = BUTTON_STYLES.base + customStyle; if (onClick) button.onclick = onClick; return button; } createButtonGroup(buttons, style = "") { const group = document.createElement("div"); group.style.cssText = this.styles.buttonGroup + style; buttons.forEach(button => group.appendChild(button)); return group; } createWorkerControls(workerId, handlers = {}) { const controlsDiv = document.createElement("div"); controlsDiv.id = `controls-${workerId}`; controlsDiv.style.cssText = this.styles.controlsDiv; const buttons = []; if (handlers.launch) { const launchBtn = this.createButton('Launch', handlers.launch); launchBtn.id = `launch-${workerId}`; launchBtn.title = "Launch this worker instance"; buttons.push(launchBtn); } if (handlers.stop) { const stopBtn = this.createButton('Stop', handlers.stop); stopBtn.id = `stop-${workerId}`; stopBtn.title = "Stop this worker instance"; buttons.push(stopBtn); } if (handlers.viewLog) { const logBtn = this.createButton('View Log', handlers.viewLog); logBtn.id = `log-${workerId}`; logBtn.title = "View worker log file"; buttons.push(logBtn); } buttons.forEach(btn => controlsDiv.appendChild(btn)); return controlsDiv; } createFormGroup(label, value, id, type = "text", placeholder = "") { const group = document.createElement("div"); group.style.cssText = this.styles.formGroup; const labelEl = document.createElement("label"); labelEl.textContent = label; labelEl.htmlFor = id; labelEl.style.cssText = this.styles.formLabel; const input = document.createElement("input"); input.type = type; input.id = id; input.value = value; input.placeholder = placeholder; input.style.cssText = this.styles.formInput; group.appendChild(labelEl); group.appendChild(input); return { group, input }; } createInfoBox(text) { const box = document.createElement("div"); box.style.cssText = this.styles.infoBox; box.textContent = text; return box; } addHoverEffect(element, onHover, onLeave) { element.onmouseover = onHover; element.onmouseout = onLeave; } createCard(type = 'worker', options = {}) { const card = document.createElement("div"); switch(type) { case 'master': case 'worker': card.style.cssText = this.styles.workerCard; break; case 'blueprint': 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 = this.styles.cardBase + this.styles.cardAdd; if (options.onClick) card.onclick = options.onClick; if (options.title) card.title = options.title; break; } if (options.onMouseEnter) { card.addEventListener('mouseenter', options.onMouseEnter); } if (options.onMouseLeave) { card.addEventListener('mouseleave', options.onMouseLeave); } return card; } createCardColumn(type = 'checkbox', options = {}) { const column = document.createElement("div"); switch(type) { case 'checkbox': column.style.cssText = this.styles.checkboxColumn; if (options.title) column.title = options.title; break; case 'icon': column.style.cssText = this.styles.columnBase + this.styles.iconColumn; break; case 'content': column.style.cssText = this.styles.contentColumn; break; } return column; } createInfoRow(options = {}) { const row = document.createElement("div"); row.style.cssText = this.styles.infoRow; if (options.onClick) row.onclick = options.onClick; return row; } createWorkerContent() { const content = document.createElement("div"); content.style.cssText = this.styles.workerContent; return content; } createSettingsForm(fields = [], options = {}) { const form = document.createElement("div"); form.style.cssText = this.styles.settingsForm; fields.forEach(field => { if (field.type === 'checkbox') { const group = document.createElement("div"); group.style.cssText = this.styles.checkboxGroup; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = field.id; checkbox.checked = field.checked || false; if (field.onChange) checkbox.onchange = field.onChange; const label = document.createElement("label"); label.htmlFor = field.id; label.textContent = field.label; label.style.cssText = this.styles.formLabelClickable; group.appendChild(checkbox); group.appendChild(label); form.appendChild(group); } else { const result = this.createFormGroup(field.label, field.value, field.id, field.type, field.placeholder); if (field.groupId) result.group.id = field.groupId; if (field.display) result.group.style.display = field.display; form.appendChild(result.group); } }); if (options.buttons) { const buttonGroup = this.createButtonGroup(options.buttons, options.buttonStyle || " margin-top: 8px;"); form.appendChild(buttonGroup); } return form; } createButtonHelper(text, onClick, style) { return this.createButton(text, onClick, style); } updateMasterDisplay(extension) { // Use persistent config value as fallback const cudaDevice = extension?.config?.master?.cuda_device ?? extension?.masterCudaDevice; // Update CUDA info if element exists const cudaInfo = document.getElementById('master-cuda-info'); if (cudaInfo) { const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80'); if (cudaDevice !== undefined && cudaDevice !== null) { cudaInfo.textContent = `CUDA ${cudaDevice} • Port ${port}`; } else { cudaInfo.textContent = `Port ${port}`; } } // Update name if changed const nameDisplay = document.getElementById('master-name-display'); if (nameDisplay && extension?.config?.master?.name) { nameDisplay.textContent = extension.config.master.name; } } showToast(app, severity, summary, detail, life = 3000) { if (app.extensionManager?.toast?.add) { app.extensionManager.toast.add({ severity, summary, detail, life }); } } showCloudflareWarning(extension, masterHost) { // Remove any existing banner first const existingBanner = document.getElementById('cloudflare-warning-banner'); if (existingBanner) { existingBanner.remove(); } // Create warning banner const banner = document.createElement('div'); banner.id = 'cloudflare-warning-banner'; banner.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; background: #ff9800; color: #333; padding: 8px 16px; text-align: center; z-index: 10000; display: flex; align-items: center; justify-content: center; gap: 16px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; const messageSpan = document.createElement('span'); messageSpan.innerHTML = `Connection issue: Master address ${masterHost} is not reachable. The cloudflare tunnel may be offline.`; messageSpan.style.fontSize = '13px'; const resetButton = document.createElement('button'); resetButton.textContent = 'Reset Master Address'; resetButton.style.cssText = ` background: #333; color: white; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s; `; resetButton.onmouseover = () => resetButton.style.background = '#555'; resetButton.onmouseout = () => resetButton.style.background = '#333'; const dismissButton = document.createElement('button'); dismissButton.textContent = 'Dismiss'; dismissButton.style.cssText = ` background: transparent; color: #333; border: 1px solid #333; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; transition: opacity 0.2s; `; dismissButton.onmouseover = () => dismissButton.style.opacity = '0.7'; dismissButton.onmouseout = () => dismissButton.style.opacity = '1'; // Add click handlers resetButton.onclick = async () => { resetButton.disabled = true; resetButton.textContent = 'Resetting...'; try { // Save with empty host - this will trigger auto-detection await extension.api.updateMaster({ name: extension.config?.master?.name || "Master", host: "" }); // Clear the local config host so detectMasterIP() doesn't skip if (extension.config?.master) { extension.config.master.host = ""; } // The API call above doesn't trigger auto-detection, so we need to do it await extension.detectMasterIP(); // Reload config to get the new detected IP await extension.loadConfig(); // Log the new master URL for debugging const newMasterUrl = extension.getMasterUrl(); extension.log(`Master host reset. New URL: ${newMasterUrl}`, "info"); // Update UI if sidebar is open if (extension.panelElement) { const hostInput = document.getElementById('master-host'); if (hostInput) { hostInput.value = extension.config?.master?.host || ""; } } // Show success message with the actual URL that will be used extension.app.extensionManager.toast.add({ severity: "success", summary: "Master Host Reset", detail: `New address: ${newMasterUrl}`, life: 4000 }); banner.remove(); } catch (error) { resetButton.disabled = false; resetButton.textContent = 'Reset Master Host'; extension.log(`Failed to reset master host: ${error.message}`, "error"); } }; dismissButton.onclick = () => banner.remove(); // Assemble banner banner.appendChild(messageSpan); banner.appendChild(resetButton); banner.appendChild(dismissButton); // Add to page document.body.prepend(banner); // Auto-dismiss after 30 seconds setTimeout(() => { if (document.getElementById('cloudflare-warning-banner')) { banner.style.transition = 'opacity 0.5s'; banner.style.opacity = '0'; setTimeout(() => banner.remove(), 500); } }, 30000); } updateStatusDot(workerId, color, title, pulsing = false) { const statusDot = document.getElementById(`status-${workerId}`); if (!statusDot) return; statusDot.style.backgroundColor = color; statusDot.title = title; statusDot.classList.toggle('status-pulsing', pulsing); } showLogModal(extension, workerId, logData) { // Remove any existing modal const existingModal = document.getElementById('distributed-log-modal'); if (existingModal) { existingModal.remove(); } const worker = extension.config.workers.find(w => w.id === workerId); const workerName = worker?.name || `Worker ${workerId}`; // Create modal container const modal = document.createElement('div'); modal.id = 'distributed-log-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; `; // Create modal content const content = document.createElement('div'); content.style.cssText = ` background: #1e1e1e; border-radius: 8px; width: 90%; max-width: 1200px; height: 80%; display: flex; flex-direction: column; border: 1px solid #444; `; // Header const header = document.createElement('div'); header.style.cssText = ` padding: 15px 20px; border-bottom: 1px solid #444; display: flex; justify-content: space-between; align-items: center; `; const title = document.createElement('h3'); title.textContent = `${workerName} - Log Viewer`; title.style.cssText = 'margin: 0; color: #fff;'; const headerButtons = document.createElement('div'); headerButtons.style.cssText = 'display: flex; gap: 20px; align-items: center;'; // Auto-refresh container const refreshContainer = document.createElement('div'); refreshContainer.style.cssText = 'display: flex; align-items: center; gap: 4px;'; // Auto-refresh checkbox const refreshCheckbox = document.createElement('input'); refreshCheckbox.type = 'checkbox'; refreshCheckbox.id = 'log-auto-refresh'; refreshCheckbox.checked = true; // Enabled by default refreshCheckbox.style.cssText = 'cursor: pointer;'; refreshCheckbox.onchange = (e) => { if (e.target.checked) { extension.startLogAutoRefresh(workerId); } else { extension.stopLogAutoRefresh(); } }; const refreshLabel = document.createElement('label'); refreshLabel.htmlFor = 'log-auto-refresh'; refreshLabel.style.cssText = 'font-size: 12px; color: #ccc; cursor: pointer; white-space: nowrap;'; refreshLabel.textContent = 'Auto-refresh'; // Add checkbox and label to container refreshContainer.appendChild(refreshCheckbox); refreshContainer.appendChild(refreshLabel); // Close button const closeBtn = this.createButton('✕', () => { extension.stopLogAutoRefresh(); modal.remove(); }, 'background-color: #c04c4c;'); closeBtn.style.cssText += ' padding: 5px 10px; font-size: 14px; font-weight: bold;'; headerButtons.appendChild(refreshContainer); headerButtons.appendChild(closeBtn); header.appendChild(title); header.appendChild(headerButtons); // Log content area const logContainer = document.createElement('div'); logContainer.style.cssText = ` flex: 1; overflow: auto; padding: 15px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 12px; line-height: 1.4; color: #ddd; background: #0d0d0d; white-space: pre-wrap; word-wrap: break-word; `; logContainer.id = 'distributed-log-content'; logContainer.textContent = logData.content; // Auto-scroll to bottom setTimeout(() => { logContainer.scrollTop = logContainer.scrollHeight; }, 0); // Status bar const statusBar = document.createElement('div'); statusBar.style.cssText = ` padding: 10px 20px; border-top: 1px solid #444; font-size: 11px; color: #888; `; statusBar.textContent = `Log file: ${logData.log_file}`; if (logData.truncated) { statusBar.textContent += ` (showing last ${logData.lines_shown} lines of ${this.formatFileSize(logData.file_size)})`; } // Assemble modal content.appendChild(header); content.appendChild(logContainer); content.appendChild(statusBar); modal.appendChild(content); // Close on background click modal.onclick = (e) => { if (e.target === modal) { extension.stopLogAutoRefresh(); modal.remove(); } }; // Close on Escape key const handleEscape = (e) => { if (e.key === 'Escape') { extension.stopLogAutoRefresh(); modal.remove(); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); document.body.appendChild(modal); // Start auto-refresh extension.startLogAutoRefresh(workerId); } formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } createWorkerSettingsForm(extension, worker) { const form = document.createElement("div"); form.style.cssText = "display: flex; flex-direction: column; gap: 8px;"; // Name field const nameGroup = this.createFormGroup("Name:", worker.name, `name-${worker.id}`); form.appendChild(nameGroup.group); // Worker type dropdown const typeGroup = document.createElement("div"); typeGroup.style.cssText = "display: flex; flex-direction: column; gap: 4px; margin: 5px 0;"; const typeLabel = document.createElement("label"); typeLabel.htmlFor = `worker-type-${worker.id}`; typeLabel.textContent = "Worker Type:"; typeLabel.style.cssText = "font-size: 12px; color: #ccc;"; const typeSelect = document.createElement("select"); typeSelect.id = `worker-type-${worker.id}`; typeSelect.style.cssText = "padding: 4px 8px; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; font-size: 12px;"; // Create options const localOption = document.createElement("option"); localOption.value = "local"; localOption.textContent = "Local"; const remoteOption = document.createElement("option"); remoteOption.value = "remote"; remoteOption.textContent = "Remote"; const cloudOption = document.createElement("option"); cloudOption.value = "cloud"; cloudOption.textContent = "Cloud"; typeSelect.appendChild(localOption); typeSelect.appendChild(remoteOption); typeSelect.appendChild(cloudOption); // Create powered by Runpod text (initially hidden) const runpodText = document.createElement("a"); runpodText.id = `runpod-text-${worker.id}`; runpodText.href = "https://get.runpod.io/0bw29uf3ug0p"; runpodText.target = "_blank"; runpodText.textContent = "Deploy Cloud Worker with Runpod"; runpodText.style.cssText = "font-size: 12px; color: #4a90e2; text-decoration: none; margin-top: 4px; display: none; cursor: pointer;"; // Store the onchange function to be assigned later const createOnChangeHandler = () => { return (e) => { const workerType = e.target.value; // Show/hide relevant fields const hostGroup = document.getElementById(`host-group-${worker.id}`); const hostInput = document.getElementById(`host-${worker.id}`); const portGroup = document.getElementById(`port-group-${worker.id}`); const portInput = document.getElementById(`port-${worker.id}`); const cudaGroup = document.getElementById(`cuda-group-${worker.id}`); const argsGroup = document.getElementById(`args-group-${worker.id}`); const runpodTextElem = document.getElementById(`runpod-text-${worker.id}`); // Check if elements exist before accessing them if (!hostGroup || !portGroup || !cudaGroup || !argsGroup || !runpodTextElem || !hostInput || !portInput) { return; // Elements not ready yet } if (workerType === "local") { hostGroup.style.display = "none"; portGroup.style.display = "flex"; cudaGroup.style.display = "flex"; argsGroup.style.display = "flex"; runpodTextElem.style.display = "none"; } else if (workerType === "remote") { hostGroup.style.display = "flex"; portGroup.style.display = "flex"; cudaGroup.style.display = "none"; argsGroup.style.display = "none"; runpodTextElem.style.display = "none"; // Update placeholder for remote workers hostInput.placeholder = "e.g., 192.168.1.100"; // If switching to remote and host is localhost, clear it if (hostInput.value === "localhost" || hostInput.value === "127.0.0.1") { hostInput.value = ""; } } else if (workerType === "cloud") { hostGroup.style.display = "flex"; portGroup.style.display = "flex"; // Keep port visible for cloud workers cudaGroup.style.display = "none"; argsGroup.style.display = "none"; runpodTextElem.style.display = "block"; // Update placeholder for cloud workers hostInput.placeholder = "e.g., your-cloud-worker.trycloudflare.com"; // Set port to 443 for cloud workers portInput.value = "443"; // If switching to cloud and host is localhost, clear it if (hostInput.value === "localhost" || hostInput.value === "127.0.0.1") { hostInput.value = ""; } } }; }; typeGroup.appendChild(typeLabel); typeGroup.appendChild(typeSelect); typeGroup.appendChild(runpodText); form.appendChild(typeGroup); // Host field (only for remote workers) const hostGroup = this.createFormGroup("Host:", worker.host || "", `host-${worker.id}`, "text", "e.g., 192.168.1.100"); hostGroup.group.id = `host-group-${worker.id}`; hostGroup.group.style.display = (extension.isRemoteWorker(worker) || worker.type === "cloud") ? "flex" : "none"; form.appendChild(hostGroup.group); // Port field const portGroup = this.createFormGroup("Port:", worker.port, `port-${worker.id}`, "number"); portGroup.group.id = `port-group-${worker.id}`; form.appendChild(portGroup.group); // CUDA Device field (only for local workers) const cudaGroup = this.createFormGroup("CUDA Device:", worker.cuda_device || 0, `cuda-${worker.id}`, "number"); cudaGroup.group.id = `cuda-group-${worker.id}`; cudaGroup.group.style.display = (extension.isRemoteWorker(worker) || worker.type === "cloud") ? "none" : "flex"; form.appendChild(cudaGroup.group); // Extra Args field (only for local workers) const argsGroup = this.createFormGroup("Extra Args:", worker.extra_args || "", `args-${worker.id}`); argsGroup.group.id = `args-group-${worker.id}`; argsGroup.group.style.display = (extension.isRemoteWorker(worker) || worker.type === "cloud") ? "none" : "flex"; form.appendChild(argsGroup.group); // Buttons const saveBtn = this.createButton("Save", () => extension.saveWorkerSettings(worker.id), "background-color: #4a7c4a;"); saveBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.success; const cancelBtn = this.createButton("Cancel", () => extension.cancelWorkerSettings(worker.id), "background-color: #555;"); cancelBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.cancel; const deleteBtn = this.createButton("Delete", () => extension.deleteWorker(worker.id), "background-color: #7c4a4a;"); deleteBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.error + BUTTON_STYLES.marginLeftAuto; const buttonGroup = this.createButtonGroup([saveBtn, cancelBtn, deleteBtn], " margin-top: 8px;"); form.appendChild(buttonGroup); // Assign the onchange handler now that all elements are created typeSelect.onchange = createOnChangeHandler(); // Set initial value and trigger state after all DOM elements are created if (worker.type === "cloud") { typeSelect.value = "cloud"; // Show Runpod text immediately for cloud workers runpodText.style.display = "block"; } else if (extension.isRemoteWorker(worker)) { typeSelect.value = "remote"; } else { typeSelect.value = "local"; } // Trigger initial state now that all elements exist typeSelect.dispatchEvent(new Event('change')); return form; } createSettingsToggle() { const settingsRow = document.createElement("div"); settingsRow.style.cssText = this.styles.settingsToggle; const settingsTitle = document.createElement("h4"); settingsTitle.textContent = "Settings"; settingsTitle.style.cssText = "margin: 0; font-size: 14px;"; const settingsToggle = document.createElement("span"); settingsToggle.textContent = "▶"; // Right arrow when collapsed settingsToggle.style.cssText = "font-size: 12px; color: #888; transition: all 0.2s ease;"; settingsRow.appendChild(settingsToggle); settingsRow.appendChild(settingsTitle); return { settingsRow, settingsToggle }; } createCheckboxOrIconColumn(config, data, extension) { const column = this.createCardColumn('checkbox'); if (config?.type === 'icon') { column.style.flex = `0 0 ${config.width || 44}px`; column.innerHTML = config.content || '+'; if (config.style) { const styles = config.style.split(';').filter(s => s.trim()); styles.forEach(style => { const [prop, value] = style.split(':').map(s => s.trim()); if (prop && value) { column.style[prop.replace(/-([a-z])/g, (g) => g[1].toUpperCase())] = value; } }); } } else { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = `gpu-${data?.id || 'master'}`; checkbox.checked = config?.checked !== undefined ? config.checked : data?.enabled; checkbox.disabled = config?.disabled || false; checkbox.style.cssText = `cursor: ${config?.disabled ? 'default' : 'pointer'}; width: 16px; height: 16px;`; if (config?.opacity) checkbox.style.opacity = config.opacity; if (config?.title) column.title = config.title; if (config?.enabled && !config?.disabled && data?.id) { checkbox.style.pointerEvents = "none"; column.style.cursor = "pointer"; column.onclick = async () => { checkbox.checked = !checkbox.checked; await extension.updateWorkerEnabled(data.id, checkbox.checked); extension.updateSummary(); }; } column.appendChild(checkbox); } return column; } createStatusDotHelper(config, data, extension) { let color = config.color || "#666"; let title = config.title || "Status"; let id = config.id; if (typeof config.initialColor === 'function') { color = config.initialColor(data); } if (typeof config.initialTitle === 'function') { title = config.initialTitle(data); } if (typeof config.id === 'function') { id = config.id(data); } const dot = this.createStatusDot(id, color, title); if (config.border) { dot.style.border = config.border; } if (config.pulsing && (typeof config.pulsing !== 'function' || config.pulsing(data))) { dot.classList.add('status-pulsing'); } return dot; } createSettingsToggleHelper(expandedId, extension) { const arrow = document.createElement("span"); arrow.className = "settings-arrow"; arrow.innerHTML = "▶"; arrow.style.cssText = this.styles.settingsArrow; const isExpanded = typeof expandedId === 'function' ? extension.state.isWorkerExpanded(expandedId(extension)) : (expandedId === 'master' ? false : extension.state.isWorkerExpanded(expandedId)); if (isExpanded) { arrow.style.transform = "rotate(90deg)"; } return arrow; } createControlsSection(config, data, extension, isRemote) { if (!config) return null; const controlsDiv = document.createElement("div"); controlsDiv.id = `controls-${data?.id || 'master'}`; controlsDiv.style.cssText = this.styles.controlsDiv; // Always create a wrapper div for consistent layout const controlsWrapper = document.createElement("div"); controlsWrapper.style.cssText = this.styles.controlsWrapper; if (config.dynamic && data) { if (isRemote) { const isCloud = data.type === 'cloud'; const workerTypeText = isCloud ? "Cloud worker" : "Remote worker"; const remoteInfo = this.createButton(workerTypeText, null, BUTTON_STYLES.info); remoteInfo.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.info + " color: #999; cursor: default;"; remoteInfo.disabled = true; controlsWrapper.appendChild(remoteInfo); } else { const controls = this.createWorkerControls(data.id, { launch: () => extension.launchWorker(data.id), stop: () => extension.stopWorker(data.id), viewLog: () => extension.viewWorkerLog(data.id) }); const launchBtn = controls.querySelector(`#launch-${data.id}`); const stopBtn = controls.querySelector(`#stop-${data.id}`); const logBtn = controls.querySelector(`#log-${data.id}`); launchBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.launch; launchBtn.title = "Launch worker (runs in background with logging)"; stopBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.stop + BUTTON_STYLES.hidden; stopBtn.title = "Stop worker"; logBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.log + BUTTON_STYLES.hidden; while (controls.firstChild) { controlsWrapper.appendChild(controls.firstChild); } } } else if (config.type === 'info') { const infoBtn = this.createButton(config.text, null, config.style || ""); infoBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + (config.style || BUTTON_STYLES.info) + " cursor: default;"; infoBtn.disabled = true; controlsWrapper.appendChild(infoBtn); } else if (config.type === 'ghost') { const ghostBtn = document.createElement("button"); ghostBtn.style.cssText = `flex: 1; padding: 5px 14px; font-size: 11px; font-weight: 500; border-radius: 4px; cursor: default; ${config.style || ""}`; ghostBtn.textContent = config.text; ghostBtn.disabled = true; controlsWrapper.appendChild(ghostBtn); } controlsDiv.appendChild(controlsWrapper); return controlsDiv; } createSettingsSection(config, data, extension) { const settingsDiv = document.createElement("div"); const settingsId = typeof config.id === 'function' ? config.id(data) : config.id; settingsDiv.id = settingsId; settingsDiv.className = "worker-settings"; const expandedId = typeof config.expandedId === 'function' ? config.expandedId(data) : config.expandedId; const isExpanded = expandedId === 'master-settings' ? false : extension.state.isWorkerExpanded(expandedId); settingsDiv.style.cssText = this.styles.workerSettings; if (isExpanded) { settingsDiv.classList.add("expanded"); settingsDiv.style.padding = "12px"; settingsDiv.style.marginTop = "8px"; settingsDiv.style.marginBottom = "8px"; } let settingsForm; if (config.formType === 'master') { settingsForm = this.createMasterSettingsForm(extension, data); } else if (config.formType === 'worker') { settingsForm = this.createWorkerSettingsForm(extension, data); } if (settingsForm) { settingsDiv.appendChild(settingsForm); } return settingsDiv; } createMasterSettingsForm(extension, data) { const settingsForm = document.createElement("div"); settingsForm.style.cssText = "display: flex; flex-direction: column; gap: 8px;"; const nameResult = this.createFormGroup("Name:", extension.config?.master?.name || "Master", "master-name"); settingsForm.appendChild(nameResult.group); const hostResult = this.createFormGroup("Host:", extension.config?.master?.host || "", "master-host", "text", "Auto-detect if empty"); settingsForm.appendChild(hostResult.group); const saveBtn = this.createButton("Save", async () => { const nameInput = document.getElementById('master-name'); const hostInput = document.getElementById('master-host'); if (!extension.config.master) extension.config.master = {}; extension.config.master.name = nameInput.value.trim() || "Master"; const hostValue = hostInput.value.trim(); await extension.api.updateMaster({ host: hostValue, name: extension.config.master.name }); // Reload config to refresh any updated values await extension.loadConfig(); // If host was emptied, trigger auto-detection if (!hostValue) { extension.log("Host field cleared, triggering IP auto-detection", "debug"); await extension.detectMasterIP(); // Reload config again to get the auto-detected IP await extension.loadConfig(); // Update the input field with the detected IP document.getElementById('master-host').value = extension.config?.master?.host || ""; } document.getElementById('master-name-display').textContent = extension.config.master.name; this.updateMasterDisplay(extension); // Show toast notification if (extension.app?.extensionManager?.toast) { const message = !hostValue ? "Master settings saved and IP auto-detected" : "Master settings saved successfully"; extension.app.extensionManager.toast.add({ severity: "success", summary: "Master Updated", detail: message, life: 3000 }); } saveBtn.textContent = "Saved!"; setTimeout(() => { saveBtn.textContent = "Save"; }, TIMEOUTS.FLASH_LONG); }, "background-color: #4a7c4a;"); saveBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.success; const cancelBtn = this.createButton("Cancel", () => { document.getElementById('master-name').value = extension.config?.master?.name || "Master"; document.getElementById('master-host').value = extension.config?.master?.host || ""; }, "background-color: #555;"); cancelBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.cancel; const buttonGroup = this.createButtonGroup([saveBtn, cancelBtn], " margin-top: 8px;"); settingsForm.appendChild(buttonGroup); return settingsForm; } addPlaceholderHover(card, leftColumn, entityType) { card.onmouseover = () => { if (entityType === 'blueprint') { card.style.borderColor = "#777"; card.style.backgroundColor = "rgba(255, 255, 255, 0.05)"; leftColumn.style.color = "#999"; } else { card.style.borderColor = "#666"; card.style.backgroundColor = "rgba(255, 255, 255, 0.02)"; leftColumn.style.color = "#888"; leftColumn.style.borderColor = "#666"; } }; card.onmouseout = () => { if (entityType === 'blueprint') { card.style.borderColor = "#555"; card.style.backgroundColor = "rgba(255, 255, 255, 0.02)"; leftColumn.style.color = "#777"; } else { card.style.borderColor = "#444"; card.style.backgroundColor = "transparent"; leftColumn.style.color = "#555"; leftColumn.style.borderColor = "#444"; } }; } renderEntityCard(entityType, data, extension) { const config = cardConfigs[entityType] || {}; const isPlaceholder = entityType === 'blueprint' || entityType === 'add'; const isWorker = entityType === 'worker'; const isMaster = entityType === 'master'; const isRemote = isWorker && extension.isRemoteWorker(data); const cardOptions = { onClick: isPlaceholder ? data?.onClick : null }; if (isPlaceholder) { cardOptions.title = entityType === 'blueprint' ? "Click to add your first worker" : "Click to add a new worker"; } const card = this.createCard(entityType, cardOptions); const leftColumn = this.createCheckboxOrIconColumn(config.checkbox, data, extension); card.appendChild(leftColumn); const rightColumn = this.createCardColumn('content'); const infoRow = this.createInfoRow(); if (config.infoRowPadding) { infoRow.style.padding = config.infoRowPadding; } if (config.minHeight === 'auto') { infoRow.style.minHeight = 'auto'; } else if (config.minHeight) { infoRow.style.minHeight = config.minHeight; } if (config.expand) { infoRow.title = "Click to expand settings"; infoRow.onclick = () => { if (isMaster) { const masterSettingsExpanded = !extension.masterSettingsExpanded; extension.masterSettingsExpanded = masterSettingsExpanded; const masterSettingsDiv = document.getElementById("master-settings"); const arrow = infoRow.querySelector('.settings-arrow'); if (masterSettingsExpanded) { masterSettingsDiv.classList.add("expanded"); masterSettingsDiv.style.padding = "12px"; masterSettingsDiv.style.marginTop = "8px"; masterSettingsDiv.style.marginBottom = "8px"; arrow.style.transform = "rotate(90deg)"; } else { masterSettingsDiv.classList.remove("expanded"); masterSettingsDiv.style.padding = "0 12px"; masterSettingsDiv.style.marginTop = "0"; masterSettingsDiv.style.marginBottom = "0"; arrow.style.transform = "rotate(0deg)"; } } else { extension.toggleWorkerExpanded(data.id); } }; } const workerContent = this.createWorkerContent(); if (entityType === 'add') { workerContent.style.alignItems = "center"; } const statusDot = this.createStatusDotHelper(config.statusDot, data, extension); workerContent.appendChild(statusDot); const infoSpan = document.createElement("span"); infoSpan.innerHTML = config.infoText(data, extension); workerContent.appendChild(infoSpan); infoRow.appendChild(workerContent); let settingsArrow; if (config.expand) { const expandedId = config.settings?.expandedId || (isMaster ? 'master' : data?.id); settingsArrow = this.createSettingsToggleHelper(expandedId, extension); if (isMaster && !extension.masterSettingsExpanded) { settingsArrow.style.transform = "rotate(0deg)"; } infoRow.appendChild(settingsArrow); } rightColumn.appendChild(infoRow); if (config.hover === true) { rightColumn.onmouseover = () => { rightColumn.style.backgroundColor = "#333"; if (settingsArrow) settingsArrow.style.color = "#fff"; }; rightColumn.onmouseout = () => { rightColumn.style.backgroundColor = "transparent"; if (settingsArrow) settingsArrow.style.color = "#888"; }; } const controlsDiv = this.createControlsSection(config.controls, data, extension, isRemote); if (controlsDiv) { rightColumn.appendChild(controlsDiv); } if (config.settings) { const settingsDiv = this.createSettingsSection(config.settings, data, extension); rightColumn.appendChild(settingsDiv); } card.appendChild(rightColumn); if (config.hover === 'placeholder') { this.addPlaceholderHover(card, leftColumn, entityType); } if (isWorker && !isRemote) { extension.updateWorkerControls(data.id); } return card; } }