116 lines
4.2 KiB
JavaScript
Executable File
116 lines
4.2 KiB
JavaScript
Executable File
|
||
// Simple status dashboard
|
||
const DATA_URL = 'data/status.json';
|
||
let autoRefresh = true;
|
||
let refreshTimer = null;
|
||
|
||
function formatMB(mb) {
|
||
return `${mb.toFixed(2)} MB`;
|
||
}
|
||
function formatPercent(p) {
|
||
return `${p.toFixed(2)}%`;
|
||
}
|
||
function setRing(el, percent) {
|
||
const deg = Math.min(100, Math.max(0, percent)) * 3.6; // 100% -> 360deg
|
||
el.style.setProperty('--deg', `${deg}deg`);
|
||
}
|
||
|
||
function renderSystem(system) {
|
||
const cpuPercent = system?.cpu?.percent ?? 0;
|
||
const ring = document.getElementById('cpu-ring');
|
||
setRing(ring, cpuPercent);
|
||
document.getElementById('cpu-percent').textContent = formatPercent(cpuPercent);
|
||
const la = system?.cpu?.load_avg ?? {};
|
||
document.getElementById('load-1m').textContent = (la['1m'] ?? '–');
|
||
document.getElementById('load-5m').textContent = (la['5m'] ?? '–');
|
||
document.getElementById('load-15m').textContent = (la['15m'] ?? '–');
|
||
|
||
const mem = system?.memory ?? {};
|
||
const pct = mem.percent ?? 0;
|
||
document.getElementById('mem-bar').style.width = `${pct}%`;
|
||
document.getElementById('mem-percent').textContent = formatPercent(pct);
|
||
document.getElementById('mem-used').textContent = formatMB(mem.used_mb ?? 0);
|
||
document.getElementById('mem-total').textContent = formatMB(mem.total_mb ?? 0);
|
||
}
|
||
|
||
function portBadge(p) {
|
||
const host = p.host_ip ? `${p.host_ip}` : '';
|
||
const hostPort = p.host_port ? `:${p.host_port}` : '';
|
||
const container = p.container_port || '';
|
||
const text = `${host}${hostPort} → ${container}`.trim();
|
||
return `<span class="port">${text || '—'}</span>`;
|
||
}
|
||
|
||
function renderContainers(list) {
|
||
const q = document.getElementById('search').value.toLowerCase();
|
||
const root = document.getElementById('container-list');
|
||
root.innerHTML = '';
|
||
|
||
const filtered = list.filter(c => {
|
||
const s = `${c.name} ${c.image}`.toLowerCase();
|
||
return s.includes(q);
|
||
});
|
||
|
||
document.getElementById('container-count').textContent = `${filtered.length} / ${list.length} shown`;
|
||
|
||
filtered.forEach(c => {
|
||
const statusClass = (c.status === 'running') ? 'status' : 'status stopped';
|
||
const ports = (c.openPorts && c.openPorts.length) ? c.openPorts.map(portBadge).join('') : '<span class="muted">No published ports</span>';
|
||
const cpu = c.resources?.cpu_percent ?? 0;
|
||
const memMb = c.resources?.memory_mb ?? 0;
|
||
const memPct = c.resources?.memory_percent ?? 0;
|
||
|
||
const row = document.createElement('div');
|
||
row.className = 'row';
|
||
row.innerHTML = `
|
||
<div class="name">${c.name}<div class="muted">${c.image}</div></div>
|
||
<div><span class="${statusClass}">${c.status}</span></div>
|
||
<div class="ports">${ports}</div>
|
||
<div>
|
||
<span class="badge">CPU: ${formatPercent(cpu)}</span>
|
||
<span class="badge">Mem: ${formatMB(memMb)} (${formatPercent(memPct)})</span>
|
||
</div>
|
||
<div class="muted small">${c.id.slice(0,12)}</div>
|
||
`;
|
||
root.appendChild(row);
|
||
});
|
||
}
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await fetch(`${DATA_URL}?t=${Date.now()}`, { cache: 'no-store' });
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const data = await res.json();
|
||
document.getElementById('last-update').textContent = new Date(data.timestamp).toLocaleString();
|
||
renderSystem(data.system);
|
||
renderContainers(data.containers || []);
|
||
} catch (err) {
|
||
console.error('Failed to load status:', err);
|
||
document.getElementById('last-update').textContent = 'failed';
|
||
}
|
||
}
|
||
|
||
function startAutoRefresh() {
|
||
if (refreshTimer) clearInterval(refreshTimer);
|
||
refreshTimer = setInterval(() => {
|
||
if (autoRefresh) loadStatus();
|
||
}, 5000);
|
||
}
|
||
|
||
// Events
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
document.getElementById('toggle-refresh').addEventListener('click', () => {
|
||
autoRefresh = !autoRefresh;
|
||
document.getElementById('toggle-refresh').textContent = autoRefresh ? 'Pause' : 'Resume';
|
||
document.getElementById('refresh-state').textContent = autoRefresh ? 'on' : 'paused';
|
||
if (autoRefresh) loadStatus();
|
||
});
|
||
document.getElementById('search').addEventListener('input', () => {
|
||
// Re-render list using current data in memory by fetching again (cheap)
|
||
loadStatus();
|
||
});
|
||
|
||
loadStatus();
|
||
startAutoRefresh();
|
||
});
|