/** * 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 = 'No migration actions recommended'; migrationCount.textContent = '0 actions'; return; } var html = ''; data.migrations.forEach(function(migration) { var impact = migration.impact || 'Low'; var impactClass = { 'Low': 'badge-success', 'Medium': 'badge-warning', 'High': 'badge-error' }[impact] || 'badge-neutral'; html += '
' + migration.instanceName + '
' + migration.source + '' + migration.destination + '
' + migration.flavor + '' + impact + ''; }); tbody.innerHTML = html; migrationCount.textContent = data.migrations.length + ' action' + (data.migrations.length !== 1 ? 's' : ''); } function updateCPUCharts(auditId) { var data = window.auditData && window.auditData[auditId]; if (!data || !data.hostData) return; var hasProjected = (auditId !== 'current'); var ctx = document.getElementById('cpuDistributionChart').getContext('2d'); var currentStats = calculateStats(data.hostData.current); document.getElementById('currentCpuMean').textContent = currentStats.mean.toFixed(1); document.getElementById('currentCpuStd').textContent = (currentStats.std * 0.5).toFixed(1); var stdBlock = document.getElementById('currentCpuStdBlock'); if (stdBlock) stdBlock.style.display = hasProjected ? '' : 'none'; if (cpuDistributionChart) cpuDistributionChart.destroy(); var colors = { primary: getCSSVar('--color-primary'), secondary: getCSSVar('--color-secondary'), accent: getCSSVar('--color-accent'), neutral: getCSSVar('--color-neutral'), info: getCSSVar('--color-info'), success: getCSSVar('--color-success'), warning: getCSSVar('--color-warning'), error: getCSSVar('--color-error') }; var textColor = getCSSVar('--color-base-content'); var gridColor = getCSSVar('--chart-grid-color') || textColor; var datasets = [ { label: 'Current', data: data.hostData.current.slice(), backgroundColor: colors.info + '40', borderColor: colors.info, borderWidth: 1, borderRadius: 3 } ]; if (hasProjected) { datasets.push({ label: 'Projected', data: data.hostData.projected.slice(), backgroundColor: colors.warning + '40', borderColor: colors.warning, borderWidth: 1, borderRadius: 3 }); } var annotationConfig = { MeanLine: { type: 'line', yMin: currentStats.mean, yMax: currentStats.mean, borderColor: colors.success, borderWidth: 2, borderDash: [] } }; if (hasProjected) { annotationConfig.upperStdLine = { type: 'line', yMin: currentStats.mean + currentStats.std * 0.5, yMax: currentStats.mean + currentStats.std * 0.5, borderColor: colors.error, borderWidth: 1, borderDash: [5, 5] }; annotationConfig.lowerStdLine = { type: 'line', yMin: currentStats.mean > currentStats.std * 0.5 ? currentStats.mean - currentStats.std * 0.5 : 0, yMax: currentStats.mean > currentStats.std * 0.5 ? currentStats.mean - currentStats.std * 0.5 : 0, borderColor: colors.error, borderWidth: 1, borderDash: [5, 5] }; } cpuDistributionChart = new Chart(ctx, { type: 'bar', data: { labels: data.hostData.labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, animation: { onComplete: function() { var chart = this.chart || this; if (chart._hidingDataset === undefined) return; var i = chart._hidingDataset; chart.getDatasetMeta(i).hidden = true; chart.data.datasets[i].data = chart._cpuOriginalData[i].slice(); delete chart._hidingDataset; chart.update('none'); } }, plugins: { legend: { display: true, position: 'top', align: 'center', onClick: function(e, legendItem, legend) { var i = legendItem.datasetIndex; var chart = legend.chart; var len = chart.data.labels.length; if (chart.isDatasetVisible(i)) { chart._hidingDataset = i; chart.data.datasets[i].data = Array(len).fill(0); chart.update(); } else { chart.data.datasets[i].data = Array(len).fill(0); chart.show(i); chart.update('none'); chart.data.datasets[i].data = chart._cpuOriginalData[i].slice(); chart.update(); } }, labels: { usePointStyle: true, pointStyle: 'rect', boxWidth: 14, boxHeight: 14, padding: 12, color: textColor, generateLabels: function(chart) { var datasets = chart.data.datasets; var labelColor = getCSSVar('--color-base-content') || textColor; return datasets.map(function(ds, i) { return { text: ds.label, fillStyle: ds.borderColor, strokeStyle: ds.borderColor, lineWidth: 1, fontColor: labelColor, color: labelColor, hidden: !chart.isDatasetVisible(i), datasetIndex: i }; }); } } }, tooltip: { callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + Number(ctx.parsed.y).toFixed(2) + '% CPU'; } } }, annotation: { annotations: annotationConfig } }, scales: { y: { beginAtZero: true, max: 100, grid: { drawBorder: false, color: gridColor }, ticks: { color: textColor, callback: function(value) { return value + '%'; } } }, x: { grid: { display: false }, ticks: { display: false }, barPercentage: 1, categoryPercentage: 0.85 } } } }); cpuDistributionChart._cpuOriginalData = hasProjected ? [ data.hostData.current.slice(), data.hostData.projected.slice() ] : [ data.hostData.current.slice() ]; } document.addEventListener('DOMContentLoaded', function() { if (typeof SKELETON_MODE !== 'undefined' && SKELETON_MODE) { Promise.all([ fetch('/api/stats/').then(function(r) { return r.ok ? r.json() : Promise.reject(r); }), fetch('/api/audits/').then(function(r) { return r.ok ? r.json() : Promise.reject(r); }) ]).then(function(results) { renderStats(results[0]); renderAudits(results[1].audits); if (!results[1].audits || results[1].audits.length === 0) { var cc = results[1].current_cluster; if (cc && cc.host_labels && cc.cpu_current && cc.host_labels.length) { window.auditData = window.auditData || {}; window.auditData.current = { hostData: { labels: cc.host_labels, current: cc.cpu_current, projected: cc.cpu_current } }; updateCPUCharts('current'); } } }).catch(function(err) { var msg = err.status ? 'Failed to load data (' + err.status + ')' : 'Failed to load data'; var countEl = document.getElementById('auditsCount'); if (countEl) countEl.textContent = msg; fetch('/api/stats/').then(function(r) { return r.ok ? r.json() : null; }).then(function(d) { if (d) renderStats(d); }); fetch('/api/audits/').then(function(r) { return r.ok ? r.json() : null; }).then(function(d) { if (d && d.audits) renderAudits(d.audits); }); }); } else { var initialAudit = typeof INITIAL_AUDIT_ID !== 'undefined' ? INITIAL_AUDIT_ID : ''; if (initialAudit && window.auditData && window.auditData[initialAudit]) { document.getElementById('auditSelector').dispatchEvent(new Event('change')); loadSelectedAudit(); } else if (!initialAudit && typeof CURRENT_CLUSTER !== 'undefined' && CURRENT_CLUSTER && CURRENT_CLUSTER.host_labels && CURRENT_CLUSTER.host_labels.length) { window.auditData = window.auditData || {}; window.auditData.current = { hostData: { labels: CURRENT_CLUSTER.host_labels, current: CURRENT_CLUSTER.cpu_current, projected: CURRENT_CLUSTER.cpu_current } }; updateCPUCharts('current'); } } }); document.addEventListener('themechange', function() { if (cpuDistributionChart) { var auditId = document.getElementById('auditSelector').value; cpuDistributionChart.destroy(); cpuDistributionChart = null; if (auditId) updateCPUCharts(auditId); } }); })();