// VM Placement Optimizer - Frontend Application with Real-Time Animations let autoRefreshIntervalId = null; let demoDataId = null; let placementId = null; let loadedPlacement = null; // Animation state tracking let vmPositionCache = {}; // { vmId: serverId | null } let isFirstRender = true; let previousScore = null; let serverLookup = {}; // For quick server access during animations $(document).ready(function () { let initialized = false; function safeInitialize() { if (!initialized) { initialized = true; initializeApp(); } } $(window).on('load', safeInitialize); setTimeout(safeInitialize, 100); }); function initializeApp() { replaceQuickstartSolverForgeAutoHeaderFooter(); $("#solveButton").click(function () { solve(); }); $("#stopSolvingButton").click(function () { stopSolving(); }); $("#analyzeButton").click(function () { analyze(); }); // View toggle $('input[name="viewToggle"]').change(function() { if ($('#rackView').is(':checked')) { $('#rackViewContainer').show(); $('#cardViewContainer').hide(); } else { $('#rackViewContainer').hide(); $('#cardViewContainer').show(); } }); setupAjax(); fetchDemoData(); } function setupAjax() { $.ajaxSetup({ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json,text/plain', } }); jQuery.each(["put", "delete"], function (i, method) { jQuery[method] = function (url, data, callback, type) { if (jQuery.isFunction(data)) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ url: url, type: method, dataType: type, data: data, success: callback }); }; }); } function fetchDemoData() { $.get("/demo-data", function (data) { data.forEach(item => { $("#testDataButton").append($('' + item + '')); $("#" + item + "TestData").click(function () { switchDataDropDownItemActive(item); placementId = null; demoDataId = item; // Reset animation state for new dataset isFirstRender = true; vmPositionCache = {}; previousScore = null; refreshPlacement(); }); }); if (data.length > 0) { demoDataId = data[0]; switchDataDropDownItemActive(demoDataId); refreshPlacement(); } }).fail(function (xhr, ajaxOptions, thrownError) { let $demo = $("#demo"); $demo.empty(); $demo.html("

No test data available

"); }); } function switchDataDropDownItemActive(newItem) { const activeCssClass = "active"; $("#testDataButton > a." + activeCssClass).removeClass(activeCssClass); $("#" + newItem + "TestData").addClass(activeCssClass); } function refreshPlacement() { let path = "/placements/" + placementId; if (placementId === null) { if (demoDataId === null) { showSimpleError("Please select a test data set."); return; } path = "/demo-data/" + demoDataId; } $.getJSON(path, function (placement) { loadedPlacement = placement; renderPlacement(placement); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Getting the placement has failed.", xhr); refreshSolvingButtons(false); }); } function renderPlacement(placement) { if (!placement) { console.error('No placement data provided'); return; } const isSolving = placement.solverStatus != null && placement.solverStatus !== "NOT_SOLVING"; refreshSolvingButtons(isSolving); // Update score with animation const newScore = placement.score == null ? "?" : placement.score; const scoreEl = $("#score"); if (previousScore !== newScore) { scoreEl.text("Score: " + newScore); if (previousScore !== null && newScore !== "?") { scoreEl.addClass("updated"); setTimeout(() => scoreEl.removeClass("updated"), 300); } previousScore = newScore; } // Update summary cards with change detection updateSummaryCard("#totalServers", placement.servers ? placement.servers.length : 0); updateSummaryCard("#activeServers", placement.activeServers || 0); updateSummaryCard("#totalVms", placement.vms ? placement.vms.length : 0); updateSummaryCard("#unassignedVms", placement.unassignedVms || 0); updateSummaryCard("#cpuUtil", Math.round((placement.totalCpuUtilization || 0) * 100) + "%"); updateSummaryCard("#memUtil", Math.round((placement.totalMemoryUtilization || 0) * 100) + "%"); // Update unassigned card styling const unassignedCard = $("#unassignedCard"); const unassignedVms = placement.unassignedVms || 0; unassignedCard.removeClass("warning danger"); if (unassignedVms > 0) { unassignedCard.addClass(unassignedVms > 5 ? "danger" : "warning"); } if (!placement.servers || !placement.vms) { return; } // Create VM and server lookups const vmById = {}; placement.vms.forEach(vm => vmById[vm.id] = vm); serverLookup = {}; placement.servers.forEach(s => serverLookup[s.id] = s); if (isFirstRender) { // Full render on first load renderRackView(placement, vmById); renderCardView(placement, vmById); renderUnassignedVMs(placement.vms); vmPositionCache = buildPositionCache(placement); isFirstRender = false; return; } // Incremental animated update const newPositions = buildPositionCache(placement); const changes = diffPositions(vmPositionCache, newPositions); if (changes.length > 0) { console.log(`Detected ${changes.length} VM position changes:`, changes.slice(0, 5)); animateVmChanges(changes, placement, vmById); } // Always update utilization bars smoothly updateUtilizationBars(placement); // Update unassigned list updateUnassignedList(placement.vms, vmById); vmPositionCache = newPositions; } function updateSummaryCard(selector, newValue) { const el = $(selector); const oldValue = el.text(); if (oldValue !== String(newValue)) { el.text(newValue); el.addClass("changed"); setTimeout(() => el.removeClass("changed"), 300); } } function buildPositionCache(placement) { const cache = {}; placement.vms.forEach(vm => { cache[vm.id] = vm.server || null; }); // Debug: log sample of cache const sample = Object.entries(cache).slice(0, 3); console.log('Position cache sample:', sample); return cache; } function diffPositions(oldCache, newCache) { const changes = []; for (const vmId in newCache) { const oldServer = oldCache[vmId]; const newServer = newCache[vmId]; if (oldServer !== newServer) { changes.push({ vmId, from: oldServer, to: newServer }); } } return changes; } function animateVmChanges(changes, placement, vmById) { changes.forEach(({ vmId, from, to }) => { const vm = vmById[vmId]; if (!vm) return; // Get source and target elements let sourceEl, targetEl; let sourceChipEl = null; // The actual chip element to hide during animation if (from) { sourceEl = $(`#server-blade-${from} .vm-chips`); sourceChipEl = $(`#server-blade-${from} #vm-chip-${vmId}`); // Highlight sending server $(`#server-blade-${from}`).addClass('sending'); setTimeout(() => $(`#server-blade-${from}`).removeClass('sending'), 200); } else { // VM is coming from unassigned list sourceEl = $(`#unassigned-${vmId}`); sourceChipEl = sourceEl; } if (to) { targetEl = $(`#server-blade-${to} .vm-chips`); // Highlight receiving server $(`#server-blade-${to}`).addClass('receiving'); setTimeout(() => $(`#server-blade-${to}`).removeClass('receiving'), 300); } else { targetEl = $('#unassignedList'); } // If elements not found, just update DOM directly if (!sourceEl.length || !targetEl.length) { console.log(`Animation fallback: sourceEl=${sourceEl.length}, targetEl=${targetEl.length} for VM ${vmId}`); updateVmPosition(vmId, from, to, vm); return; } // Create flying clone const flyingChip = createVmChip(vm); flyingChip.addClass('flying-vm'); // Get positions const sourceRect = sourceEl[0].getBoundingClientRect(); const targetRect = targetEl[0].getBoundingClientRect(); // Position at source flyingChip.css({ left: sourceRect.left + 'px', top: sourceRect.top + 'px', transform: 'translate(0, 0)' }); $('body').append(flyingChip); // Hide original element during animation if (sourceChipEl && sourceChipEl.length) { sourceChipEl.css('opacity', '0'); } // Trigger fly animation (use double RAF for reliable animation start) requestAnimationFrame(() => { requestAnimationFrame(() => { flyingChip.addClass('animate'); flyingChip.css({ transform: `translate(${targetRect.left - sourceRect.left}px, ${targetRect.top - sourceRect.top}px)` }); }); }); // Clean up and update DOM after animation setTimeout(() => { flyingChip.remove(); updateVmPosition(vmId, from, to, vm); }, 220); // Slightly longer than animation duration to ensure completion }); } function updateVmPosition(vmId, from, to, vm) { // Remove from source if (from) { $(`#server-blade-${from} #vm-chip-${vmId}`).remove(); } else { $(`#unassigned-${vmId}`).remove(); } // Add to target if (to) { let vmChipsContainer = $(`#server-blade-${to} .vm-chips`); if (!vmChipsContainer.length) { // Create vm-chips container if it doesn't exist vmChipsContainer = $('
'); $(`#server-blade-${to}`).append(vmChipsContainer); } const newChip = createVmChip(vm); newChip.attr('id', `vm-chip-${vm.id}`); vmChipsContainer.append(newChip); // Update server blade state (empty/not empty) $(`#server-blade-${to}`).removeClass('empty'); } // Update source server empty state if (from) { const sourceChips = $(`#server-blade-${from} .vm-chips`).children(); if (sourceChips.length === 0) { $(`#server-blade-${from}`).addClass('empty'); } } } function updateUtilizationBars(placement) { placement.servers.forEach(server => { const cpuBar = $(`#util-${server.id}-cpu`); const memBar = $(`#util-${server.id}-mem`); const stoBar = $(`#util-${server.id}-sto`); const updates = [ { bar: cpuBar, util: server.cpuUtilization || 0 }, { bar: memBar, util: server.memoryUtilization || 0 }, { bar: stoBar, util: server.storageUtilization || 0 } ]; updates.forEach(({ bar, util }) => { if (bar.length) { const newPct = Math.min(util * 100, 100); const oldPct = parseFloat(bar[0].style.width) || 0; bar.css('width', newPct + '%'); bar.removeClass('low medium high over').addClass(getUtilClass(util)); if (Math.abs(oldPct - newPct) > 1) { bar.addClass('changed'); setTimeout(() => bar.removeClass('changed'), 300); } } }); // Update server blade empty/overcommitted state const blade = $(`#server-blade-${server.id}`); const hasVms = server.vms && server.vms.length > 0; const isOvercommitted = (server.cpuUtilization || 0) > 1 || (server.memoryUtilization || 0) > 1 || (server.storageUtilization || 0) > 1; blade.toggleClass('empty', !hasVms); blade.toggleClass('overcommitted', isOvercommitted); }); } function updateUnassignedList(vms, vmById) { const container = $("#unassignedList"); const unassigned = vms.filter(vm => !vm.server); if (unassigned.length === 0) { if (!container.find('.all-assigned').length) { container.empty(); container.append(`
All VMs assigned!
`); } return; } // Remove "all assigned" message if present container.find('.all-assigned').remove(); // Update existing or add new unassigned VMs unassigned.sort((a, b) => (b.priority || 1) - (a.priority || 1)); unassigned.forEach(vm => { if (!$(`#unassigned-${vm.id}`).length) { const vmDiv = createUnassignedVmElement(vm); container.append(vmDiv); } }); } function createUnassignedVmElement(vm) { return $(`
${vm.name} ${vm.affinityGroup ? `${vm.affinityGroup}` : ''} ${vm.antiAffinityGroup ? `${vm.antiAffinityGroup}` : ''}
${vm.cpuCores}c ${vm.memoryGb}GB ${vm.storageGb}GB P${vm.priority || 1}
`); } function renderRackView(placement, vmById) { const container = $("#rackViewContainer"); container.empty(); // Group servers by rack const rackGroups = {}; placement.servers.forEach(server => { const rack = server.rack || "Unracked"; if (!rackGroups[rack]) { rackGroups[rack] = []; } rackGroups[rack].push(server); }); // Sort racks alphabetically const sortedRacks = Object.keys(rackGroups).sort(); sortedRacks.forEach(rackName => { const servers = rackGroups[rackName]; servers.sort((a, b) => a.name.localeCompare(b.name)); const rackDiv = $('
'); const rackHeader = $(`
${rackName} ${servers.length} servers
`); rackDiv.append(rackHeader); servers.forEach(server => { const blade = createServerBlade(server, vmById); rackDiv.append(blade); }); container.append(rackDiv); }); } function createServerBlade(server, vmById) { const cpuUtil = server.cpuUtilization || 0; const memUtil = server.memoryUtilization || 0; const storageUtil = server.storageUtilization || 0; const isEmpty = !server.vms || server.vms.length === 0; const isOvercommitted = cpuUtil > 1 || memUtil > 1 || storageUtil > 1; let bladeClass = "server-blade"; if (isEmpty) bladeClass += " empty"; if (isOvercommitted) bladeClass += " overcommitted"; const blade = $(`
`); // Header const header = $(`
${server.name} ${server.cpuCores}c / ${server.memoryGb}GB / ${server.storageGb}GB
`); blade.append(header); // Utilization bars with IDs for animation const utilBars = $('
'); utilBars.append(createMiniUtilBar(cpuUtil, "CPU", `util-${server.id}-cpu`)); utilBars.append(createMiniUtilBar(memUtil, "MEM", `util-${server.id}-mem`)); utilBars.append(createMiniUtilBar(storageUtil, "STO", `util-${server.id}-sto`)); blade.append(utilBars); // VM chips with IDs for animation const vmChips = $('
'); if (server.vms && server.vms.length > 0) { server.vms.forEach(vmId => { const vm = vmById[vmId]; if (vm) { const chip = createVmChip(vm); chip.attr('id', `vm-chip-${vm.id}`); vmChips.append(chip); } }); } blade.append(vmChips); // Click handler for details - pass server ID to get fresh data blade.click(function(e) { if (!$(e.target).hasClass('vm-chip')) { showServerDetails(server.id); } }); return blade; } function createMiniUtilBar(value, label, id) { const percentage = Math.min(value * 100, 100); const utilClass = getUtilClass(value); return $(`
`); } function createVmChip(vm) { const priority = vm.priority || 1; let chipClass = `vm-chip priority-${priority}`; if (vm.affinityGroup) chipClass += " affinity"; if (vm.antiAffinityGroup) chipClass += " anti-affinity"; let tooltip = vm.name; tooltip += `\nCPU: ${vm.cpuCores} cores`; tooltip += `\nMemory: ${vm.memoryGb} GB`; tooltip += `\nStorage: ${vm.storageGb} GB`; tooltip += `\nPriority: ${priority}`; if (vm.affinityGroup) tooltip += `\nAffinity: ${vm.affinityGroup}`; if (vm.antiAffinityGroup) tooltip += `\nAnti-Affinity: ${vm.antiAffinityGroup}`; return $(`${vm.name}`); } function renderCardView(placement, vmById) { const container = $("#cardViewContainer"); container.empty(); const sortedServers = [...placement.servers].sort((a, b) => { const aVms = a.vms ? a.vms.length : 0; const bVms = b.vms ? b.vms.length : 0; if (bVms !== aVms) return bVms - aVms; return a.name.localeCompare(b.name); }); sortedServers.forEach(server => { const card = createServerCard(server, vmById); container.append(card); }); } function createServerCard(server, vmById) { const cpuUtil = server.cpuUtilization || 0; const memUtil = server.memoryUtilization || 0; const storageUtil = server.storageUtilization || 0; const isEmpty = !server.vms || server.vms.length === 0; const cardWrapper = $('
'); const card = $(`
${server.name} ${server.rack ? `${server.rack}` : ''}
${createUtilRow("CPU", cpuUtil, server.cpuCores + " cores")} ${createUtilRow("Memory", memUtil, server.memoryGb + " GB")} ${createUtilRow("Storage", storageUtil, server.storageGb + " GB")}
${(server.vms || []).map(vmId => { const vm = vmById[vmId]; if (vm) { const priority = vm.priority || 1; let chipClass = `vm-chip priority-${priority}`; if (vm.affinityGroup) chipClass += " affinity"; if (vm.antiAffinityGroup) chipClass += " anti-affinity"; return `${vm.name}`; } return ''; }).join('')}
`); cardWrapper.append(card); return cardWrapper; } function createUtilRow(label, value, capacity) { const percentage = Math.min(value * 100, 100); const displayPercentage = Math.round(value * 100); const utilClass = getUtilClass(value); const bgClass = { 'low': 'bg-success', 'medium': 'bg-warning', 'high': 'bg-danger', 'over': 'bg-danger' }[utilClass]; return `
${label} ${capacity} (${displayPercentage}%)
`; } function renderUnassignedVMs(vms) { const container = $("#unassignedList"); container.empty(); const unassigned = vms.filter(vm => !vm.server); if (unassigned.length === 0) { container.append(`
All VMs assigned!
`); return; } unassigned.sort((a, b) => (b.priority || 1) - (a.priority || 1)); unassigned.forEach(vm => { const vmDiv = createUnassignedVmElement(vm); container.append(vmDiv); }); } function getPriorityBadgeClass(priority) { switch(priority) { case 5: return "danger"; case 4: return "primary"; case 3: return "success"; case 2: return "secondary"; default: return "light text-dark"; } } function showServerDetails(serverId) { // Always get fresh data from loadedPlacement if (!loadedPlacement || !loadedPlacement.servers) { console.error('No placement data available'); return; } const server = loadedPlacement.servers.find(s => s.id === serverId); if (!server) { console.error('Server not found:', serverId); return; } // Build VM lookup from current placement const vmById = {}; if (loadedPlacement.vms) { loadedPlacement.vms.forEach(vm => vmById[vm.id] = vm); } const modal = new bootstrap.Modal("#serverDetailModal"); const content = $("#serverDetailModalContent"); $("#serverDetailModalLabel").html(`${server.name}`); const cpuUtil = server.cpuUtilization || 0; const memUtil = server.memoryUtilization || 0; const storageUtil = server.storageUtilization || 0; content.html(`
Specifications
Rack${server.rack || 'Unracked'}
CPU Cores${server.cpuCores}
Memory${server.memoryGb} GB
Storage${server.storageGb} GB
Utilization
${createUtilRow("CPU", cpuUtil, `${server.usedCpu || 0}/${server.cpuCores} cores`)} ${createUtilRow("Memory", memUtil, `${server.usedMemory || 0}/${server.memoryGb} GB`)} ${createUtilRow("Storage", storageUtil, `${server.usedStorage || 0}/${server.storageGb} GB`)}
Assigned VMs (${server.vms ? server.vms.length : 0})
${server.vms && server.vms.length > 0 ? ` ${server.vms.map(vmId => { const vm = vmById[vmId]; if (vm) { return ` `; } return ''; }).join('')}
Name CPU Memory Storage Priority
${vm.name} ${vm.cpuCores} ${vm.memoryGb} GB ${vm.storageGb} GB P${vm.priority || 1}
` : '

No VMs assigned

'}
`); modal.show(); } function getUtilClass(value) { if (value > 1) return 'over'; if (value > 0.8) return 'high'; if (value > 0.5) return 'medium'; return 'low'; } function solve() { if (!loadedPlacement) { showSimpleError("No placement data loaded. Please wait for the data to load or refresh the page."); return; } // Reset animation state for solving - capture current positions before solving modifies them vmPositionCache = buildPositionCache(loadedPlacement); console.log('Solve started. Initial position cache size:', Object.keys(vmPositionCache).length); $.post("/placements", JSON.stringify(loadedPlacement), function (data) { placementId = data; refreshSolvingButtons(true); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Start solving failed.", xhr); refreshSolvingButtons(false); }, "text"); } function stopSolving() { $.delete(`/placements/${placementId}`, function () { refreshSolvingButtons(false); // Do a final full render to ensure accuracy isFirstRender = true; refreshPlacement(); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Stop solving failed.", xhr); }); } function analyze() { const modal = new bootstrap.Modal("#scoreAnalysisModal"); modal.show(); const scoreAnalysisModalContent = $("#scoreAnalysisModalContent"); scoreAnalysisModalContent.empty(); if (!loadedPlacement || loadedPlacement.score == null) { scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'Solve' button."); return; } $('#scoreAnalysisScoreLabel').text(`(${loadedPlacement.score})`); $.put("/placements/analyze", JSON.stringify(loadedPlacement), function (scoreAnalysis) { let constraints = scoreAnalysis.constraints; constraints.sort((a, b) => { let aComponents = getScoreComponents(a.score); let bComponents = getScoreComponents(b.score); if (aComponents.hard < 0 && bComponents.hard > 0) return -1; if (aComponents.hard > 0 && bComponents.soft < 0) return 1; if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) { return -1; } else { if (Math.abs(aComponents.soft) > Math.abs(bComponents.soft)) { return -1; } return Math.abs(bComponents.soft) - Math.abs(aComponents.soft); } }); constraints = constraints.map((e) => { let components = getScoreComponents(e.weight); e.type = components.hard !== 0 ? 'hard' : 'soft'; e.weight = components[e.type]; let scores = getScoreComponents(e.score); e.implicitScore = scores.hard !== 0 ? scores.hard : scores.soft; return e; }); scoreAnalysisModalContent.empty(); const analysisTable = $(``).css({textAlign: 'center'}); const analysisTHead = $(``).append($(``) .append($(``)) .append($(``).css({textAlign: 'left'})) .append($(``)) .append($(``)) .append($(``)) .append($(``))); analysisTable.append(analysisTHead); const analysisTBody = $(``); $.each(constraints, (index, constraintAnalysis) => { let icon = constraintAnalysis.type === "hard" && constraintAnalysis.implicitScore < 0 ? '' : ''; if (!icon) { icon = constraintAnalysis.matches.length === 0 ? '' : ''; } let row = $(``); row.append($(`
ConstraintType# MatchesWeightScore
`).html(icon)) .append($(``).text(constraintAnalysis.name).css({textAlign: 'left'})) .append($(``).html(`${constraintAnalysis.type}`)) .append($(``).html(`${constraintAnalysis.matches.length}`)) .append($(``).text(constraintAnalysis.weight)) .append($(``).text(constraintAnalysis.implicitScore)); analysisTBody.append(row); }); analysisTable.append(analysisTBody); scoreAnalysisModalContent.append(analysisTable); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Analyze failed.", xhr); }, "text"); } function getScoreComponents(score) { let components = {hard: 0, soft: 0}; if (!score) return components; $.each([...score.matchAll(/(-?\d*\.?\d+)(hard|soft)/g)], (i, parts) => { components[parts[2]] = parseFloat(parts[1]); }); return components; } function refreshSolvingButtons(solving) { if (solving) { $("#solveButton").hide(); $("#stopSolvingButton").show(); $("#solvingSpinner").addClass("active"); // Add solving pulse animation to summary card values $(".summary-card .value").addClass("solving-pulse"); if (autoRefreshIntervalId == null) { // 250ms polling for smooth real-time animations autoRefreshIntervalId = setInterval(refreshPlacement, 250); } } else { $("#solveButton").show(); $("#stopSolvingButton").hide(); $("#solvingSpinner").removeClass("active"); // Remove solving pulse animation $(".summary-card .value").removeClass("solving-pulse"); if (autoRefreshIntervalId != null) { clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = null; } } } function replaceQuickstartSolverForgeAutoHeaderFooter() { const solverforgeHeader = $("header#solverforge-auto-header"); if (solverforgeHeader != null) { solverforgeHeader.css("background-color", "#ffffff"); solverforgeHeader.append( $(`
`)); } const solverforgeFooter = $("footer#solverforge-auto-footer"); if (solverforgeFooter != null) { solverforgeFooter.append( $(``)); } } function copyTextToClipboard(elementId) { const element = document.getElementById(elementId); if (element) { navigator.clipboard.writeText(element.textContent).then(() => { // Optional: show feedback }).catch(err => { console.error('Failed to copy text: ', err); }); } }