/** * Dashboard logic: stats rendering, audit selector, CPU chart, migration table. * Expects globals: SKELETON_MODE, CURRENT_CLUSTER, auditData (set by index.html). * Depends on: utils.js (formatBytes, getCSSVar, calculateStats) */ (function() { var cpuDistributionChart = null; document.getElementById('auditSelector').addEventListener('change', function(e) { var option = this.options[this.selectedIndex]; if (!option) return; document.getElementById('previewCpu').textContent = option.dataset.cpu || '1.0'; document.getElementById('previewRam').textContent = option.dataset.ram || '1.0'; document.getElementById('previewScope').textContent = option.dataset.scope || 'Full Cluster'; document.getElementById('previewStrategy').textContent = option.dataset.strategy || 'Balanced'; }); function setStat(key, text) { document.querySelectorAll('[data-stats="' + key + '"]').forEach(function(el) { el.textContent = text; el.classList.remove('animate-pulse'); }); } function setProgress(key, value) { document.querySelectorAll('[data-stats="' + key + '"]').forEach(function(el) { if (el.tagName === 'PROGRESS') { el.value = value; el.classList.remove('animate-pulse'); } }); } function renderStats(data) { if (!data) return; var el = function(k) { return document.querySelector('[data-stats="' + k + '"]'); }; var regionBadge = document.getElementById('regionBadge'); if (regionBadge) regionBadge.textContent = data.region && data.region.name ? data.region.name : '—'; setStat('pcpu.usage', Number((data.pcpu && data.pcpu.usage) || 0).toFixed(1)); setStat('pcpu.total', String((data.pcpu && data.pcpu.total) || 0)); setStat('pcpu.used_percentage', Number((data.pcpu && data.pcpu.used_percentage) || 0).toFixed(1) + '%'); setStat('pcpu.usage_val', Number((data.pcpu && data.pcpu.usage) || 0).toFixed(1) + ' CPU'); setProgress('pcpu.progress', (data.pcpu && data.pcpu.used_percentage) || 0); setStat('pcpu.free', String((data.pcpu && data.pcpu.free) || 0)); var pramUsageGb = formatBytes(data.pram && data.pram.usage, 'GB'); var pramTotalGb = formatBytes(data.pram && data.pram.total, 'GB'); var pramFreeGb = formatBytes(data.pram && data.pram.free, 'GB'); setStat('pram.usage_gb', pramUsageGb); setStat('pram.total_gb', pramTotalGb); setStat('pram.used_percentage', Number((data.pram && data.pram.used_percentage) || 0).toFixed(1) + '%'); setStat('pram.usage_gb_val', pramUsageGb + ' GB'); setProgress('pram.progress', (data.pram && data.pram.used_percentage) || 0); setStat('pram.free_gb', pramFreeGb + ' GB'); setStat('vm.active', String(data.vm && data.vm.active)); setStat('vm.stopped', String(data.vm && data.vm.stopped)); setStat('vm.count', String(data.vm && data.vm.count)); setStat('flavors.first_name', data.flavors && data.flavors.first_common_flavor ? data.flavors.first_common_flavor.name : '—'); setStat('vm.avg_cpu', Number((data.vm && data.vm.avg_cpu) || 0).toFixed(1)); setStat('vm.density', Number((data.vm && data.vm.density) || 0).toFixed(1) + '/host'); setStat('vcpu.allocated_total', ((data.vcpu && data.vcpu.allocated) || 0) + ' / ' + ((data.vcpu && data.vcpu.total) || 0) + ' vCPU'); setProgress('vcpu.progress', (data.vcpu && data.vcpu.allocated_percentage) || 0); setStat('vcpu.allocated_percentage', Number((data.vcpu && data.vcpu.allocated_percentage) || 0).toFixed(1) + '%'); var vcpuOver = el('vcpu.overcommit'); if (vcpuOver) { vcpuOver.textContent = 'overcommit: ' + Number((data.vcpu && data.vcpu.overcommit_ratio) || 0).toFixed(1) + ' / ' + Number((data.vcpu && data.vcpu.overcommit_max) || 0).toFixed(1) + ' — ' + Number((data.vcpu && data.vcpu.allocated_percentage) || 0).toFixed(1) + '% allocated'; vcpuOver.classList.remove('animate-pulse'); } var vramAllocGb = formatBytes(data.vram && data.vram.allocated, 'GB'); var vramTotalGb = formatBytes(data.vram && data.vram.total, 'GB'); setStat('vram.allocated_total', vramAllocGb + ' / ' + vramTotalGb + ' GB'); setProgress('vram.progress', (data.vram && data.vram.allocated_percentage) || 0); setStat('vram.allocated_percentage', Number((data.vram && data.vram.allocated_percentage) || 0).toFixed(1) + '%'); var vramOver = el('vram.overcommit'); if (vramOver) { vramOver.textContent = 'overcommit: ' + Number((data.vram && data.vram.overcommit_ratio) || 0).toFixed(1) + ' / ' + Number((data.vram && data.vram.overcommit_max) || 0).toFixed(1) + ' — ' + Number((data.vram && data.vram.allocated_percentage) || 0).toFixed(1) + '% allocated'; vramOver.classList.remove('animate-pulse'); } setStat('flavors.first_count', (data.flavors && data.flavors.first_common_flavor ? data.flavors.first_common_flavor.count : 0) + ' instances'); var vmCount = data.vm && data.vm.count ? data.vm.count : 0; var firstCount = data.flavors && data.flavors.first_common_flavor ? data.flavors.first_common_flavor.count : 0; setStat('flavors.first_share', (vmCount ? Math.round(firstCount / vmCount * 100) : 0) + '%'); setStat('flavors.second_name', data.flavors && data.flavors.second_common_flavor ? data.flavors.second_common_flavor.name : '—'); setStat('flavors.second_count', data.flavors && data.flavors.second_common_flavor ? String(data.flavors.second_common_flavor.count) : '—'); setStat('flavors.third_name', data.flavors && data.flavors.third_common_flavor ? data.flavors.third_common_flavor.name : '—'); setStat('flavors.third_count', data.flavors && data.flavors.third_common_flavor ? String(data.flavors.third_common_flavor.count) : '—'); document.querySelectorAll('[data-stats]').forEach(function(n) { n.classList.remove('animate-pulse'); }); } function renderAudits(auditsList) { if (!auditsList || !auditsList.length) { var countEl = document.getElementById('auditsCount'); if (countEl) countEl.textContent = '0 available'; var sel = document.getElementById('auditSelector'); if (sel) { sel.disabled = false; sel.innerHTML = ''; } return; } window.auditData = {}; auditsList.forEach(function(a) { window.auditData[a.id] = { name: a.name, migrations: typeof a.migrations === 'string' ? JSON.parse(a.migrations) : a.migrations, hostData: { labels: typeof a.host_labels === 'string' ? JSON.parse(a.host_labels) : a.host_labels, current: typeof a.cpu_current === 'string' ? JSON.parse(a.cpu_current) : a.cpu_current, projected: typeof a.cpu_projected === 'string' ? JSON.parse(a.cpu_projected) : a.cpu_projected } }; }); var sel = document.getElementById('auditSelector'); if (sel) { sel.disabled = false; sel.innerHTML = ''; auditsList.forEach(function(audit) { var opt = document.createElement('option'); opt.value = audit.id; opt.setAttribute('data-cpu', audit.cpu_weight || '1.0'); opt.setAttribute('data-ram', audit.ram_weight || '1.0'); opt.setAttribute('data-scope', audit.scope || 'Full Cluster'); opt.setAttribute('data-strategy', audit.strategy || 'Balanced'); opt.setAttribute('data-goal', audit.goal || ''); var dateStr = audit.created_at ? new Date(audit.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : ''; opt.textContent = audit.name + ' (' + dateStr + ')'; sel.appendChild(opt); }); } var countEl = document.getElementById('auditsCount'); if (countEl) countEl.textContent = auditsList.length + ' available'; if (auditsList.length > 0) { document.getElementById('auditSelector').dispatchEvent(new Event('change')); loadSelectedAudit(); } } window.loadSelectedAudit = function() { var auditId = document.getElementById('auditSelector').value; updateMigrationTable(auditId); updateCPUCharts(auditId); }; function updateMigrationTable(auditId) { var tbody = document.getElementById('migrationTableBody'); var migrationCount = document.getElementById('migrationCount'); var data = window.auditData && window.auditData[auditId]; if (!data || !data.migrations || data.migrations.length === 0) { tbody.innerHTML = '
' + migration.flavor + '