Files
robertvoy-ComfyUI-Distributed/web/ui.js
T

919 lines
37 KiB
JavaScript

import { BUTTON_STYLES, UI_STYLES, STATUS_COLORS, UI_COLORS, TIMEOUTS } from './constants.js';
import { createButtonHelper as createButtonHelperFn } from './ui/buttonHelpers.js';
import { showCloudflareWarning as showCloudflareWarningFn } from './ui/cloudflareWarning.js';
import { createWorkerSettingsForm as createWorkerSettingsFormFn } from './ui/settingsForm.js';
import { renderEntityCard as renderEntityCardFn } from './ui/entityCard.js';
const cardConfigs = {
master: {
checkbox: {
enabled: true,
masterToggle: true,
title: "Toggle master participation in workloads"
},
statusDot: {
id: 'master-status',
initialColor: (_, extension) => extension.isMasterParticipating() ? STATUS_COLORS.ONLINE_GREEN : STATUS_COLORS.DISABLED_GRAY,
initialTitle: (_, extension) => extension.isMasterParticipating() ? 'Master participating' : 'Master orchestrator only',
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');
const participationEnabled = extension.isMasterParticipationEnabled();
const fallbackActive = extension.isMasterFallbackActive();
let delegateBadge = '';
if (!participationEnabled) {
delegateBadge = fallbackActive
? `<br><small style="color: #6bd06b;">Fallback active • Master executing</small>`
: `<br><small style="color: ${UI_COLORS.SECONDARY_TEXT};">Orchestrator-only mode</small>`;
}
return `<strong id="master-name-display">${data?.name || extension.config?.master?.name || "Master"}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};"><span id="master-cuda-info">${cudaInfo}Port ${port}</span></small>${delegateBadge}`;
},
controls: {
type: 'master'
},
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 `<strong>${data.name}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};">${data.host}</small>`;
} else if (isRemote) {
return `<strong>${data.name}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};">${data.host}:${data.port}</small>`;
} else {
const cudaInfo = data.cuda_device !== undefined ? `CUDA ${data.cuda_device} • ` : '';
return `<strong>${data.name}</strong><br><small style="color: ${UI_COLORS.MUTED_TEXT};">${cudaInfo}Port ${data.port}</small>`;
}
},
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: () => `<strong style="color: #aaa; font-size: 16px;">Add New Worker</strong><br><small style="color: ${UI_COLORS.BORDER_LIGHT};">[CUDA] • [Port]</small>`,
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: () => `<span style="color: ${UI_COLORS.ICON_COLOR}; font-weight: bold; font-size: 13px;">Add New Worker</span>`,
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 createButtonHelperFn(this, 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) {
return showCloudflareWarningFn(extension, masterHost);
}
updateStatusDot(workerId, color, title, pulsing = false) {
const statusDot = document.getElementById(`status-${workerId}`);
if (!statusDot) return;
const statusClasses = [
"worker-status--online",
"worker-status--offline",
"worker-status--unknown",
"worker-status--processing",
];
statusDot.classList.remove(...statusClasses);
const colorClassMap = {
[STATUS_COLORS.ONLINE_GREEN]: "worker-status--online",
[STATUS_COLORS.OFFLINE_RED]: "worker-status--offline",
[STATUS_COLORS.DISABLED_GRAY]: "worker-status--unknown",
[STATUS_COLORS.PROCESSING_YELLOW]: "worker-status--processing",
};
const statusClass = colorClassMap[color];
if (statusClass) {
statusDot.classList.add(statusClass);
statusDot.style.backgroundColor = "";
} else {
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) {
return createWorkerSettingsFormFn(this, extension, worker);
}
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;
const isMasterToggle = config?.masterToggle && typeof extension.isMasterParticipating === 'function';
if (isMasterToggle) {
const participationEnabled = extension.isMasterParticipationEnabled();
const fallbackActive = extension.isMasterFallbackActive();
const buildTitle = (enabled, fallback) => {
if (enabled) {
return "Master participating • Click to switch to orchestrator-only";
}
if (fallback) {
return "No workers selected • Master fallback execution active";
}
return "Master orchestrator-only • Click to re-enable participation";
};
checkbox.checked = participationEnabled;
checkbox.style.pointerEvents = "none";
column.style.cursor = "pointer";
column.title = buildTitle(participationEnabled, fallbackActive);
column.onclick = async (event) => {
if (event) {
event.stopPropagation();
event.preventDefault();
}
const nextState = !extension.isMasterParticipationEnabled();
const nextFallback = !nextState && extension.enabledWorkers.length === 0;
checkbox.checked = nextState;
column.title = buildTitle(nextState, nextFallback);
await extension.updateMasterParticipation(nextState);
};
} else 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);
};
}
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, extension);
}
if (typeof config.initialTitle === 'function') {
title = config.initialTitle(data, extension);
}
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.type === 'master') {
const participationEnabled = extension.isMasterParticipationEnabled();
const fallbackActive = extension.isMasterFallbackActive();
let message;
const badge = document.createElement("div");
badge.style.cssText = this.styles.infoBox;
if (fallbackActive) {
message = "No workers selected. Master fallback execution active.";
badge.textContent = message;
badge.style.backgroundColor = "#243024";
badge.style.color = "#6bd06b";
badge.style.border = "1px solid #335533";
} else if (!participationEnabled) {
message = "Master disabled: running as orchestrator only.";
badge.textContent = message;
badge.style.backgroundColor = "#3a3a3a";
badge.style.color = "#ffcc66";
} else {
message = "Master participating in workflows.";
badge.textContent = message;
}
controlsWrapper.appendChild(badge);
} else 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);
// Cloudflare tunnel toggle (simple button inside master settings)
const tunnelBtn = this.createButton("Enable Cloudflare Tunnel", (e) => extension.handleTunnelToggle(e.target), "background-color: #665533;");
tunnelBtn.id = "cloudflare-tunnel-button";
tunnelBtn.style.cssText = BUTTON_STYLES.base + " background-color: #665533; margin: 4px 0 -5px 0;";
settingsForm.appendChild(tunnelBtn);
extension.tunnelElements = { button: tunnelBtn };
extension.updateTunnelUIElements();
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) {
return renderEntityCardFn(this, cardConfigs, entityType, data, extension);
}
}