Initial commit
This commit is contained in:
119
public/index.html
Executable file
119
public/index.html
Executable file
@@ -0,0 +1,119 @@
|
||||
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>System & Docker Status</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f172a; /* slate-900 */
|
||||
--card: #111827; /* gray-900 */
|
||||
--muted: #94a3b8; /* slate-400 */
|
||||
--text: #e5e7eb; /* gray-200 */
|
||||
--accent: #22d3ee; /* cyan-400 */
|
||||
--accent2: #34d399; /* green-400 */
|
||||
--danger: #f87171; /* red-400 */
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji','Segoe UI Emoji'; background: var(--bg); color: var(--text); }
|
||||
header { padding: 24px; border-bottom: 1px solid #1f2937; position: sticky; top: 0; background: linear-gradient(180deg, rgba(15,23,42,0.9), rgba(15,23,42,0.9)); backdrop-filter: blur(6px); z-index: 10; }
|
||||
h1 { margin: 0 0 8px; font-size: 1.4rem; }
|
||||
.sub { color: var(--muted); font-size: .9rem; }
|
||||
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
||||
|
||||
/* Top grid for system */
|
||||
.grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
|
||||
.card { background: var(--card); border: 1px solid #1f2937; border-radius: 12px; padding: 16px; }
|
||||
.card h2 { margin: 0 0 8px; font-size: 1.1rem; }
|
||||
|
||||
.kpi { display:flex; align-items:center; gap: 16px; }
|
||||
.ring { --size: 84px; width: var(--size); height: var(--size); border-radius: 50%; position: relative; display:grid; place-items:center; background: conic-gradient(var(--accent) var(--deg), #334155 var(--deg)); }
|
||||
.ring::before { content:""; width: calc(var(--size) - 14px); height: calc(var(--size) - 14px); border-radius: 50%; background: var(--card); position: absolute; }
|
||||
.ring span { position: relative; font-weight: 700; }
|
||||
.legend { color: var(--muted); font-size: .9rem; }
|
||||
|
||||
.bar { height: 10px; background: #334155; border-radius: 999px; overflow: hidden; }
|
||||
.bar > i { display:block; height: 100%; background: var(--accent2); width: 0; }
|
||||
|
||||
/* Container list */
|
||||
.list { display:flex; flex-direction: column; gap: 12px; }
|
||||
.row { display:grid; grid-template-columns: 2fr 1fr 2fr 1fr 120px; gap: 12px; align-items:center; padding: 12px; border-radius: 10px; background: #0b1220; border: 1px solid #1f2937; }
|
||||
.row .name { font-weight: 600; }
|
||||
.row .muted { color: var(--muted); font-size: .9rem; }
|
||||
.status { padding: 2px 8px; border-radius: 999px; font-size: .8rem; display:inline-block; background: #064e3b; color: #a7f3d0; }
|
||||
.status.stopped { background:#3f0a0a; color:#fecaca; }
|
||||
|
||||
.ports { display:flex; flex-wrap: wrap; gap: 6px; }
|
||||
.port { padding: 4px 8px; border-radius: 999px; font-size: .8rem; background:#1f2937; color:#cbd5e1; border: 1px solid #334155; }
|
||||
.badge { padding: 2px 6px; border-radius: 6px; font-size: .8rem; background:#1f2937; border: 1px solid #334155; }
|
||||
|
||||
.toolbar { display:flex; gap: 12px; align-items:center; margin-bottom: 12px; }
|
||||
.toolbar input { background:#0b1220; border:1px solid #1f2937; color:var(--text); border-radius:8px; padding:8px 10px; }
|
||||
.toolbar button { background:#111827; border:1px solid #1f2937; color:var(--text); border-radius:8px; padding:8px 12px; cursor:pointer; }
|
||||
.toolbar button:hover { border-color:#334155; }
|
||||
|
||||
footer { color: var(--muted); padding: 24px; text-align:center; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
.small { font-size: .85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>System & Docker Status</h1>
|
||||
<div class="sub">Last update: <span id="last-update">–</span> · Auto-refresh <span id="refresh-state">on</span></div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="grid" id="system-grid">
|
||||
<div class="card" style="grid-column: span 4;">
|
||||
<h2>CPU</h2>
|
||||
<div class="kpi">
|
||||
<div class="ring" id="cpu-ring"><span id="cpu-percent">0%</span></div>
|
||||
<div>
|
||||
<div class="legend small">Load averages</div>
|
||||
<div class="small">1m: <span id="load-1m">–</span></div>
|
||||
<div class="small">5m: <span id="load-5m">–</span></div>
|
||||
<div class="small">15m: <span id="load-15m">–</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: span 8;">
|
||||
<h2>Memory</h2>
|
||||
<div class="kpi" style="gap: 24px;">
|
||||
<div style="min-width:220px">
|
||||
<div class="legend">Used</div>
|
||||
<div class="bar"><i id="mem-bar"></i></div>
|
||||
<div class="small"><span id="mem-used">–</span> / <span id="mem-total">–</span> (<span id="mem-percent">–</span>)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="legend">Tip</div>
|
||||
<div class="small">Keep memory usage under 80% to avoid OOM kills.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 24px;">
|
||||
<div class="card">
|
||||
<div class="toolbar">
|
||||
<input id="search" type="search" placeholder="Filter containers by name or image…" />
|
||||
<button id="toggle-refresh">Pause</button>
|
||||
<span class="small" id="container-count"></span>
|
||||
</div>
|
||||
<div class="list" id="container-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="small">This page reads <code>data/status.json</code>. Place your status file under <code>./data</code> (mounted to <code>/app/public/data</code>).</div>
|
||||
</footer>
|
||||
|
||||
<script src="js/status.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
``
|
||||
115
public/js/status.js
Executable file
115
public/js/status.js
Executable file
@@ -0,0 +1,115 @@
|
||||
|
||||
// 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();
|
||||
});
|
||||
Reference in New Issue
Block a user