239 lines
9.3 KiB
Go
239 lines
9.3 KiB
Go
package webui
|
|
|
|
func renderMetrics() string {
|
|
return `<p style="color:var(--muted);font-size:13px;margin-bottom:16px">Live metrics — updated every 2 seconds.</p>
|
|
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">Server — Load</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-server-load" data-chart-refresh="1" src="/api/metrics/chart/server-load.svg" style="width:100%;display:block;border-radius:6px" alt="CPU/Mem load">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">Temperature — CPU</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-server-temp-cpu" data-chart-refresh="1" src="/api/metrics/chart/server-temp-cpu.svg" style="width:100%;display:block;border-radius:6px" alt="CPU temperature">
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">Temperature — Ambient Sensors</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-server-temp-ambient" data-chart-refresh="1" src="/api/metrics/chart/server-temp-ambient.svg" style="width:100%;display:block;border-radius:6px" alt="Ambient temperature sensors">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">Server — Power</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-server-power" data-chart-refresh="1" src="/api/metrics/chart/server-power.svg" style="width:100%;display:block;border-radius:6px" alt="System power">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="card-server-fans" class="card" style="margin-bottom:16px;display:none">
|
|
<div class="card-head">Server — Fan RPM</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-server-fans" data-chart-refresh="1" src="/api/metrics/chart/server-fans.svg" style="width:100%;display:block;border-radius:6px" alt="Fan RPM">
|
|
</div>
|
|
</div>
|
|
|
|
<section id="gpu-metrics-section" style="display:none;margin-top:24px;padding:16px 16px 4px;border:1px solid #d7e0ea;border-radius:10px;background:linear-gradient(180deg,#f7fafc 0%,#eef4f8 100%)">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px">
|
|
<div>
|
|
<div style="font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#486581">GPU Metrics</div>
|
|
<div id="gpu-metrics-summary" style="font-size:13px;color:var(--muted);margin-top:4px">Detected GPUs are rendered in a dedicated section.</div>
|
|
</div>
|
|
<label style="display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--ink);font-weight:700;cursor:pointer">
|
|
<input id="gpu-chart-toggle" type="checkbox">
|
|
<span>One chart per GPU</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div id="gpu-metrics-by-metric">
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">GPU — Compute Load</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-gpu-all-load" data-chart-refresh="1" src="/api/metrics/chart/gpu-all-load.svg" style="width:100%;display:block;border-radius:6px" alt="GPU compute load">
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">GPU — Memory Load</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-gpu-all-memload" data-chart-refresh="1" src="/api/metrics/chart/gpu-all-memload.svg" style="width:100%;display:block;border-radius:6px" alt="GPU memory load">
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">GPU — Core Clock</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-gpu-all-clock" data-chart-refresh="1" src="/api/metrics/chart/gpu-all-clock.svg" style="width:100%;display:block;border-radius:6px" alt="GPU core clock">
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">GPU — Power</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-gpu-all-power" data-chart-refresh="1" src="/api/metrics/chart/gpu-all-power.svg" style="width:100%;display:block;border-radius:6px" alt="GPU power">
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:16px">
|
|
<div class="card-head">GPU — Temperature</div>
|
|
<div class="card-body" style="padding:8px">
|
|
<img id="chart-gpu-all-temp" data-chart-refresh="1" src="/api/metrics/chart/gpu-all-temp.svg" style="width:100%;display:block;border-radius:6px" alt="GPU temperature">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="gpu-metrics-by-gpu" style="display:none"></div>
|
|
</section>
|
|
|
|
<script>
|
|
let gpuChartKey = '';
|
|
const gpuChartModeStorageKey = 'bee.metrics.gpuChartMode';
|
|
let metricsNvidiaGPUsPromise = null;
|
|
|
|
function loadMetricsNvidiaGPUs() {
|
|
if (!metricsNvidiaGPUsPromise) {
|
|
metricsNvidiaGPUsPromise = fetch('/api/gpu/nvidia')
|
|
.then(function(r) {
|
|
if (!r.ok) throw new Error('Failed to load NVIDIA GPUs.');
|
|
return r.json();
|
|
})
|
|
.then(function(list) { return Array.isArray(list) ? list : []; })
|
|
.catch(function() { return []; });
|
|
}
|
|
return metricsNvidiaGPUsPromise;
|
|
}
|
|
|
|
function metricsGPUNameMap(list) {
|
|
const out = {};
|
|
(list || []).forEach(function(gpu) {
|
|
const idx = Number(gpu.index);
|
|
if (!Number.isFinite(idx) || !gpu.name) return;
|
|
out[idx] = gpu.name;
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function metricsGPUDisplayLabel(idx, names) {
|
|
const name = names && names[idx];
|
|
return name ? ('GPU ' + idx + ' — ' + name) : ('GPU ' + idx);
|
|
}
|
|
|
|
function loadGPUChartModePreference() {
|
|
try {
|
|
return sessionStorage.getItem(gpuChartModeStorageKey) === 'per-gpu';
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function saveGPUChartModePreference(perGPU) {
|
|
try {
|
|
sessionStorage.setItem(gpuChartModeStorageKey, perGPU ? 'per-gpu' : 'per-metric');
|
|
} catch (_) {}
|
|
}
|
|
|
|
function refreshChartImage(el) {
|
|
if (!el || el.dataset.loading === '1') return;
|
|
if (el.offsetParent === null) return;
|
|
const baseSrc = el.dataset.baseSrc || el.src.split('?')[0];
|
|
const nextSrc = baseSrc + '?t=' + Date.now();
|
|
const probe = new Image();
|
|
el.dataset.baseSrc = baseSrc;
|
|
el.dataset.loading = '1';
|
|
probe.onload = function() {
|
|
el.src = nextSrc;
|
|
el.dataset.loading = '0';
|
|
};
|
|
probe.onerror = function() {
|
|
el.dataset.loading = '0';
|
|
};
|
|
probe.src = nextSrc;
|
|
}
|
|
|
|
function refreshCharts() {
|
|
document.querySelectorAll('img[data-chart-refresh="1"]').forEach(refreshChartImage);
|
|
}
|
|
|
|
function gpuIndices(rows) {
|
|
const seen = {};
|
|
const out = [];
|
|
(rows || []).forEach(function(row) {
|
|
const idx = Number(row.index);
|
|
if (!Number.isFinite(idx) || seen[idx]) return;
|
|
seen[idx] = true;
|
|
out.push(idx);
|
|
});
|
|
return out.sort(function(a, b) { return a - b; });
|
|
}
|
|
|
|
function renderGPUOverviewCards(indices, names) {
|
|
const host = document.getElementById('gpu-metrics-by-gpu');
|
|
if (!host) return;
|
|
host.innerHTML = indices.map(function(idx) {
|
|
const label = metricsGPUDisplayLabel(idx, names);
|
|
return '<div class="card" style="margin-bottom:16px">' +
|
|
'<div class="card-head">' + label + ' — Overview</div>' +
|
|
'<div class="card-body" style="padding:8px">' +
|
|
'<img id="chart-gpu-' + idx + '-overview" data-chart-refresh="1" src="/api/metrics/chart/gpu/' + idx + '-overview.svg" style="width:100%;display:block;border-radius:6px" alt="' + label + ' overview">' +
|
|
'</div></div>';
|
|
}).join('');
|
|
}
|
|
|
|
function applyGPUChartMode() {
|
|
const perMetric = document.getElementById('gpu-metrics-by-metric');
|
|
const perGPU = document.getElementById('gpu-metrics-by-gpu');
|
|
const toggle = document.getElementById('gpu-chart-toggle');
|
|
const gpuModePerGPU = !!(toggle && toggle.checked);
|
|
if (perMetric) perMetric.style.display = gpuModePerGPU ? 'none' : '';
|
|
if (perGPU) perGPU.style.display = gpuModePerGPU ? '' : 'none';
|
|
}
|
|
|
|
function syncMetricsLayout(d) {
|
|
const fanCard = document.getElementById('card-server-fans');
|
|
if (fanCard) fanCard.style.display = (d.fans && d.fans.length > 0) ? '' : 'none';
|
|
const section = document.getElementById('gpu-metrics-section');
|
|
const summary = document.getElementById('gpu-metrics-summary');
|
|
const indices = gpuIndices(d.gpus);
|
|
loadMetricsNvidiaGPUs().then(function(gpus) {
|
|
const names = metricsGPUNameMap(gpus);
|
|
if (section) section.style.display = indices.length > 0 ? '' : 'none';
|
|
if (summary) {
|
|
summary.textContent = indices.length > 0
|
|
? ('Detected GPUs: ' + indices.map(function(idx) { return metricsGPUDisplayLabel(idx, names); }).join(', '))
|
|
: 'No GPUs detected in live metrics.';
|
|
}
|
|
const nextKey = indices.join(',') + '|' + indices.map(function(idx) { return names[idx] || ''; }).join(',');
|
|
if (nextKey !== gpuChartKey) {
|
|
renderGPUOverviewCards(indices, names);
|
|
gpuChartKey = nextKey;
|
|
}
|
|
applyGPUChartMode();
|
|
});
|
|
}
|
|
|
|
function loadMetricsLayout() {
|
|
fetch('/api/metrics/latest').then(function(r) { return r.json(); }).then(syncMetricsLayout).catch(function() {});
|
|
}
|
|
|
|
const gpuChartToggle = document.getElementById('gpu-chart-toggle');
|
|
if (gpuChartToggle) {
|
|
gpuChartToggle.checked = loadGPUChartModePreference();
|
|
}
|
|
applyGPUChartMode();
|
|
|
|
if (gpuChartToggle) {
|
|
gpuChartToggle.addEventListener('change', function() {
|
|
saveGPUChartModePreference(!!gpuChartToggle.checked);
|
|
applyGPUChartMode();
|
|
refreshCharts();
|
|
});
|
|
}
|
|
|
|
loadMetricsLayout();
|
|
setInterval(refreshCharts, 3000);
|
|
setInterval(loadMetricsLayout, 5000);
|
|
</script>`
|
|
}
|