Menu
Public repository / dc540296357442733693769e22cd6944f7c2a721
AkurAI-Mail / src/routes/(admin)/status/+page.svelte
Latest change dc540296357442733693769e22cd6944f7c2a721 - refactor: split admin UI into separate routes with shared sidebar layout by Ólafur Búi Ólafsson
<script lang="ts">
let { data } = $props();
let metrics = $derived(data.metrics);
let metricsHistory = $derived(data.metricsHistory);
let generatedAt = $derived(data.generatedAt);
let busy = $state(false);
function pct(value: number | undefined) {
return `${Math.round(value ?? 0)}%`;
}
function points(key: "cpu" | "memory" | "disk") {
const rows = metricsHistory ?? [];
if (rows.length === 0) return "0,54 260,54";
return rows.map((row: any, index: number) => {
const x = rows.length === 1 ? 260 : (index / (rows.length - 1)) * 260;
const y = 58 - Math.max(0, Math.min(100, Number(row[key] ?? 0))) * 0.52;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(" ");
}
async function refresh() {
const response = await fetch(location.href, { headers: { accept: "text/html" } });
if (response.ok) location.reload();
}
</script>
<svelte:head><title>VM Status - AkurAI Mail Admin</title></svelte:head>
<header class="topbar">
<div>
<p class="kicker">Infrastructure</p>
<h1>VM Status</h1>
<p>Last sample {new Date(generatedAt * 1000).toLocaleTimeString()}</p>
</div>
<button class="primary" onclick={refresh} disabled={busy}>Refresh</button>
</header>
<section class="panel">
<div class="charts">
<article>
<div><span>CPU</span><strong>{pct(metrics?.cpu)}</strong></div>
<svg viewBox="0 0 260 64" role="img" aria-label="CPU usage trend">
<path d="M0 58H260" />
<polyline points={points("cpu")} />
</svg>
</article>
<article>
<div><span>Memory</span><strong>{pct(metrics?.memory?.percent)}</strong></div>
<svg viewBox="0 0 260 64" role="img" aria-label="Memory usage trend">
<path d="M0 58H260" />
<polyline points={points("memory")} />
</svg>
<p>{metrics?.memory?.usedMb} MB / {metrics?.memory?.totalMb} MB</p>
</article>
<article>
<div><span>Disk</span><strong>{pct(metrics?.disk?.percent)}</strong></div>
<svg viewBox="0 0 260 64" role="img" aria-label="Disk usage trend">
<path d="M0 58H260" />
<polyline points={points("disk")} />
</svg>
<p>{metrics?.disk?.usedGb} GB / {metrics?.disk?.totalGb} GB</p>
</article>
</div>
</section>
<style>
.topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;min-width:0}
.kicker{margin:0 0 6px;color:#126b5a;font-size:13px;font-weight:800}.topbar h1{margin:0;font-size:30px;line-height:1.15;text-wrap:balance}.topbar p:last-child{margin:6px 0 0;color:#61716c;overflow-wrap:anywhere}
.primary{border:0;border-radius:6px;background:#126b5a;color:#fff;min-height:44px;padding:10px 15px;font-weight:800;cursor:pointer}.primary:hover{background:#0e594b}.primary:disabled{opacity:.55;cursor:not-allowed}
.panel{background:#fff;border:1px solid #dce4e1;border-radius:8px;min-width:0;padding:18px}
.charts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.charts article{border:1px solid #edf1ef;border-radius:8px;padding:14px;min-width:0}.charts div{display:flex;justify-content:space-between;gap:12px}.charts span{color:#61716c}.charts strong{font-size:24px}.charts svg{width:100%;height:76px;margin-top:8px}.charts path{stroke:#e1e8e5}.charts polyline{fill:none;stroke:#126b5a;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}.charts p{margin:6px 0 0;color:#61716c;font-size:13px}
@media(max-width:1050px){.charts{grid-template-columns:minmax(0,1fr)}}
@media(max-width:760px){.topbar{display:grid}.topbar h1{font-size:25px}.primary{width:100%}}
@media(max-width:420px){.panel{padding:15px}.topbar h1{font-size:23px}}
</style>