2372 lines
73 KiB
JavaScript
2372 lines
73 KiB
JavaScript
// 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 = `
|
||
<li style="text-align: center; padding: 16px; color: #666; font-size: 13px;">
|
||
<div style="margin-bottom: 8px;">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2"/>
|
||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2"/>
|
||
<line x1="6" x2="6.01" y1="6" y2="6"/>
|
||
<line x1="6" x2="6.01" y1="18" y2="18"/>
|
||
</svg>
|
||
</div>
|
||
<div style="font-weight: 500; margin-bottom: 12px;">No machine found</div>
|
||
<div style="display: flex; gap: 8px; justify-content: center;">
|
||
<button
|
||
onclick="showAddMachineDialog()"
|
||
style="
|
||
background: #27ae60;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
"
|
||
title="Add existing machine"
|
||
>
|
||
Add Machine
|
||
</button>
|
||
<button
|
||
onclick="showCreateMachineDialog()"
|
||
style="
|
||
background: #3498db;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
"
|
||
title="Create new machine from local setup"
|
||
>
|
||
Sync Local Machine
|
||
</button>
|
||
</div>
|
||
</li>
|
||
`;
|
||
}
|
||
|
||
// Show error message
|
||
function showMachineError(machineLoading, machineList, errorMessage) {
|
||
machineLoading.style.display = "none";
|
||
machineList.style.display = "block";
|
||
machineList.innerHTML = `
|
||
<li style="text-align: center; padding: 16px; color: #e74c3c; font-size: 13px;">
|
||
<div style="margin-bottom: 8px;">⚠️</div>
|
||
<div style="margin-bottom: 12px;">${errorMessage}</div>
|
||
<div style="display: flex; gap: 8px; justify-content: center;">
|
||
<button
|
||
onclick="showAddMachineDialog(true)"
|
||
style="
|
||
background: #f39c12;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
"
|
||
title="Update machine ID"
|
||
>
|
||
Update Machine ID
|
||
</button>
|
||
<button
|
||
onclick="removeMachine()"
|
||
style="
|
||
background: #e74c3c;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
"
|
||
title="Remove machine"
|
||
>
|
||
Remove Machine
|
||
</button>
|
||
</div>
|
||
</li>
|
||
`;
|
||
}
|
||
|
||
// 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 = `
|
||
<li style="
|
||
border: 1px solid #404040;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%);
|
||
position: relative;
|
||
color: #ffffff;
|
||
">
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 600; font-size: 14px; margin-bottom: 8px;">
|
||
<a href="${endpoint}/machines/${machineId}" target="_blank" style="color: #ffffff; text-decoration: none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">
|
||
${machineName}
|
||
</a>
|
||
</div>
|
||
<div style="display: flex; align-items: center; margin-bottom: 4px;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">
|
||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2"/>
|
||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2"/>
|
||
<line x1="6" x2="6.01" y1="6" y2="6"/>
|
||
<line x1="6" x2="6.01" y1="18" y2="18"/>
|
||
</svg>
|
||
<span style="font-size: 12px; color: #cccccc;">${machineGpu}</span>
|
||
</div>
|
||
<div style="display: flex; align-items: center;">
|
||
<div style="
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: ${statusStyle.color};
|
||
margin-right: 6px;
|
||
"></div>
|
||
<span style="font-size: 12px; color: ${statusStyle.color}; font-weight: 500;">
|
||
${statusStyle.text}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button
|
||
onclick="syncMachine('${machineId}')"
|
||
style="
|
||
background: #f39c12;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 6px 8px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
"
|
||
title="Sync machine status"
|
||
>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/>
|
||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||
<path d="M16 16h5v5"/>
|
||
</svg>
|
||
Sync
|
||
</button>
|
||
<button
|
||
onclick="removeMachine('${machineId}')"
|
||
style="
|
||
background: #e74c3c;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 6px 8px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
"
|
||
title="Remove machine from local storage"
|
||
>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="3,6 5,6 21,6"></polyline>
|
||
<path d="M19,6l-2,14c0,1.1-0.9,2-2,2H9c-1.1,0-2-0.9-2-2L5,6"></path>
|
||
<path d="M10,11v6"></path>
|
||
<path d="M14,11v6"></path>
|
||
</svg>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
`;
|
||
}
|
||
|
||
// 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 = `
|
||
<li style="text-align: center; padding: 20px; color: #27ae60; font-size: 14px;">
|
||
<div style="margin-bottom: 10px;">✓</div>
|
||
<div>Machine removed successfully</div>
|
||
</li>
|
||
`;
|
||
|
||
// 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 = `
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin">
|
||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/>
|
||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||
<path d="M16 16h5v5"/>
|
||
</svg>
|
||
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 = `
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/>
|
||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||
<path d="M16 16h5v5"/>
|
||
</svg>
|
||
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 = `
|
||
<div style="
|
||
padding: 18px 24px;
|
||
border-bottom: 1px solid #404040;
|
||
background: linear-gradient(135deg, #2c2c2c 0%, #1f1f1f 100%);
|
||
">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<h3 style="margin: 0; font-size: 18px; font-weight: 600;">Dependencies Sync</h3>
|
||
<p style="margin: 4px 0 0 0; font-size: 12px; color: #999;">
|
||
${
|
||
comfyuiComparison
|
||
? comfyuiComparison.versionsMatch
|
||
? "1 ComfyUI (same), "
|
||
: "1 ComfyUI version, "
|
||
: ""
|
||
}${conflicts.length} conflicts, ${newItems.length} new, ${
|
||
removedItems.length
|
||
} removed, ${unchangedItems.length} unchanged
|
||
</p>
|
||
</div>
|
||
<button onclick="closeSyncDialog()" style="
|
||
background: none;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.background='#404040'" onmouseout="this.style.background='none'">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16px 20px;
|
||
background: #1a1a1a;
|
||
">
|
||
`;
|
||
|
||
// 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 `
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: #252525;
|
||
border-radius: 6px;
|
||
margin-bottom: 6px;
|
||
border: 1px solid #444;
|
||
">
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
background: #f39c1220;
|
||
color: #f39c12;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">⚠</div>
|
||
|
||
<div style="flex: 1; min-width: 0;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #fff; margin-bottom: 6px;">
|
||
${name}
|
||
</div>
|
||
<div style="display: flex; gap: 12px; font-size: 11px;">
|
||
<label style="
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.borderColor='#555'" onmouseout="this.style.borderColor='#333'">
|
||
<input
|
||
type="radio"
|
||
name="conflict-${item.key}"
|
||
value="machine"
|
||
${item.selectedVersion === "machine" ? "checked" : ""}
|
||
onchange="updateConflictSelection('${item.key}', 'machine')"
|
||
style="margin-right: 6px; cursor: pointer;"
|
||
/>
|
||
<span style="color: #e74c3c;">Machine:</span>
|
||
<span style="margin-left: 4px; font-family: monospace; color: #999;">
|
||
${
|
||
machineHash
|
||
? machineHash.substring(0, 8)
|
||
: machineVersion || "unknown"
|
||
}
|
||
</span>
|
||
</label>
|
||
|
||
<label style="
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.borderColor='#555'" onmouseout="this.style.borderColor='#333'">
|
||
<input
|
||
type="radio"
|
||
name="conflict-${item.key}"
|
||
value="snapshot"
|
||
${item.selectedVersion === "snapshot" ? "checked" : ""}
|
||
onchange="updateConflictSelection('${item.key}', 'snapshot')"
|
||
style="margin-right: 6px; cursor: pointer;"
|
||
/>
|
||
<span style="color: #27ae60;">Local:</span>
|
||
<span style="margin-left: 4px; font-family: monospace; color: #999;">
|
||
${
|
||
snapshotHash
|
||
? snapshotHash.substring(0, 8)
|
||
: snapshotVersion || "unknown"
|
||
}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (item.type === "new") {
|
||
// New item - show checkbox
|
||
return `
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: #252525;
|
||
border-radius: 6px;
|
||
margin-bottom: 6px;
|
||
border: 1px solid #333;
|
||
">
|
||
<input
|
||
type="checkbox"
|
||
id="new-${item.key}"
|
||
${item.selected ? "checked" : ""}
|
||
onchange="updateItemSelection('${item.key}', 'new', this.checked)"
|
||
style="margin-right: 10px; cursor: pointer; width: 16px; height: 16px;"
|
||
/>
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
background: #27ae6020;
|
||
color: #27ae60;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">+</div>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #fff;">
|
||
${name}
|
||
</div>
|
||
${
|
||
step.type === "custom-node" && step.data?.hash
|
||
? `<div style="font-size: 11px; color: #666; font-family: monospace;">${step.data.hash.substring(
|
||
0,
|
||
8
|
||
)}</div>`
|
||
: step.type === "custom-node-manager" && step.data?.version
|
||
? `<div style="font-size: 11px; color: #666;">v${step.data.version}</div>`
|
||
: ""
|
||
}
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (item.type === "removed") {
|
||
// Removed item - show checkbox (unchecked by default)
|
||
return `
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: #252525;
|
||
border-radius: 6px;
|
||
margin-bottom: 6px;
|
||
border: 1px solid #333;
|
||
opacity: ${item.selected ? "1" : "0.6"};
|
||
">
|
||
<input
|
||
type="checkbox"
|
||
id="remove-${item.key}"
|
||
${item.selected ? "checked" : ""}
|
||
onchange="updateItemSelection('${
|
||
item.key
|
||
}', 'removed', this.checked)"
|
||
style="margin-right: 10px; cursor: pointer; width: 16px; height: 16px;"
|
||
/>
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
background: #e74c3c20;
|
||
color: #e74c3c;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">−</div>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #fff;">
|
||
${name}
|
||
</div>
|
||
${
|
||
step.type === "custom-node" && step.data?.hash
|
||
? `<div style="font-size: 11px; color: #666; font-family: monospace;">${step.data.hash.substring(
|
||
0,
|
||
8
|
||
)}</div>`
|
||
: step.type === "custom-node-manager" && step.data?.version
|
||
? `<div style="font-size: 11px; color: #666;">v${step.data.version}</div>`
|
||
: ""
|
||
}
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Unchanged - just show as info
|
||
return `
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
background: #1f1f1f;
|
||
border-radius: 6px;
|
||
margin-bottom: 4px;
|
||
border: 1px solid #2a2a2a;
|
||
opacity: 0.5;
|
||
">
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
color: #555;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">✓</div>
|
||
<div style="flex: 1;">
|
||
<div style="font-size: 12px; color: #888;">
|
||
${name}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
};
|
||
|
||
// 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 += `
|
||
<div style="margin-bottom: 16px;">
|
||
<h4 style="
|
||
margin: 0 0 8px 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #27ae60;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">ComfyUI Version</h4>
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: #1f2f1f;
|
||
border-radius: 6px;
|
||
border: 1px solid #27ae60;
|
||
opacity: 0.8;
|
||
">
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
background: #27ae60;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">✓</div>
|
||
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #27ae60; margin-bottom: 2px;">
|
||
ComfyUI Core - Versions Match
|
||
</div>
|
||
<div style="font-size: 11px; color: #666; font-family: monospace;">
|
||
${comfyuiComparison.machineVersion}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Versions differ - show conflict state
|
||
dialogContent += `
|
||
<div style="margin-bottom: 20px;">
|
||
<h4 style="
|
||
margin: 0 0 8px 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #9b59b6;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">ComfyUI Version</h4>
|
||
<div style="font-size: 11px; color: #666; margin-bottom: 8px;">Choose which ComfyUI version to use:</div>
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: #252525;
|
||
border-radius: 6px;
|
||
border: 1px solid #444;
|
||
">
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
background: #9b59b620;
|
||
color: #9b59b6;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">🔧</div>
|
||
|
||
<div style="flex: 1; min-width: 0;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #fff; margin-bottom: 6px;">
|
||
ComfyUI Core
|
||
</div>
|
||
<div style="display: flex; gap: 12px; font-size: 11px;">
|
||
<label style="
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.borderColor='#555'" onmouseout="this.style.borderColor='#333'">
|
||
<input
|
||
type="radio"
|
||
name="comfyui-version"
|
||
value="machine"
|
||
${
|
||
comfyuiComparison.selectedVersion === "machine"
|
||
? "checked"
|
||
: ""
|
||
}
|
||
onchange="updateComfyUIVersionSelection('machine')"
|
||
style="margin-right: 6px; cursor: pointer;"
|
||
/>
|
||
<span style="color: #e74c3c;">Machine:</span>
|
||
<span style="margin-left: 4px; font-family: monospace; color: #999;">
|
||
${comfyuiComparison.machineVersion}
|
||
</span>
|
||
</label>
|
||
|
||
<label style="
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.borderColor='#555'" onmouseout="this.style.borderColor='#333'">
|
||
<input
|
||
type="radio"
|
||
name="comfyui-version"
|
||
value="local"
|
||
${
|
||
comfyuiComparison.selectedVersion === "local"
|
||
? "checked"
|
||
: ""
|
||
}
|
||
onchange="updateComfyUIVersionSelection('local')"
|
||
style="margin-right: 6px; cursor: pointer;"
|
||
/>
|
||
<span style="color: #27ae60;">Local:</span>
|
||
<span style="margin-left: 4px; font-family: monospace; color: #999;">
|
||
${comfyuiComparison.localVersion}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Render sections
|
||
if (conflicts.length > 0) {
|
||
dialogContent += `
|
||
<div style="margin-bottom: 16px;">
|
||
<h4 style="
|
||
margin: 0 0 8px 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #f39c12;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">Version Conflicts (${conflicts.length})</h4>
|
||
<div style="font-size: 11px; color: #666; margin-bottom: 8px;">Choose which version to use:</div>
|
||
${conflicts.map((item, i) => renderCompactItem(item, i)).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (newItems.length > 0) {
|
||
dialogContent += `
|
||
<div style="margin-bottom: 16px;">
|
||
<h4 style="
|
||
margin: 0 0 8px 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #27ae60;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">New Dependencies (${newItems.length})</h4>
|
||
${newItems.map((item, i) => renderCompactItem(item, i)).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (removedItems.length > 0) {
|
||
dialogContent += `
|
||
<div style="margin-bottom: 16px;">
|
||
<h4 style="
|
||
margin: 0 0 8px 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #e74c3c;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">Not in Local Snapshot (${removedItems.length})</h4>
|
||
<div style="font-size: 11px; color: #666; margin-bottom: 8px;">Select to remove from machine:</div>
|
||
${removedItems.map((item, i) => renderCompactItem(item, i)).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (unchangedItems.length > 0) {
|
||
dialogContent += `
|
||
<details style="margin-bottom: 16px;">
|
||
<summary style="
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #666;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 8px;
|
||
">Unchanged (${unchangedItems.length})</summary>
|
||
${unchangedItems.map((item, i) => renderCompactItem(item, i)).join("")}
|
||
</details>
|
||
`;
|
||
}
|
||
|
||
const hasChanges =
|
||
conflicts.length > 0 || newItems.length > 0 || removedItems.length > 0;
|
||
|
||
if (!hasChanges) {
|
||
dialogContent += `
|
||
<div style="
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #666;
|
||
">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">✅</div>
|
||
<div style="font-size: 16px; font-weight: 500;">Everything is up to date!</div>
|
||
<div style="font-size: 13px; margin-top: 8px;">Your machine dependencies match the current snapshot.</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
dialogContent += `
|
||
</div>
|
||
|
||
<div style="
|
||
padding: 16px 20px;
|
||
border-top: 1px solid #404040;
|
||
background: linear-gradient(135deg, #2c2c2c 0%, #1f1f1f 100%);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
">
|
||
<div id="sync-selected-count" style="font-size: 12px; color: #999;">
|
||
${
|
||
hasChanges
|
||
? `${getSelectedCount(comparisonResults)} changes selected`
|
||
: ""
|
||
}
|
||
</div>
|
||
<div style="display: flex; gap: 12px;">
|
||
<button onclick="closeSyncDialog()" style="
|
||
background: #404040;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 8px 18px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.background='#555'" onmouseout="this.style.background='#404040'">
|
||
Cancel
|
||
</button>
|
||
${
|
||
hasChanges
|
||
? `
|
||
<button onclick="applySyncChanges('${machineId}')" style="
|
||
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 8px 20px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
||
" onmouseover="this.style.transform='translateY(-1px)'" onmouseout="this.style.transform='translateY(0)'">
|
||
Apply Changes
|
||
</button>
|
||
`
|
||
: ""
|
||
}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin" style="margin-right: 6px;">
|
||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/>
|
||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||
<path d="M16 16h5v5"/>
|
||
</svg>
|
||
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 = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||
<h3 style="margin: 0; font-size: 16px; font-weight: 600;">${
|
||
isUpdate ? "Update Machine ID" : "Add Machine"
|
||
}</h3>
|
||
<button onclick="closeAddMachineDialog()" style="
|
||
background: none;
|
||
border: none;
|
||
color: #ccc;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
">×</button>
|
||
</div>
|
||
|
||
${
|
||
isUpdate && currentMachineId
|
||
? `
|
||
<div style="margin-bottom: 12px; padding: 8px 12px; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #f39c12;">
|
||
<div style="font-size: 12px; color: #ccc; margin-bottom: 4px;">Current Machine ID:</div>
|
||
<div style="font-size: 13px; color: #ffffff; font-family: monospace;">${currentMachineId}</div>
|
||
</div>
|
||
`
|
||
: ""
|
||
}
|
||
|
||
<div style="margin-bottom: 16px;">
|
||
<label style="display: block; margin-bottom: 8px; font-size: 13px; color: #ccc;">${
|
||
isUpdate ? "New Machine ID:" : "Machine ID:"
|
||
}</label>
|
||
<input
|
||
type="text"
|
||
id="dialog-machine-id-input"
|
||
placeholder="${
|
||
isUpdate ? "Enter new machine ID" : "Enter your machine ID"
|
||
}"
|
||
onkeypress="if(event.key === 'Enter') addMachineFromDialog()"
|
||
oninput="validateMachineIdInput()"
|
||
style="
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
background: #1a1a1a;
|
||
color: #ffffff;
|
||
box-sizing: border-box;
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
||
<button onclick="closeAddMachineDialog()" style="
|
||
background: #555;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
">Cancel</button>
|
||
<button onclick="addMachineFromDialog()" style="
|
||
background: ${isUpdate ? "#f39c12" : "#27ae60"};
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
">${isUpdate ? "Update Machine" : "Add Machine"}</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div style="
|
||
padding: 18px 24px;
|
||
border-bottom: 1px solid #404040;
|
||
background: linear-gradient(135deg, #2c2c2c 0%, #1f1f1f 100%);
|
||
">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<h3 style="margin: 0; font-size: 18px; font-weight: 600;">Create New Machine</h3>
|
||
<p style="margin: 4px 0 0 0; font-size: 12px; color: #999;">
|
||
Sync your local ComfyUI setup to the cloud
|
||
</p>
|
||
</div>
|
||
<button onclick="closeCreateMachineDialog()" style="
|
||
background: none;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.background='#404040'" onmouseout="this.style.background='none'">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="create-machine-content" style="
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16px 20px;
|
||
background: #1a1a1a;
|
||
">
|
||
<div style="text-align: center; padding: 40px; color: #666;">
|
||
<div style="font-size: 24px; margin-bottom: 16px;">⏳</div>
|
||
<div style="font-size: 16px; font-weight: 500;">Loading local setup...</div>
|
||
<div style="font-size: 13px; margin-top: 8px;">Analyzing your ComfyUI installation</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="
|
||
padding: 16px 20px;
|
||
border-top: 1px solid #404040;
|
||
background: linear-gradient(135deg, #2c2c2c 0%, #1f1f1f 100%);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
">
|
||
<div style="font-size: 12px; color: #999;"></div>
|
||
<div style="display: flex; gap: 12px;">
|
||
<button onclick="closeCreateMachineDialog()" style="
|
||
background: #404040;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 8px 18px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.background='#555'" onmouseout="this.style.background='#404040'">
|
||
Cancel
|
||
</button>
|
||
<button id="create-machine-btn" onclick="createMachine()" disabled style="
|
||
background: #555;
|
||
color: #999;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 8px 20px;
|
||
font-size: 13px;
|
||
cursor: not-allowed;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
">
|
||
Create Machine
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 8px; font-size: 13px; color: #ccc;">Machine Name:</label>
|
||
<input
|
||
type="text"
|
||
id="machine-name-input"
|
||
placeholder="Local Machine"
|
||
value="Local Machine"
|
||
style="
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
background: #1a1a1a;
|
||
color: #ffffff;
|
||
box-sizing: border-box;
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<h4 style="
|
||
margin: 0 0 8px 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #3498db;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">ComfyUI Version</h4>
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: #1f2f2f;
|
||
border-radius: 6px;
|
||
border: 1px solid #3498db;
|
||
opacity: 0.8;
|
||
">
|
||
<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
background: #3498db;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
">🔧</div>
|
||
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #3498db; margin-bottom: 2px;">
|
||
ComfyUI Core
|
||
</div>
|
||
<div style="font-size: 11px; color: #666; font-family: monospace;">
|
||
${comfyuiVersion}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${
|
||
steps.length > 0
|
||
? `
|
||
<div style="margin-bottom: 20px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<h4 style="
|
||
margin: 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #27ae60;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
">Custom Nodes (${steps.length})</h4>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button onclick="selectAllNodes()" style="
|
||
background: #27ae60;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
">Select All</button>
|
||
<button onclick="deselectAllNodes()" style="
|
||
background: #e74c3c;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
">Deselect All</button>
|
||
</div>
|
||
</div>
|
||
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #333; border-radius: 6px; background: #252525;">
|
||
${steps.map((step, index) => renderNodeStep(step, index)).join("")}
|
||
</div>
|
||
</div>
|
||
`
|
||
: `
|
||
<div style="
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #666;
|
||
background: #252525;
|
||
border-radius: 6px;
|
||
border: 1px solid #333;
|
||
margin-bottom: 20px;
|
||
">
|
||
<div style="font-size: 16px; margin-bottom: 8px;">📦</div>
|
||
<div style="font-size: 13px;">No custom nodes detected</div>
|
||
</div>
|
||
`
|
||
}
|
||
`;
|
||
|
||
// 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 `
|
||
<div style="
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid #333;
|
||
">
|
||
<input
|
||
type="checkbox"
|
||
id="node-${index}"
|
||
${isSelected ? "checked" : ""}
|
||
onchange="toggleNodeSelection(${index})"
|
||
style="margin-right: 10px; cursor: pointer; width: 16px; height: 16px;"
|
||
/>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; font-size: 13px; color: #fff;">
|
||
${name}
|
||
</div>
|
||
${
|
||
step.type === "custom-node" && step.data?.hash
|
||
? `<div style="font-size: 11px; color: #666; font-family: monospace;">${step.data.hash.substring(
|
||
0,
|
||
8
|
||
)}</div>`
|
||
: step.type === "custom-node-manager" && step.data?.version
|
||
? `<div style="font-size: 11px; color: #666;">v${step.data.version}</div>`
|
||
: ""
|
||
}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Render error state
|
||
function renderCreateMachineError(message) {
|
||
const content = document.getElementById("create-machine-content");
|
||
if (!content) return;
|
||
|
||
content.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: #e74c3c;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
|
||
<div style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">Error</div>
|
||
<div style="font-size: 13px;">${message}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 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 = `
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin" style="margin-right: 6px;">
|
||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/>
|
||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||
<path d="M16 16h5v5"/>
|
||
</svg>
|
||
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 };
|