// Machine Manager Script
// Handles local storage machine ID functionality
import { fetchSnapshot, fetchSnapshotSimple } from "./snapshot-utils.js";
const MACHINE_STORAGE_KEY = "comfy_deploy_machine_id";
// Blacklisted URLs that should never appear in machine creation or sync
const BLACKLISTED_URLS = [
"https://github.com/Comfy-Org/ComfyUI-Manager",
"https://github.com/ltdrdata/ComfyUI-Manager",
// Add more URLs to blacklist here
];
// Helper function to check if a step should be blacklisted
function isBlacklisted(step) {
if (step.type === "custom-node" && step.data?.url) {
const normalizedStepUrl = normalizeRepoUrl(step.data.url);
return BLACKLISTED_URLS.some(
(blacklistedUrl) => normalizeRepoUrl(blacklistedUrl) === normalizedStepUrl
);
}
return false;
}
// Helper function to filter out blacklisted steps
function filterBlacklistedSteps(steps) {
if (!steps || !Array.isArray(steps)) return steps;
return steps.filter((step) => !isBlacklisted(step));
}
// Initialize machine manager
async function initializeMachineManager(element, getData) {
const machineContainer = element.querySelector("#machine-container");
const machineLoading = element.querySelector("#machine-loading");
const machineList = element.querySelector("#machine-list");
// Store getData function globally for sync functionality
window.comfyDeployGetData = getData;
const data = getData();
if (!machineContainer || !machineLoading || !machineList || !data.apiKey) {
return;
}
try {
// Check if machine ID exists in localStorage
const storedMachineId = localStorage.getItem(MACHINE_STORAGE_KEY);
if (!storedMachineId) {
showNoMachineMessage(machineLoading, machineList);
return;
}
// Show loading state
machineLoading.style.display = "flex";
machineList.style.display = "none";
// Get machine details from API
const machineData = await fetchMachineDetails(storedMachineId, getData);
if (machineData && !machineData.error) {
displayMachine(machineData, machineLoading, machineList, data.endpoint);
} else {
showMachineError(
machineLoading,
machineList,
"Failed to load machine details"
);
}
} catch (error) {
showMachineError(machineLoading, machineList, "Error loading machine");
}
}
// Show message when no machine is found
function showNoMachineMessage(machineLoading, machineList) {
machineLoading.style.display = "none";
machineList.style.display = "block";
machineList.innerHTML = `
No machine found
Add Machine
Sync Local Machine
`;
}
// Show error message
function showMachineError(machineLoading, machineList, errorMessage) {
machineLoading.style.display = "none";
machineList.style.display = "block";
machineList.innerHTML = `
⚠️
${errorMessage}
Update Machine ID
Remove Machine
`;
}
// Display machine information
function displayMachine(machineData, machineLoading, machineList, endpoint) {
machineLoading.style.display = "none";
machineList.style.display = "block";
const machine = machineData.data || machineData;
const machineName = machine.name || "Unnamed Machine";
const machineGpu = machine.gpu || "Unknown GPU";
const machineStatus = machine.status || "ready";
const machineId = machine.id;
// Determine status color and display for specific statuses
const getStatusStyle = (status) => {
const statusLower = status.toLowerCase();
if (statusLower === "ready") {
return { color: "#27ae60", text: "Ready" };
} else if (statusLower === "building") {
return { color: "#f39c12", text: "Building" };
} else if (statusLower === "error") {
return { color: "#e74c3c", text: "Error" };
} else {
return { color: "#95a5a6", text: "Unknown" };
}
};
const statusStyle = getStatusStyle(machineStatus);
machineList.innerHTML = `
`;
}
// Fetch machine details from API
async function fetchMachineDetails(machineId, getData) {
try {
const data = getData();
if (!data.apiKey) {
throw new Error("No API key available");
}
const params = new URLSearchParams({
machine_id: machineId,
api_url: data.apiUrl || "https://api.comfydeploy.com",
});
const response = await fetch(`/comfyui-deploy/machine?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching machine details:", error);
return { error: error.message };
}
}
// Remove machine from localStorage
window.removeMachine = function () {
if (confirm("Are you sure you want to unlink this machine?")) {
localStorage.removeItem(MACHINE_STORAGE_KEY);
// Show success message temporarily
const machineList = document.querySelector("#machine-list");
if (machineList) {
machineList.innerHTML = `
✓
Machine removed successfully
`;
// After 2 seconds, show the no machine message
setTimeout(() => {
const machineLoading = document.querySelector("#machine-loading");
showNoMachineMessage(machineLoading, machineList);
}, 2000);
}
}
};
// Sync machine status
window.syncMachine = async function (machineId) {
try {
// Get current machine elements
const machineList = document.querySelector("#machine-list");
const machineLoading = document.querySelector("#machine-loading");
if (!machineList || !machineLoading) {
console.error("Machine elements not found");
return;
}
// Update the sync button to show loading state
const syncButton = machineList.querySelector(
'button[onclick*="syncMachine"]'
);
let originalButtonContent = null;
if (syncButton) {
originalButtonContent = syncButton.innerHTML;
syncButton.disabled = true;
syncButton.innerHTML = `
Loading...
`;
// Add spinning animation
const style = document.createElement("style");
style.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.animate-spin {
animation: spin 1s linear infinite;
}
`;
document.head.appendChild(style);
}
// Get getData function from global scope (passed during initialization)
if (!window.comfyDeployGetData) {
console.error("getData function not available");
showMachineError(machineLoading, machineList, "Configuration error");
return;
}
const data = window.comfyDeployGetData();
// Fetch machine data and snapshot docker steps in parallel
const [machineData, snapshot] = await Promise.all([
fetchMachineDetails(machineId, window.comfyDeployGetData),
fetchSnapshot(window.comfyDeployGetData),
]);
if (!machineData || machineData.error) {
showMachineError(
machineLoading,
machineList,
"Failed to fetch machine data"
);
return;
}
// Fetch docker steps from snapshot
const dockerSteps = await fetch("/comfyui-deploy/snapshot-to-docker", {
method: "POST",
body: JSON.stringify({
api_url: data.apiUrl,
snapshot: snapshot,
}),
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
}).then((x) => x.json());
// Restore the sync button
if (syncButton && originalButtonContent) {
syncButton.disabled = false;
syncButton.innerHTML = originalButtonContent;
}
// Get endpoint for the link
const endpoint = data.endpoint || "https://app.comfydeploy.com";
// Update display with fresh data
displayMachine(machineData, machineLoading, machineList, endpoint);
// Store data globally for version comparison
window.lastMachineData = machineData;
window.lastSnapshotData = snapshot;
// Show the sync dialog with comparison
showSyncComparisonDialog(
machineData.docker_command_steps || { steps: [] },
dockerSteps || { steps: [] },
machineId
);
} catch (error) {
console.error("Error syncing machine:", error);
const machineList = document.querySelector("#machine-list");
const machineLoading = document.querySelector("#machine-loading");
// Restore the sync button if it was modified
const syncButton = machineList?.querySelector(
'button[onclick*="syncMachine"]'
);
if (syncButton) {
syncButton.disabled = false;
// Try to restore original button content or use default
syncButton.innerHTML = `
Sync Local Machine
`;
}
if (machineList && machineLoading) {
// Check if the error is related to snapshot fetch
if (error.message && error.message.includes("Snapshot fetch failed")) {
// Show toast notification to install ComfyUI Manager
if (
window.app &&
window.app.extensionManager &&
window.app.extensionManager.toast
) {
window.app.extensionManager.toast.add({
severity: "error",
summary: "ComfyUI Manager Required",
detail:
"Please install ComfyUI Manager to sync machine configurations",
life: 8000,
});
}
showMachineError(
machineLoading,
machineList,
"ComfyUI Manager is required to sync machine configurations"
);
} else {
showMachineError(machineLoading, machineList, "Error syncing machine");
}
}
}
};
// Normalize repository URL for comparison
function normalizeRepoUrl(url) {
if (!url) return null;
// Remove protocol, www, .git extension, and trailing slashes
return url
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/^www\./, "")
.replace(/\.git$/, "")
.replace(/\/$/, "");
}
// Get a unique key for comparison based on the step type
function getComparisonKey(step) {
if (step.type === "custom-node") {
// Use normalized URL as key for custom nodes
const url = step.data?.url;
return normalizeRepoUrl(url) || step.id;
} else if (step.type === "custom-node-manager") {
// Use node_id as key for custom-node-manager
return step.data?.node_id || step.id;
}
return step.id;
}
// Compare docker steps and generate diff
function compareDockerSteps(machineSteps, snapshotSteps) {
const machineMap = new Map();
const snapshotMap = new Map();
const comparisonResults = [];
// Filter out blacklisted steps before comparison
const filteredMachineSteps = filterBlacklistedSteps(machineSteps.steps || []);
const filteredSnapshotSteps = filterBlacklistedSteps(
snapshotSteps.steps || []
);
// Build maps using comparison keys
filteredMachineSteps.forEach((step) => {
const key = getComparisonKey(step);
machineMap.set(key, step);
});
filteredSnapshotSteps.forEach((step) => {
const key = getComparisonKey(step);
snapshotMap.set(key, step);
});
// Process all unique keys
const allKeys = new Set([...machineMap.keys(), ...snapshotMap.keys()]);
allKeys.forEach((key) => {
const machineStep = machineMap.get(key);
const snapshotStep = snapshotMap.get(key);
if (machineStep && snapshotStep) {
// Both exist - check if they match
let isMatch = false;
if (
machineStep.type === "custom-node" &&
snapshotStep.type === "custom-node"
) {
isMatch = machineStep.data?.hash === snapshotStep.data?.hash;
} else if (
machineStep.type === "custom-node-manager" &&
snapshotStep.type === "custom-node-manager"
) {
isMatch = machineStep.data?.version === snapshotStep.data?.version;
}
if (isMatch) {
// Identical - no action needed
comparisonResults.push({
type: "unchanged",
key: key,
step: snapshotStep,
name: snapshotStep.data?.name || snapshotStep.id,
});
} else {
// Version conflict - user needs to choose
comparisonResults.push({
type: "conflict",
key: key,
machineStep: machineStep,
snapshotStep: snapshotStep,
selectedVersion: "snapshot", // Default to snapshot version
name: snapshotStep.data?.name || machineStep.data?.name || key,
});
}
} else if (snapshotStep && !machineStep) {
// Only in snapshot - new addition
comparisonResults.push({
type: "new",
key: key,
step: snapshotStep,
selected: true, // Default selected for addition
name: snapshotStep.data?.name || snapshotStep.id,
});
} else if (machineStep && !snapshotStep) {
// Only in machine - might be removed
comparisonResults.push({
type: "removed",
key: key,
step: machineStep,
selected: false, // Default not selected for removal
name: machineStep.data?.name || machineStep.id,
});
}
});
// Sort results: conflicts first, then new, then removed, then unchanged
const typeOrder = { conflict: 0, new: 1, removed: 2, unchanged: 3 };
comparisonResults.sort((a, b) => {
const orderDiff = typeOrder[a.type] - typeOrder[b.type];
if (orderDiff !== 0) return orderDiff;
// Within same type, sort by name
return (a.name || "").localeCompare(b.name || "");
});
return comparisonResults;
}
// Show sync comparison dialog
window.showSyncComparisonDialog = function (
machineSteps,
snapshotSteps,
machineId
) {
// Compare the steps
const comparisonResults = compareDockerSteps(machineSteps, snapshotSteps);
// Get ComfyUI version comparison data from global storage
const machineData = window.lastMachineData;
const snapshotData = window.lastSnapshotData;
let comfyuiComparison = null;
if (machineData && snapshotData) {
const machineVersion = machineData.comfyui_version;
const localVersion = snapshotData.comfyui;
if (machineVersion && localVersion) {
const versionsMatch = machineVersion === localVersion;
comfyuiComparison = {
machineVersion,
localVersion,
versionsMatch,
selectedVersion: versionsMatch ? "same" : "local", // Default to local version if different
};
}
}
// Create dialog overlay
const overlay = document.createElement("div");
overlay.id = "sync-dialog-overlay";
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
// Create dialog box
const dialog = document.createElement("div");
dialog.style.cssText = `
background: #2c2c2c;
border-radius: 12px;
padding: 0;
width: 800px;
max-width: 90vw;
max-height: 85vh;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
color: #ffffff;
display: flex;
flex-direction: column;
overflow: hidden;
`;
// Count different types
const conflicts = comparisonResults.filter((r) => r.type === "conflict");
const newItems = comparisonResults.filter((r) => r.type === "new");
const removedItems = comparisonResults.filter((r) => r.type === "removed");
const unchangedItems = comparisonResults.filter(
(r) => r.type === "unchanged"
);
// Create dialog content
let dialogContent = `
Dependencies Sync
${
comfyuiComparison
? comfyuiComparison.versionsMatch
? "1 ComfyUI (same), "
: "1 ComfyUI version, "
: ""
}${conflicts.length} conflicts, ${newItems.length} new, ${
removedItems.length
} removed, ${unchangedItems.length} unchanged
×
`;
// Helper function to render compact item
const renderCompactItem = (item, index) => {
const step = item.step || item.snapshotStep || item.machineStep;
const name = item.name;
if (item.type === "conflict") {
// Version conflict - show radio buttons for selection
const machineHash = item.machineStep.data?.hash;
const snapshotHash = item.snapshotStep.data?.hash;
const machineVersion = item.machineStep.data?.version;
const snapshotVersion = item.snapshotStep.data?.version;
return `
`;
} else if (item.type === "new") {
// New item - show checkbox
return `
+
${name}
${
step.type === "custom-node" && step.data?.hash
? `
${step.data.hash.substring(
0,
8
)}
`
: step.type === "custom-node-manager" && step.data?.version
? `
v${step.data.version}
`
: ""
}
`;
} else if (item.type === "removed") {
// Removed item - show checkbox (unchecked by default)
return `
−
${name}
${
step.type === "custom-node" && step.data?.hash
? `
${step.data.hash.substring(
0,
8
)}
`
: step.type === "custom-node-manager" && step.data?.version
? `
v${step.data.version}
`
: ""
}
`;
} else {
// Unchanged - just show as info
return `
`;
}
};
// Store comfyui comparison globally
window.currentComfyUIComparison = comfyuiComparison;
// Render ComfyUI version section - always show if we have version data
if (comfyuiComparison) {
if (comfyuiComparison.versionsMatch) {
// Versions match - show green success state
dialogContent += `
ComfyUI Version
✓
ComfyUI Core - Versions Match
${comfyuiComparison.machineVersion}
`;
} else {
// Versions differ - show conflict state
dialogContent += `
ComfyUI Version
Choose which ComfyUI version to use:
`;
}
}
// Render sections
if (conflicts.length > 0) {
dialogContent += `
Version Conflicts (${conflicts.length})
Choose which version to use:
${conflicts.map((item, i) => renderCompactItem(item, i)).join("")}
`;
}
if (newItems.length > 0) {
dialogContent += `
New Dependencies (${newItems.length})
${newItems.map((item, i) => renderCompactItem(item, i)).join("")}
`;
}
if (removedItems.length > 0) {
dialogContent += `
Not in Local Snapshot (${removedItems.length})
Select to remove from machine:
${removedItems.map((item, i) => renderCompactItem(item, i)).join("")}
`;
}
if (unchangedItems.length > 0) {
dialogContent += `
Unchanged (${unchangedItems.length})
${unchangedItems.map((item, i) => renderCompactItem(item, i)).join("")}
`;
}
const hasChanges =
conflicts.length > 0 || newItems.length > 0 || removedItems.length > 0;
if (!hasChanges) {
dialogContent += `
✅
Everything is up to date!
Your machine dependencies match the current snapshot.
`;
}
dialogContent += `
${
hasChanges
? `${getSelectedCount(comparisonResults)} changes selected`
: ""
}
Cancel
${
hasChanges
? `
Apply Changes
`
: ""
}
`;
dialog.innerHTML = dialogContent;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Store comparison results globally for access by apply function
window.currentComparisonResults = comparisonResults;
// Close on overlay click
overlay.addEventListener("click", function (e) {
if (e.target === overlay) {
closeSyncDialog();
}
});
};
// Helper function to get selected count
function getSelectedCount(comparisonResults) {
let count = 0;
// Count ComfyUI version if there's a difference (not if they match)
if (
window.currentComfyUIComparison &&
!window.currentComfyUIComparison.versionsMatch
) {
count++;
}
comparisonResults.forEach((item) => {
if (item.type === "conflict") {
count++; // Conflicts always need resolution
} else if (item.type === "new" && item.selected) {
count++;
} else if (item.type === "removed" && item.selected) {
count++;
}
});
return count;
}
// Update selected count in dialog
function updateSelectedCountDisplay() {
if (!window.currentComparisonResults) return;
const selectedCount = getSelectedCount(window.currentComparisonResults);
const countElement = document.querySelector("#sync-selected-count");
if (countElement) {
countElement.textContent = `${selectedCount} changes selected`;
}
}
// Update ComfyUI version selection
window.updateComfyUIVersionSelection = function (version) {
if (window.currentComfyUIComparison) {
window.currentComfyUIComparison.selectedVersion = version;
updateSelectedCountDisplay();
}
};
// Update conflict selection
window.updateConflictSelection = function (key, version) {
if (!window.currentComparisonResults) return;
const item = window.currentComparisonResults.find(
(r) => r.key === key && r.type === "conflict"
);
if (item) {
item.selectedVersion = version;
updateSelectedCountDisplay();
}
};
// Update item selection (for new and removed items)
window.updateItemSelection = function (key, type, selected) {
if (!window.currentComparisonResults) return;
const item = window.currentComparisonResults.find(
(r) => r.key === key && r.type === type
);
if (item) {
item.selected = selected;
updateSelectedCountDisplay();
}
};
// Close sync dialog
window.closeSyncDialog = function () {
const overlay = document.getElementById("sync-dialog-overlay");
if (overlay) {
overlay.remove();
}
window.currentComparisonResults = null;
window.currentComfyUIComparison = null;
};
// Apply sync changes
window.applySyncChanges = async function (machineId) {
if (!window.currentComparisonResults) return;
// Get the apply button and show loading state
const applyButton = document.querySelector(
'button[onclick*="applySyncChanges"]'
);
let originalButtonContent = null;
if (applyButton) {
originalButtonContent = applyButton.innerHTML;
applyButton.disabled = true;
applyButton.innerHTML = `
Applying...
`;
applyButton.style.background = "#555";
applyButton.style.cursor = "not-allowed";
}
try {
const finalSteps = [];
window.currentComparisonResults.forEach((item) => {
let stepToAdd = null;
if (item.type === "conflict") {
// Use the selected version for conflicts
if (item.selectedVersion === "machine") {
stepToAdd = item.machineStep;
} else {
stepToAdd = item.snapshotStep;
}
} else if (item.type === "new" && item.selected) {
// Add selected new items
stepToAdd = item.step;
} else if (item.type === "removed" && !item.selected) {
// Keep items that are NOT selected for removal
stepToAdd = item.step;
} else if (item.type === "unchanged") {
// Always include unchanged items
stepToAdd = item.step;
}
// Only add if not blacklisted
if (stepToAdd && !isBlacklisted(stepToAdd)) {
finalSteps.push(stepToAdd);
}
});
// Format as docker steps
const dockerSteps = {
steps: finalSteps,
};
// Add ComfyUI version if there's a comparison (whether matching or not)
let selectedComfyUIVersion = null;
if (window.currentComfyUIComparison) {
if (window.currentComfyUIComparison.versionsMatch) {
// If versions match, use the common version
selectedComfyUIVersion = window.currentComfyUIComparison.machineVersion;
} else {
// If versions differ, use the selected one
if (window.currentComfyUIComparison.selectedVersion === "machine") {
selectedComfyUIVersion =
window.currentComfyUIComparison.machineVersion;
} else {
selectedComfyUIVersion = window.currentComfyUIComparison.localVersion;
}
}
dockerSteps.comfyui_version = selectedComfyUIVersion;
}
// Send these docker steps to the backend to update the machine
const body = {
machine_id: machineId,
docker_steps: dockerSteps,
api_url: window.comfyDeployGetData().apiUrl,
};
if (selectedComfyUIVersion) {
body.comfyui_version = selectedComfyUIVersion;
}
const response = await fetch(`/comfyui-deploy/machine/update`, {
method: "POST",
headers: {
Authorization: `Bearer ${window.comfyDeployGetData().apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}).then((response) => response.json());
// Close the dialog
closeSyncDialog();
if (response.ok) {
window.app.extensionManager.toast.add({
severity: "success",
summary: "Machine updated successfully",
detail: "Your machine has been updated successfully",
life: 3000,
});
} else {
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to update machine",
detail: response.error || "Please try again",
life: 5000,
});
}
} catch (error) {
console.error("Error updating machine:", error);
// Restore button if there was an error and dialog is still open
if (applyButton && originalButtonContent) {
applyButton.disabled = false;
applyButton.innerHTML = originalButtonContent;
applyButton.style.background =
"linear-gradient(135deg, #27ae60 0%, #229954 100%)";
applyButton.style.cursor = "pointer";
}
window.app.extensionManager.toast.add({
severity: "error",
summary: "Network error",
detail: "Failed to connect to server. Please try again.",
life: 5000,
});
}
};
// Show add machine dialog
window.showAddMachineDialog = function (isUpdate = false) {
const currentMachineId = localStorage.getItem(MACHINE_STORAGE_KEY);
// Create dialog overlay
const overlay = document.createElement("div");
overlay.id = "machine-dialog-overlay";
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
// Create dialog box
const dialog = document.createElement("div");
dialog.style.cssText = `
background: #2c2c2c;
border-radius: 8px;
padding: 24px;
width: 400px;
max-width: 90vw;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
color: #ffffff;
`;
dialog.innerHTML = `
${
isUpdate ? "Update Machine ID" : "Add Machine"
}
×
${
isUpdate && currentMachineId
? `
Current Machine ID:
${currentMachineId}
`
: ""
}
${
isUpdate ? "New Machine ID:" : "Machine ID:"
}
Cancel
${isUpdate ? "Update Machine" : "Add Machine"}
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Focus on input
setTimeout(() => {
const input = document.getElementById("dialog-machine-id-input");
if (input) input.focus();
}, 100);
// Close on overlay click
overlay.addEventListener("click", function (e) {
if (e.target === overlay) {
closeAddMachineDialog();
}
});
};
// Close add machine dialog
window.closeAddMachineDialog = function () {
const overlay = document.getElementById("machine-dialog-overlay");
if (overlay) {
overlay.remove();
}
};
// Validate UUID format
function isValidUUID(str) {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}
// Show validation error in dialog
function showValidationError(message) {
let errorDiv = document.getElementById("dialog-validation-error");
if (!errorDiv) {
errorDiv = document.createElement("div");
errorDiv.id = "dialog-validation-error";
errorDiv.style.cssText = `
color: #e74c3c;
font-size: 12px;
margin-top: 4px;
padding: 8px 12px;
background: rgba(231, 76, 60, 0.1);
border: 1px solid rgba(231, 76, 60, 0.3);
border-radius: 4px;
`;
const input = document.getElementById("dialog-machine-id-input");
if (input && input.parentElement) {
input.parentElement.appendChild(errorDiv);
}
}
errorDiv.textContent = message;
errorDiv.style.display = "block";
}
// Hide validation error
function hideValidationError() {
const errorDiv = document.getElementById("dialog-validation-error");
if (errorDiv) {
errorDiv.style.display = "none";
}
}
// Real-time validation as user types
window.validateMachineIdInput = function () {
const input = document.getElementById("dialog-machine-id-input");
if (!input) return;
const machineId = input.value.trim();
// Clear validation if input is empty
if (!machineId) {
hideValidationError();
updateInputBorder(input, "neutral");
return;
}
// Check UUID format
if (isValidUUID(machineId)) {
hideValidationError();
updateInputBorder(input, "valid");
} else {
// Only show error if user has typed something substantial
if (machineId.length > 8) {
showValidationError("Machine ID must be a valid UUID format");
updateInputBorder(input, "invalid");
} else {
hideValidationError();
updateInputBorder(input, "neutral");
}
}
};
// Update input border color based on validation state
function updateInputBorder(input, state) {
switch (state) {
case "valid":
input.style.borderColor = "#27ae60";
break;
case "invalid":
input.style.borderColor = "#e74c3c";
break;
default:
input.style.borderColor = "#555";
}
}
// Add machine from dialog
window.addMachineFromDialog = function () {
const input = document.getElementById("dialog-machine-id-input");
if (!input) {
console.error("Machine ID input not found");
return;
}
const machineId = input.value.trim();
if (!machineId) {
showValidationError("Please enter a machine ID");
return;
}
// Validate UUID format
if (!isValidUUID(machineId)) {
showValidationError("Machine ID must be a valid UUID format");
return;
}
// Hide any validation errors
hideValidationError();
// Close dialog
closeAddMachineDialog();
// Add the machine and refresh
addMachine(machineId);
};
// Add machine to localStorage (utility function)
window.addMachine = function (machineId) {
localStorage.setItem(MACHINE_STORAGE_KEY, machineId);
// Show loading state immediately
const machineLoading = document.querySelector("#machine-loading");
const machineList = document.querySelector("#machine-list");
if (machineLoading && machineList) {
machineLoading.style.display = "flex";
machineList.style.display = "none";
}
// Find the correct parent element that contains the machine elements
let element = document.querySelector(".comfy-menu");
// If .comfy-menu doesn't work, try to find the parent of machine-container
if (!element || !element.querySelector("#machine-container")) {
const machineContainer = document.querySelector("#machine-container");
if (machineContainer) {
element = machineContainer.parentElement;
}
}
if (element && window.comfyDeployGetData) {
// Use setTimeout to ensure DOM updates are processed
setTimeout(() => {
initializeMachineManager(element, window.comfyDeployGetData);
}, 100);
} else {
console.error("Could not find elements for machine refresh");
}
};
// Show create machine dialog
window.showCreateMachineDialog = function () {
// Create dialog overlay
const overlay = document.createElement("div");
overlay.id = "create-machine-dialog-overlay";
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
// Create dialog box
const dialog = document.createElement("div");
dialog.style.cssText = `
background: #2c2c2c;
border-radius: 12px;
padding: 0;
width: 600px;
max-width: 90vw;
max-height: 85vh;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
color: #ffffff;
display: flex;
flex-direction: column;
overflow: hidden;
`;
dialog.innerHTML = `
Create New Machine
Sync your local ComfyUI setup to the cloud
×
⏳
Loading local setup...
Analyzing your ComfyUI installation
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener("click", function (e) {
if (e.target === overlay) {
closeCreateMachineDialog();
}
});
// Load machine creation data
loadCreateMachineData();
};
// Close create machine dialog
window.closeCreateMachineDialog = function () {
const overlay = document.getElementById("create-machine-dialog-overlay");
if (overlay) {
overlay.remove();
}
window.currentCreateMachineData = null;
};
// Load data for machine creation
async function loadCreateMachineData() {
try {
// Get current snapshot and convert to docker steps
const snapshot = await fetchSnapshot(window.comfyDeployGetData);
const dockerStepsResponse = await fetch(
"/comfyui-deploy/snapshot-to-docker",
{
method: "POST",
body: JSON.stringify({
api_url: window.comfyDeployGetData().apiUrl,
snapshot: snapshot,
}),
headers: {
Authorization: `Bearer ${window.comfyDeployGetData().apiKey}`,
"Content-Type": "application/json",
},
}
).then((x) => x.json());
// Store data globally
window.currentCreateMachineData = {
snapshot,
dockerSteps: dockerStepsResponse,
selectedSteps: new Set(), // Track selected docker steps
};
// Render the machine creation form
renderCreateMachineForm(snapshot, dockerStepsResponse);
} catch (error) {
console.error("Error loading machine creation data:", error);
// Check if the error is related to snapshot fetch
if (error.message && error.message.includes("Snapshot fetch failed")) {
// Show toast notification to install ComfyUI Manager
if (
window.app &&
window.app.extensionManager &&
window.app.extensionManager.toast
) {
window.app.extensionManager.toast.add({
severity: "error",
summary: "ComfyUI Manager Required",
detail:
"Please install ComfyUI Manager to enable machine creation features",
life: 8000,
});
}
renderCreateMachineError(
"ComfyUI Manager is required. Please install ComfyUI Manager to continue."
);
} else {
renderCreateMachineError("Failed to load local setup data");
}
}
}
// Render machine creation form
function renderCreateMachineForm(snapshot, dockerSteps) {
const content = document.getElementById("create-machine-content");
if (!content) return;
const comfyuiVersion = snapshot.comfyui || "Unknown";
// Filter out blacklisted steps before rendering
const steps = filterBlacklistedSteps(dockerSteps.steps || []);
// Initialize all steps as selected by default
if (window.currentCreateMachineData) {
// Update the dockerSteps with filtered steps
window.currentCreateMachineData.dockerSteps = {
...dockerSteps,
steps: steps,
};
window.currentCreateMachineData.selectedSteps = new Set(
steps.map((_, index) => index)
);
}
content.innerHTML = `
Machine Name:
ComfyUI Version
🔧
ComfyUI Core
${comfyuiVersion}
${
steps.length > 0
? `
Custom Nodes (${steps.length})
Select All
Deselect All
${steps.map((step, index) => renderNodeStep(step, index)).join("")}
`
: `
📦
No custom nodes detected
`
}
`;
// Enable the create button
const createBtn = document.getElementById("create-machine-btn");
if (createBtn) {
createBtn.disabled = false;
createBtn.style.background =
"linear-gradient(135deg, #3498db 0%, #2980b9 100%)";
createBtn.style.color = "white";
createBtn.style.cursor = "pointer";
createBtn.style.boxShadow = "0 2px 8px rgba(52, 152, 219, 0.3)";
}
}
// Render individual node step
function renderNodeStep(step, index) {
const name = step.data?.name || step.id || "Unknown Node";
const isSelected =
window.currentCreateMachineData?.selectedSteps.has(index) || false;
return `
${name}
${
step.type === "custom-node" && step.data?.hash
? `
${step.data.hash.substring(
0,
8
)}
`
: step.type === "custom-node-manager" && step.data?.version
? `
v${step.data.version}
`
: ""
}
`;
}
// Render error state
function renderCreateMachineError(message) {
const content = document.getElementById("create-machine-content");
if (!content) return;
content.innerHTML = `
`;
}
// Toggle node selection
window.toggleNodeSelection = function (index) {
if (!window.currentCreateMachineData) return;
const selectedSteps = window.currentCreateMachineData.selectedSteps;
if (selectedSteps.has(index)) {
selectedSteps.delete(index);
} else {
selectedSteps.add(index);
}
};
// Select all nodes
window.selectAllNodes = function () {
if (!window.currentCreateMachineData) return;
const steps = window.currentCreateMachineData.dockerSteps.steps || [];
window.currentCreateMachineData.selectedSteps = new Set(
steps.map((_, index) => index)
);
// Update checkboxes
steps.forEach((_, index) => {
const checkbox = document.getElementById(`node-${index}`);
if (checkbox) checkbox.checked = true;
});
};
// Deselect all nodes
window.deselectAllNodes = function () {
if (!window.currentCreateMachineData) return;
const steps = window.currentCreateMachineData.dockerSteps.steps || [];
window.currentCreateMachineData.selectedSteps.clear();
// Update checkboxes
steps.forEach((_, index) => {
const checkbox = document.getElementById(`node-${index}`);
if (checkbox) checkbox.checked = false;
});
};
// Create machine
window.createMachine = async function () {
if (!window.currentCreateMachineData) return;
const nameInput = document.getElementById("machine-name-input");
const machineName = nameInput?.value.trim() || "Local Machine";
// Get create button and show loading state
const createBtn = document.getElementById("create-machine-btn");
let originalButtonContent = null;
if (createBtn) {
originalButtonContent = createBtn.innerHTML;
createBtn.disabled = true;
createBtn.innerHTML = `
Creating...
`;
createBtn.style.background = "#555";
createBtn.style.cursor = "not-allowed";
}
try {
// Get selected docker steps
const allSteps = window.currentCreateMachineData.dockerSteps.steps || [];
const selectedSteps = Array.from(
window.currentCreateMachineData.selectedSteps
)
.map((index) => allSteps[index])
.filter((step) => step);
const dockerCommandSteps = {
steps: selectedSteps,
};
const data = window.comfyDeployGetData();
// Create machine via API
const response = await fetch("/comfyui-deploy/machine/create", {
method: "POST",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: machineName,
docker_command_steps: dockerCommandSteps,
comfyui_version: window.currentCreateMachineData.snapshot.comfyui,
api_url: data.apiUrl || "https://api.comfydeploy.com",
}),
}).then((response) => response.json());
// Close dialog
closeCreateMachineDialog();
if (response.id) {
// Save machine ID to localStorage
localStorage.setItem(MACHINE_STORAGE_KEY, response.id);
// Show success message
window.app.extensionManager.toast.add({
severity: "success",
summary: "Machine created successfully",
detail: `Machine "${machineName}" has been created and linked`,
life: 3000,
});
// Refresh machine manager
const machineLoading = document.querySelector("#machine-loading");
const machineList = document.querySelector("#machine-list");
if (machineLoading && machineList) {
machineLoading.style.display = "flex";
machineList.style.display = "none";
}
// Find the correct parent element and reinitialize
let element = document.querySelector(".comfy-menu");
if (!element || !element.querySelector("#machine-container")) {
const machineContainer = document.querySelector("#machine-container");
if (machineContainer) {
element = machineContainer.parentElement;
}
}
if (element && window.comfyDeployGetData) {
setTimeout(() => {
initializeMachineManager(element, window.comfyDeployGetData);
}, 100);
}
} else {
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to create machine",
detail: result.error || "Please try again",
life: 5000,
});
}
} catch (error) {
console.error("Error creating machine:", error);
// Restore button if there was an error and dialog is still open
if (createBtn && originalButtonContent) {
createBtn.disabled = false;
createBtn.innerHTML = originalButtonContent;
createBtn.style.background =
"linear-gradient(135deg, #3498db 0%, #2980b9 100%)";
createBtn.style.cursor = "pointer";
}
window.app.extensionManager.toast.add({
severity: "error",
summary: "Network error",
detail: "Failed to connect to server. Please try again.",
life: 5000,
});
}
};
// Export the main function
export { initializeMachineManager };