include master in UI panel, auto detect local workers on first launch
This commit is contained in:
+52
-8
@@ -95,6 +95,53 @@ async def get_network_info_endpoint(request):
|
||||
"""Get network interfaces and recommend best IP for master."""
|
||||
import socket
|
||||
|
||||
# Get CUDA device if available
|
||||
cuda_device = None
|
||||
cuda_device_count = 0
|
||||
physical_device_count = 0
|
||||
|
||||
if torch.cuda.is_available():
|
||||
try:
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# Get visible device count (what PyTorch sees)
|
||||
cuda_device_count = torch.cuda.device_count()
|
||||
|
||||
# Try to get actual physical device info
|
||||
cuda_visible = os.environ.get('CUDA_VISIBLE_DEVICES', '')
|
||||
|
||||
# Method 1: Parse CUDA_VISIBLE_DEVICES
|
||||
if cuda_visible and cuda_visible.strip():
|
||||
visible_devices = [int(d.strip()) for d in cuda_visible.split(',') if d.strip().isdigit()]
|
||||
if visible_devices:
|
||||
# Get the first visible device as the actual physical device
|
||||
cuda_device = visible_devices[0]
|
||||
|
||||
# Try to get total physical device count using nvidia-smi
|
||||
try:
|
||||
result = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
physical_device_count = len(result.stdout.strip().split('\n'))
|
||||
else:
|
||||
physical_device_count = max(visible_devices) + 1 # Best guess
|
||||
except:
|
||||
physical_device_count = max(visible_devices) + 1 # Best guess
|
||||
else:
|
||||
cuda_device = 0
|
||||
physical_device_count = cuda_device_count
|
||||
else:
|
||||
# No CUDA_VISIBLE_DEVICES set, current device is actual device
|
||||
cuda_device = torch.cuda.current_device()
|
||||
physical_device_count = cuda_device_count
|
||||
|
||||
except Exception as e:
|
||||
debug_log(f"CUDA detection error: {e}")
|
||||
cuda_device = None
|
||||
cuda_device_count = 0
|
||||
physical_device_count = 0
|
||||
|
||||
def get_network_ips():
|
||||
"""Get all network IPs, trying multiple methods."""
|
||||
ips = []
|
||||
@@ -195,6 +242,8 @@ async def get_network_info_endpoint(request):
|
||||
"hostname": hostname,
|
||||
"all_ips": all_ips,
|
||||
"recommended_ip": recommended_ip,
|
||||
"cuda_device": cuda_device,
|
||||
"cuda_device_count": physical_device_count if physical_device_count > 0 else cuda_device_count,
|
||||
"message": "Auto-detected network configuration"
|
||||
})
|
||||
except Exception as e:
|
||||
@@ -347,6 +396,8 @@ async def update_master_endpoint(request):
|
||||
config['master'] = {}
|
||||
|
||||
# Update all provided fields
|
||||
if "name" in data:
|
||||
config['master']['name'] = data['name']
|
||||
if "host" in data:
|
||||
config['master']['host'] = data['host']
|
||||
if "port" in data:
|
||||
@@ -1340,14 +1391,7 @@ class DistributedCollectorNode:
|
||||
session = await get_client_session()
|
||||
url = f"{master_url}/distributed/job_complete"
|
||||
debug_log(f"Worker - Sending image to URL: {url}")
|
||||
# Disable SSL verification for HTTPS URLs (temporary fix for tunnels)
|
||||
ssl_context = None
|
||||
if url.startswith('https://'):
|
||||
import ssl
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
async with session.post(url, data=data, ssl=ssl_context) as response:
|
||||
async with session.post(url, data=data) as response:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
log(f"Worker - Failed to send image {image_index+1} to master: {e}")
|
||||
|
||||
+462
-82
@@ -75,12 +75,13 @@ class DistributedExtension {
|
||||
// Inject CSS for pulsing animation
|
||||
this.injectStyles();
|
||||
|
||||
this.loadConfig().then(() => {
|
||||
this.loadConfig().then(async () => {
|
||||
this.registerSidebarTab();
|
||||
this.setupInterceptor();
|
||||
this.startStatusChecking();
|
||||
this.loadManagedWorkers();
|
||||
this.detectMasterIP(); // Auto-detect master IP on startup
|
||||
// Detect master IP after everything is set up
|
||||
this.detectMasterIP();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ class DistributedExtension {
|
||||
const response = await fetch(`${window.location.origin}/distributed/config`);
|
||||
if (response.ok) {
|
||||
this.config = await response.json();
|
||||
this.debugLog("Loaded config: " + JSON.stringify(this.config));
|
||||
this.log("Loaded config: " + JSON.stringify(this.config));
|
||||
} else {
|
||||
this.log("Failed to load config");
|
||||
this.config = { workers: [], settings: {} };
|
||||
@@ -143,6 +144,7 @@ class DistributedExtension {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ worker_id: workerId, enabled: enabled })
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.log("Error updating worker: " + error.message);
|
||||
}
|
||||
@@ -211,7 +213,7 @@ class DistributedExtension {
|
||||
try {
|
||||
const targetsToInterrupt = this.enabledWorkers.map(w => ({
|
||||
name: w.name,
|
||||
url: `http://${w.host || window.location.hostname}:${w.port}`
|
||||
url: this.getWorkerUrl(w)
|
||||
}));
|
||||
|
||||
if (targetsToInterrupt.length === 0) {
|
||||
@@ -266,7 +268,7 @@ class DistributedExtension {
|
||||
try {
|
||||
const urlsToClear = this.enabledWorkers.map(w => ({
|
||||
name: w.name,
|
||||
url: `http://${w.host || window.location.hostname}:${w.port}`
|
||||
url: this.getWorkerUrl(w)
|
||||
}));
|
||||
|
||||
if (urlsToClear.length === 0) {
|
||||
@@ -324,7 +326,7 @@ class DistributedExtension {
|
||||
}
|
||||
|
||||
|
||||
renderSidebarContent(el) {
|
||||
async renderSidebarContent(el) {
|
||||
// Panel is being opened/rendered
|
||||
this.debugLog("Panel opened");
|
||||
|
||||
@@ -333,8 +335,19 @@ class DistributedExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store reference to the panel element
|
||||
this.panelElement = el;
|
||||
// Prevent infinite recursion
|
||||
if (this._isRendering) {
|
||||
this.debugLog("Already rendering, skipping");
|
||||
return;
|
||||
}
|
||||
this._isRendering = true;
|
||||
|
||||
try {
|
||||
// Store reference to the panel element
|
||||
this.panelElement = el;
|
||||
|
||||
// Reload config when panel opens to pick up any external changes
|
||||
await this.loadConfig();
|
||||
|
||||
// Reload managed workers when panel opens
|
||||
this.loadManagedWorkers().then(() => {
|
||||
@@ -349,20 +362,275 @@ class DistributedExtension {
|
||||
setTimeout(() => this.checkAllWorkerStatuses(), 0);
|
||||
|
||||
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;";
|
||||
|
||||
const toolbarStart = document.createElement("div");
|
||||
toolbarStart.className = "p-toolbar-start";
|
||||
|
||||
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: 100%;";
|
||||
container.style.cssText = "padding: 15px; display: flex; flex-direction: column; height: calc(100% - 32px);";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = "ComfyUI Distributed";
|
||||
title.style.cssText = "margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 1px solid #444;";
|
||||
container.appendChild(title);
|
||||
// Detect master info on panel open (in case CUDA info wasn't available at startup)
|
||||
this.log(`Panel opened. CUDA device count: ${this.cudaDeviceCount}, Workers: ${this.config?.workers?.length || 0}`);
|
||||
if (!this.cudaDeviceCount) {
|
||||
await this.detectMasterIP();
|
||||
}
|
||||
|
||||
// Check if we should auto-populate workers after detection
|
||||
if (this.cudaDeviceCount > 0 && (!this.config.workers || this.config.workers.length === 0)) {
|
||||
this.log(`Auto-populating workers based on ${this.cudaDeviceCount} CUDA devices (excluding master on CUDA ${this.masterCudaDevice})`);
|
||||
|
||||
const newWorkers = [];
|
||||
let workerNum = 1;
|
||||
let portOffset = 0;
|
||||
|
||||
for (let i = 0; i < this.cudaDeviceCount; i++) {
|
||||
// Skip the CUDA device used by master
|
||||
if (i === this.masterCudaDevice) {
|
||||
this.log(`Skipping CUDA ${i} (used by master)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const worker = {
|
||||
id: Date.now() + i,
|
||||
name: `Worker ${workerNum}`,
|
||||
host: "localhost",
|
||||
port: 8189 + portOffset,
|
||||
cuda_device: i,
|
||||
enabled: true
|
||||
};
|
||||
newWorkers.push(worker);
|
||||
workerNum++;
|
||||
portOffset++;
|
||||
}
|
||||
|
||||
// Only proceed if we have workers to add
|
||||
if (newWorkers.length > 0) {
|
||||
this.log(`Auto-populating ${newWorkers.length} workers`);
|
||||
|
||||
// Add workers to config
|
||||
this.config.workers = newWorkers;
|
||||
|
||||
// Save each worker using the update endpoint
|
||||
for (const worker of newWorkers) {
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/distributed/config/update_worker`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
worker_id: worker.id,
|
||||
...worker
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.log(`Failed to save worker ${worker.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Error saving worker ${worker.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`Auto-populated ${newWorkers.length} workers and saved config`);
|
||||
|
||||
// Reload the config to include the new workers
|
||||
await this.loadConfig();
|
||||
|
||||
// Continue rendering with the updated config
|
||||
} else {
|
||||
this.log("No additional CUDA devices available for workers (all used by master)");
|
||||
}
|
||||
}
|
||||
|
||||
// Master Node Section - styled exactly like a worker card
|
||||
const masterDiv = document.createElement("div");
|
||||
masterDiv.style.cssText = "margin-bottom: 12px; background: #2a2a2a; border-radius: 4px; overflow: hidden; display: flex;";
|
||||
|
||||
// Left column - checkbox area (always checked and disabled)
|
||||
const checkboxColumn = document.createElement("div");
|
||||
checkboxColumn.style.cssText = "flex: 0 0 40px; display: flex; align-items: center; justify-content: center; border-right: 1px solid #444; cursor: default;";
|
||||
checkboxColumn.title = "Master node is always enabled";
|
||||
|
||||
// Greyed out checkbox that's always checked
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = true;
|
||||
checkbox.disabled = true;
|
||||
checkbox.style.cssText = "cursor: default; width: 16px; height: 16px; opacity: 0.6;";
|
||||
|
||||
checkboxColumn.appendChild(checkbox);
|
||||
|
||||
// Right column - content area
|
||||
const contentColumn = document.createElement("div");
|
||||
contentColumn.style.cssText = "flex: 1; display: flex; flex-direction: column; transition: background-color 0.2s ease;";
|
||||
|
||||
// First row: master info
|
||||
const infoRow = document.createElement("div");
|
||||
infoRow.style.cssText = "display: flex; align-items: center; padding: 8px; cursor: pointer; min-height: 60px;";
|
||||
infoRow.title = "Click to expand settings";
|
||||
|
||||
// Worker content
|
||||
const workerContent = document.createElement("div");
|
||||
workerContent.style.cssText = "display: flex; align-items: center; gap: 8px; flex: 1;";
|
||||
|
||||
const statusDot = document.createElement("span");
|
||||
statusDot.id = "master-status";
|
||||
statusDot.style.cssText = "display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: #4CAF50;";
|
||||
statusDot.title = "Online";
|
||||
|
||||
const gpuInfo = document.createElement("span");
|
||||
const cudaInfo = this.masterCudaDevice !== undefined ? `CUDA ${this.masterCudaDevice} • ` : '';
|
||||
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
||||
gpuInfo.innerHTML = `<strong id="master-name-display">${this.config?.master?.name || "Master"}</strong><br><small style="color: #888;"><span id="master-cuda-info">${cudaInfo}Port ${port}</span></small>`;
|
||||
|
||||
workerContent.appendChild(statusDot);
|
||||
workerContent.appendChild(gpuInfo);
|
||||
|
||||
// Settings arrow
|
||||
const settingsArrow = document.createElement("span");
|
||||
settingsArrow.className = "settings-arrow";
|
||||
settingsArrow.innerHTML = "▶";
|
||||
settingsArrow.style.cssText = "font-size: 12px; color: #888; transition: all 0.2s ease; margin-left: auto;";
|
||||
|
||||
infoRow.appendChild(workerContent);
|
||||
infoRow.appendChild(settingsArrow);
|
||||
|
||||
// Click handler for info row
|
||||
let masterSettingsExpanded = false;
|
||||
infoRow.onclick = () => {
|
||||
masterSettingsExpanded = !masterSettingsExpanded;
|
||||
if (masterSettingsExpanded) {
|
||||
masterSettingsDiv.classList.add("expanded");
|
||||
masterSettingsDiv.style.padding = "12px";
|
||||
masterSettingsDiv.style.marginTop = "8px";
|
||||
masterSettingsDiv.style.marginBottom = "8px";
|
||||
settingsArrow.style.transform = "rotate(90deg)";
|
||||
} else {
|
||||
masterSettingsDiv.classList.remove("expanded");
|
||||
masterSettingsDiv.style.padding = "0 12px";
|
||||
masterSettingsDiv.style.marginTop = "0";
|
||||
masterSettingsDiv.style.marginBottom = "0";
|
||||
settingsArrow.style.transform = "rotate(0deg)";
|
||||
}
|
||||
};
|
||||
|
||||
// Hover effect for entire content column
|
||||
contentColumn.onmouseover = () => {
|
||||
contentColumn.style.backgroundColor = "#333";
|
||||
settingsArrow.style.color = "#fff";
|
||||
};
|
||||
contentColumn.onmouseout = () => {
|
||||
contentColumn.style.backgroundColor = "transparent";
|
||||
settingsArrow.style.color = "#888";
|
||||
};
|
||||
|
||||
contentColumn.appendChild(infoRow);
|
||||
|
||||
// Second row: master controls (styled like remote worker)
|
||||
const controlsDiv = document.createElement("div");
|
||||
controlsDiv.style.cssText = "padding: 0 8px 6px 8px; display: flex; gap: 4px;";
|
||||
|
||||
// Master info box styled like remote worker
|
||||
const masterInfo = document.createElement("div");
|
||||
masterInfo.style.cssText = "background-color: #333; color: #888; padding: 4px 12px; border-radius: 4px; font-size: 11px; text-align: center; flex: 1;";
|
||||
masterInfo.textContent = "Master";
|
||||
controlsDiv.appendChild(masterInfo);
|
||||
|
||||
// Third row: expandable settings panel
|
||||
const masterSettingsDiv = document.createElement("div");
|
||||
masterSettingsDiv.id = "master-settings";
|
||||
masterSettingsDiv.className = "worker-settings";
|
||||
masterSettingsDiv.style.cssText = "margin: 0 8px; padding: 0 12px; background: #1e1e1e; border-radius: 4px;";
|
||||
|
||||
// Create settings form using reusable components
|
||||
const settingsForm = document.createElement("div");
|
||||
settingsForm.style.cssText = "display: flex; flex-direction: column; gap: 8px;";
|
||||
|
||||
// Name field - reuse createFormGroup
|
||||
const nameGroup = this.createFormGroup("Name:", this.config?.master?.name || "Master", "master-name");
|
||||
settingsForm.appendChild(nameGroup);
|
||||
|
||||
// Host field
|
||||
const hostGroup = this.createFormGroup("Host:", this.config?.master?.host || "", "master-host", "text", "Auto-detect if empty");
|
||||
settingsForm.appendChild(hostGroup);
|
||||
|
||||
// Buttons row - using same style as workers
|
||||
const buttonGroup = document.createElement("div");
|
||||
buttonGroup.style.cssText = "display: flex; gap: 8px; margin-top: 8px;";
|
||||
|
||||
const saveBtn = this._createButton("Save",
|
||||
async () => {
|
||||
const nameInput = document.getElementById('master-name');
|
||||
const hostInput = document.getElementById('master-host');
|
||||
|
||||
// Update config
|
||||
if (!this.config.master) this.config.master = {};
|
||||
this.config.master.name = nameInput.value.trim() || "Master";
|
||||
|
||||
// Save both host and name
|
||||
await this.saveMasterConfig({
|
||||
host: hostInput.value.trim(),
|
||||
name: this.config.master.name
|
||||
});
|
||||
|
||||
// Update display
|
||||
document.getElementById('master-name-display').textContent = this.config.master.name;
|
||||
this.updateMasterDisplay();
|
||||
|
||||
saveBtn.textContent = "Saved!";
|
||||
setTimeout(() => { saveBtn.textContent = "Save"; }, 2000);
|
||||
},
|
||||
"background-color: #4a7c4a;");
|
||||
saveBtn.style.cssText += " padding: 6px 12px; font-size: 12px;";
|
||||
|
||||
const cancelBtn = this._createButton("Cancel",
|
||||
() => {
|
||||
// Reset inputs to original values
|
||||
document.getElementById('master-name').value = this.config?.master?.name || "Master";
|
||||
document.getElementById('master-host').value = this.config?.master?.host || "";
|
||||
},
|
||||
"background-color: #555;");
|
||||
cancelBtn.style.cssText += " padding: 6px 12px; font-size: 12px;";
|
||||
|
||||
buttonGroup.appendChild(saveBtn);
|
||||
buttonGroup.appendChild(cancelBtn);
|
||||
|
||||
settingsForm.appendChild(buttonGroup);
|
||||
masterSettingsDiv.appendChild(settingsForm);
|
||||
|
||||
contentColumn.appendChild(controlsDiv);
|
||||
contentColumn.appendChild(masterSettingsDiv);
|
||||
|
||||
masterDiv.appendChild(checkboxColumn);
|
||||
masterDiv.appendChild(contentColumn);
|
||||
container.appendChild(masterDiv);
|
||||
|
||||
// Workers Section (no heading)
|
||||
const gpuSection = document.createElement("div");
|
||||
gpuSection.style.cssText = "flex: 1; overflow-y: auto; margin-bottom: 15px;";
|
||||
const gpuTitle = document.createElement("h4");
|
||||
gpuTitle.textContent = "Available Workers";
|
||||
gpuTitle.style.cssText = "margin: 0 0 10px 0; font-size: 14px;";
|
||||
gpuSection.appendChild(gpuTitle);
|
||||
|
||||
const gpuList = document.createElement("div");
|
||||
const workers = this.config?.workers || [];
|
||||
@@ -687,23 +955,23 @@ class DistributedExtension {
|
||||
const settingsHeader = document.createElement("div");
|
||||
settingsHeader.style.cssText = "display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none;";
|
||||
|
||||
const settingsTitle = document.createElement("h4");
|
||||
settingsTitle.textContent = "Settings";
|
||||
settingsTitle.style.cssText = "margin: 0; font-size: 14px;";
|
||||
const workerSettingsTitle = document.createElement("h4");
|
||||
workerSettingsTitle.textContent = "Settings";
|
||||
workerSettingsTitle.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;";
|
||||
const workerSettingsToggle = document.createElement("span");
|
||||
workerSettingsToggle.textContent = "▶"; // Right arrow when collapsed
|
||||
workerSettingsToggle.style.cssText = "font-size: 12px; color: #888; transition: all 0.2s ease;";
|
||||
|
||||
settingsHeader.appendChild(settingsTitle);
|
||||
settingsHeader.appendChild(settingsToggle);
|
||||
settingsHeader.appendChild(workerSettingsTitle);
|
||||
settingsHeader.appendChild(workerSettingsToggle);
|
||||
|
||||
// Hover effect for header
|
||||
settingsHeader.onmouseover = () => {
|
||||
settingsToggle.style.color = "#fff";
|
||||
workerSettingsToggle.style.color = "#fff";
|
||||
};
|
||||
settingsHeader.onmouseout = () => {
|
||||
settingsToggle.style.color = "#888";
|
||||
workerSettingsToggle.style.color = "#888";
|
||||
};
|
||||
|
||||
// Collapsible settings content
|
||||
@@ -719,10 +987,10 @@ class DistributedExtension {
|
||||
settingsExpanded = !settingsExpanded;
|
||||
if (settingsExpanded) {
|
||||
settingsContent.style.maxHeight = "200px";
|
||||
settingsToggle.style.transform = "rotate(90deg)";
|
||||
workerSettingsToggle.style.transform = "rotate(90deg)";
|
||||
} else {
|
||||
settingsContent.style.maxHeight = "0";
|
||||
settingsToggle.style.transform = "rotate(0deg)";
|
||||
workerSettingsToggle.style.transform = "rotate(0deg)";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -798,6 +1066,29 @@ class DistributedExtension {
|
||||
container.appendChild(summarySection);
|
||||
el.appendChild(container);
|
||||
this.updateSummary();
|
||||
} finally {
|
||||
// Always reset the rendering flag
|
||||
this._isRendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateMasterDisplay() {
|
||||
// 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 (this.masterCudaDevice !== undefined && this.masterCudaDevice !== null) {
|
||||
cudaInfo.textContent = `CUDA ${this.masterCudaDevice} • Port ${port}`;
|
||||
} else {
|
||||
cudaInfo.textContent = `Port ${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update name if changed
|
||||
const nameDisplay = document.getElementById('master-name-display');
|
||||
if (nameDisplay && this.config?.master?.name) {
|
||||
nameDisplay.textContent = this.config.master.name;
|
||||
}
|
||||
}
|
||||
|
||||
updateSummary() {
|
||||
@@ -1314,6 +1605,9 @@ class DistributedExtension {
|
||||
}
|
||||
|
||||
async checkAllWorkerStatuses() {
|
||||
// Check master status
|
||||
this.checkMasterStatus();
|
||||
|
||||
if (!this.config || !this.config.workers) return;
|
||||
|
||||
for (const worker of this.config.workers) {
|
||||
@@ -1324,23 +1618,57 @@ class DistributedExtension {
|
||||
}
|
||||
}
|
||||
|
||||
async checkWorkerStatus(worker) {
|
||||
async checkMasterStatus() {
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/prompt`, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(900)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const queueRemaining = data.exec_info?.queue_remaining || 0;
|
||||
const isProcessing = queueRemaining > 0;
|
||||
|
||||
// Update master status dot
|
||||
const statusDot = document.getElementById('master-status');
|
||||
if (statusDot) {
|
||||
if (isProcessing) {
|
||||
statusDot.style.backgroundColor = "#f0ad4e";
|
||||
statusDot.title = `Processing (${queueRemaining} in queue)`;
|
||||
} else {
|
||||
statusDot.style.backgroundColor = "#4CAF50";
|
||||
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 = "#4CAF50";
|
||||
statusDot.title = "Online";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to build worker URL
|
||||
getWorkerUrl(worker, endpoint = '') {
|
||||
const host = worker.host || window.location.hostname;
|
||||
|
||||
// Detect if we should use HTTPS based on hostname or port
|
||||
const useHttps = host.includes('trycloudflare.com') ||
|
||||
host.includes('ngrok') ||
|
||||
host.includes('.ts.net') ||
|
||||
worker.port === 443;
|
||||
|
||||
// Simple rule: port 443 = HTTPS, anything else = HTTP
|
||||
const useHttps = worker.port === 443;
|
||||
const protocol = useHttps ? 'https' : 'http';
|
||||
|
||||
// Don't add port for standard HTTPS/HTTP ports
|
||||
const defaultPort = useHttps ? 443 : 80;
|
||||
const port = worker.port === defaultPort ? '' : `:${worker.port}`;
|
||||
|
||||
const url = `${protocol}://${host}${port}/prompt`;
|
||||
|
||||
return `${protocol}://${host}${port}${endpoint}`;
|
||||
}
|
||||
|
||||
async checkWorkerStatus(worker) {
|
||||
const url = this.getWorkerUrl(worker, '/prompt');
|
||||
const statusDot = document.getElementById(`status-${worker.id}`);
|
||||
|
||||
try {
|
||||
@@ -1401,21 +1729,7 @@ class DistributedExtension {
|
||||
}
|
||||
|
||||
async _dispatchToWorker(worker, prompt, workflow, imageReferences) {
|
||||
const host = worker.host || "localhost";
|
||||
|
||||
// Detect if we should use HTTPS based on hostname or port
|
||||
const useHttps = host.includes('trycloudflare.com') ||
|
||||
host.includes('ngrok') ||
|
||||
host.includes('.ts.net') ||
|
||||
worker.port === 443;
|
||||
|
||||
const protocol = useHttps ? 'https' : 'http';
|
||||
|
||||
// Don't add port for standard HTTPS/HTTP ports
|
||||
const defaultPort = useHttps ? 443 : 80;
|
||||
const port = worker.port === defaultPort ? '' : `:${worker.port}`;
|
||||
|
||||
const workerUrl = `${protocol}://${host}${port}`;
|
||||
const workerUrl = this.getWorkerUrl(worker);
|
||||
|
||||
// Debug logging - always log to console for debugging
|
||||
console.log(`[Distributed] === Dispatching to ${worker.name} (${worker.id}) ===`);
|
||||
@@ -1577,21 +1891,7 @@ class DistributedExtension {
|
||||
const startTime = Date.now();
|
||||
|
||||
const checkPromises = workers.map(async (worker) => {
|
||||
const host = worker.host || "localhost";
|
||||
|
||||
// Detect if we should use HTTPS based on hostname or port
|
||||
const useHttps = host.includes('trycloudflare.com') ||
|
||||
host.includes('ngrok') ||
|
||||
host.includes('.ts.net') ||
|
||||
worker.port === 443;
|
||||
|
||||
const protocol = useHttps ? 'https' : 'http';
|
||||
|
||||
// Don't add port for standard HTTPS/HTTP ports
|
||||
const defaultPort = useHttps ? 443 : 80;
|
||||
const port = worker.port === defaultPort ? '' : `:${worker.port}`;
|
||||
|
||||
const url = `${protocol}://${host}${port}/prompt`;
|
||||
const url = this.getWorkerUrl(worker, '/prompt');
|
||||
|
||||
this.debugLog(`Pre-flight checking ${worker.name} at: ${url}`);
|
||||
|
||||
@@ -2215,13 +2515,29 @@ class DistributedExtension {
|
||||
getMasterUrl() {
|
||||
// Always use the detected/configured master IP for consistency
|
||||
if (this.config?.master?.host) {
|
||||
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
||||
// For standard ports, don't include them in the URL
|
||||
if ((window.location.protocol === 'https:' && port === '443') ||
|
||||
(window.location.protocol === 'http:' && port === '80')) {
|
||||
return `${window.location.protocol}//${this.config.master.host}`;
|
||||
const configuredHost = this.config.master.host;
|
||||
|
||||
// If the configured host already includes protocol, use as-is
|
||||
if (configuredHost.startsWith('http://') || configuredHost.startsWith('https://')) {
|
||||
return configuredHost;
|
||||
}
|
||||
|
||||
// For domain names (not IPs), default to HTTPS
|
||||
const isIP = /^(\d{1,3}\.){3}\d{1,3}$/.test(configuredHost);
|
||||
const isLocalhost = configuredHost === 'localhost' || configuredHost === '127.0.0.1';
|
||||
|
||||
if (!isIP && !isLocalhost && configuredHost.includes('.')) {
|
||||
// It's a domain name, use HTTPS
|
||||
return `https://${configuredHost}`;
|
||||
} else {
|
||||
// For IPs and localhost, use current access method
|
||||
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
||||
if ((window.location.protocol === 'https:' && port === '443') ||
|
||||
(window.location.protocol === 'http:' && port === '80')) {
|
||||
return `${window.location.protocol}//${configuredHost}`;
|
||||
}
|
||||
return `${window.location.protocol}//${configuredHost}:${port}`;
|
||||
}
|
||||
return `${window.location.protocol}//${this.config.master.host}:${port}`;
|
||||
}
|
||||
|
||||
// If no master IP is set but we're on a network address, use it
|
||||
@@ -2245,7 +2561,49 @@ class DistributedExtension {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.debugLog("Network info: " + JSON.stringify(data));
|
||||
this.log("Network info: " + JSON.stringify(data));
|
||||
|
||||
// Store CUDA device info
|
||||
if (data.cuda_device !== null && data.cuda_device !== undefined) {
|
||||
this.masterCudaDevice = data.cuda_device;
|
||||
// Update the master display with CUDA info
|
||||
this.updateMasterDisplay();
|
||||
}
|
||||
|
||||
// Store CUDA device count for auto-population
|
||||
if (data.cuda_device_count > 0) {
|
||||
this.cudaDeviceCount = data.cuda_device_count;
|
||||
this.log(`Detected ${this.cudaDeviceCount} CUDA devices`);
|
||||
|
||||
// Auto-populate workers if none exist
|
||||
this.log(`Current workers: ${this.config.workers ? this.config.workers.length : 'null'}`);
|
||||
if (!this.config.workers || this.config.workers.length === 0) {
|
||||
this.log(`Auto-populating ${this.cudaDeviceCount} workers based on CUDA devices`);
|
||||
|
||||
const newWorkers = [];
|
||||
for (let i = 0; i < this.cudaDeviceCount; i++) {
|
||||
const worker = {
|
||||
id: Date.now() + i,
|
||||
name: `Worker ${i + 1}`,
|
||||
host: "localhost",
|
||||
port: 8189 + i,
|
||||
cuda_device: i,
|
||||
enabled: true
|
||||
};
|
||||
newWorkers.push(worker);
|
||||
}
|
||||
|
||||
// Update config and save
|
||||
this.config.workers = newWorkers;
|
||||
await this.saveConfig();
|
||||
|
||||
// Refresh the panel if it's already open
|
||||
const panel = document.getElementById("distributed-panel");
|
||||
if (panel) {
|
||||
this.renderSidebarContent(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we already have a master host configured
|
||||
if (this.config?.master?.host) {
|
||||
@@ -2258,7 +2616,7 @@ class DistributedExtension {
|
||||
this.log(`Auto-detected master IP: ${data.recommended_ip}`);
|
||||
|
||||
// Save the detected IP (pass true to suppress notification)
|
||||
await this.saveMasterIp(data.recommended_ip, true);
|
||||
await this.saveMasterConfig({ host: data.recommended_ip }, true);
|
||||
|
||||
// Show a single combined notification
|
||||
if (app.extensionManager?.toast) {
|
||||
@@ -2275,14 +2633,12 @@ class DistributedExtension {
|
||||
}
|
||||
}
|
||||
|
||||
async saveMasterIp(masterIp, suppressNotification = false) {
|
||||
async saveMasterConfig(updates, suppressNotification = false) {
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/distributed/config/update_master`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
host: masterIp.trim()
|
||||
})
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -2290,7 +2646,8 @@ class DistributedExtension {
|
||||
if (!this.config.master) {
|
||||
this.config.master = {};
|
||||
}
|
||||
this.config.master.host = masterIp.trim();
|
||||
// Update all provided fields
|
||||
Object.assign(this.config.master, updates);
|
||||
|
||||
// Only show notification if not suppressed
|
||||
if (!suppressNotification) {
|
||||
@@ -2459,6 +2816,24 @@ class DistributedExtension {
|
||||
return form;
|
||||
}
|
||||
|
||||
createSettingsToggle() {
|
||||
const settingsRow = document.createElement("div");
|
||||
settingsRow.style.cssText = "display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer; user-select: none;";
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
createFormGroup(label, value, id, type = "text", placeholder = "") {
|
||||
const group = document.createElement("div");
|
||||
group.style.cssText = "display: flex; align-items: center; gap: 8px;";
|
||||
@@ -2519,7 +2894,7 @@ class DistributedExtension {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "Validation Error",
|
||||
detail: "Port must be between 1024 and 65535",
|
||||
detail: "Port must be between 1 and 65535",
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
@@ -2697,6 +3072,11 @@ class DistributedExtension {
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility wrapper
|
||||
async saveMasterIp(masterIp, suppressNotification = false) {
|
||||
return this.saveMasterConfig({ host: masterIp.trim() }, suppressNotification);
|
||||
}
|
||||
|
||||
async addNewWorker() {
|
||||
// Generate new worker ID
|
||||
const newId = `worker_${Date.now()}`;
|
||||
|
||||
Reference in New Issue
Block a user