AkurAI Build
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>