AkurAI Build
Menu

Public repository / c3ef99b092caa55fb6a3eed9d3e776734cc99e97

AkurAI-Mail / src/routes/(admin)/+page.svelte

Latest change 22202d1bde69be4ace8a299dffd91f8ff24378df - Recover deployed webmail frontend (frontend parity with EC2 a6afffc) by Ólafur Búi Ólafsson

<script lang="ts">
  import { invalidateAll } from "$app/navigation";

  let { data } = $props();
  let status = $derived(data.status);
  let busy = $state(false);

  const serviceLabels: Record<string, string> = {
    postfix: "Postfix",
    dovecot: "Dovecot",
    opendkim: "OpenDKIM",
    nginx: "Nginx",
    "akurai-mail-ui": "Console",
    fail2ban: "Fail2ban"
  };

  async function refresh() {
    await invalidateAll();
  }
</script>

<svelte:head><title>AkurAI Mail Admin</title></svelte:head>

<header class="topbar">
  <div>
    <p class="kicker">Operations console</p>
    <h1>Mail platform control plane</h1>
    <p>{status.publicIp} / {status.domain}</p>
  </div>
  <button class="primary" onclick={refresh} disabled={busy}>Refresh</button>
</header>

<section class="service-strip" aria-label="Service health">
  {#each Object.entries(status.services) as [name, value]}
    <article class:ok={value === "active"}>
      <span>{serviceLabels[name] ?? name}</span>
      <strong>{value}</strong>
    </article>
  {/each}
</section>

<section class="split">
  <article class="panel">
    <div class="panel-head">
      <div>
        <h2>Mail queue</h2>
        <p>{status.queue?.summary ?? "Queue status unavailable"}</p>
      </div>
      <strong class="big">{status.queue?.count ?? 0}</strong>
    </div>
  </article>
  <article class="panel">
    <div class="panel-head">
      <div>
        <h2>Delivery errors</h2>
        <p>Recent Postfix warnings, rejects, and bounces</p>
      </div>
    </div>
    <div class="log-list">
      {#each status.deliveryErrors ?? [] as line}
        <code>{line}</code>
      {:else}
        <p class="empty">No delivery errors in the recent journal sample.</p>
      {/each}
    </div>
  </article>
</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}
  .service-strip{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px}
  .service-strip article,.panel{background:#fff;border:1px solid #dce4e1;border-radius:6px;min-width:0}
  .service-strip article{padding:12px}.service-strip span{display:block;color:#61716c;font-size:12px}.service-strip strong{display:block;margin-top:5px;color:#9a473a;font-size:15px}.service-strip article.ok strong{color:#126b5a}
  .panel{padding:18px}.panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:15px}.panel h2{margin:0;font-size:18px}.panel p{margin:4px 0 0;color:#61716c}.big{font-size:32px;color:#126b5a}.log-list{display:grid;gap:8px}.log-list code{background:#f6f8f7;border-radius:5px;padding:7px;overflow-wrap:anywhere}
  .split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}
  .empty{color:#61716c}
  @media(max-width:1050px){.service-strip{grid-template-columns:repeat(3,minmax(0,1fr))}.split{grid-template-columns:minmax(0,1fr)}}
  @media(max-width:760px){.topbar{display:grid}.topbar h1{font-size:25px}.primary{width:100%}.service-strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
  @media(max-width:420px){.service-strip{grid-template-columns:minmax(0,1fr)}.panel{padding:15px}.topbar h1{font-size:23px}}
</style>