5.8 KiB · text History b85db9e
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="/app.css">
  <title>Device Status</title>
  <script>
    (() => {
      const SYSTEM_DARK = "afterdark";
      const SYSTEM_LIGHT = "her";
      const mq = window.matchMedia("(prefers-color-scheme: dark)");
      const systemTheme = () => (mq.matches ? SYSTEM_DARK : SYSTEM_LIGHT);
      window.portalSetTheme = (theme) => {
        if (theme === "system") {
          localStorage.removeItem("portal:theme");
          document.documentElement.setAttribute("data-theme", systemTheme());
        } else {
          localStorage.setItem("portal:theme", theme);
          document.documentElement.setAttribute("data-theme", theme);
        }
      };
      if (!document.documentElement.hasAttribute("data-theme")) {
        window.portalSetTheme(localStorage.getItem("portal:theme") || "system");
      }
      mq.addEventListener("change", () => {
        if (!localStorage.getItem("portal:theme")) window.portalSetTheme("system");
      });
    })();
  </script>
</head>
<body class="min-h-screen bg-base-100 text-base-content">
  <header class="navbar bg-base-200 border-b border-base-300 px-4 sm:px-6">
    <div class="flex-1">
      <span class="font-bold tracking-[0.2em] text-accent">DEVICE&nbsp;STATUS</span>
    </div>
    <div class="flex-none">
      <div class="dropdown dropdown-end">
        <div tabindex="0" role="button" class="btn btn-ghost btn-sm gap-2" aria-label="Choose theme">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none"
               stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/>
            <circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/>
            <circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/>
            <circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/>
            <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>
          </svg>
          <span class="hidden sm:inline">Theme</span>
        </div>
        <ul tabindex="0" class="dropdown-content menu bg-base-200 border border-base-300 rounded-box z-10 mt-1 w-44 p-1 shadow-lg">
          <li><button type="button" onclick="portalSetTheme('system')">System</button></li>
          <li>
            <button type="button" onclick="portalSetTheme('her')" class="flex items-center justify-between">
              <span>Her</span>
              <span class="size-3 rounded-full border border-base-content/20" data-theme="her" style="background: hsl(var(--p))"></span>
            </button>
          </li>
          <li>
            <button type="button" onclick="portalSetTheme('afterdark')" class="flex items-center justify-between">
              <span>Afterdark</span>
              <span class="size-3 rounded-full border border-base-content/20" data-theme="afterdark" style="background: hsl(var(--p))"></span>
            </button>
          </li>
        </ul>
      </div>
    </div>
  </header>

  <main class="px-4 py-10 sm:px-6">
    <div class="mx-auto max-w-md space-y-4">
      <div class="card bg-base-200 border border-base-300 shadow-lg">
        <div class="card-body gap-4">
          <h1 class="card-title text-accent">Status</h1>
          <!-- This page doubles as the Tailwind purge template for consumer
               status pages: the crate's CSS is purged against web/src/*.html,
               so a custom page passed to StatusServer::new is only styled for
               classes that appear here. Keep the vocabulary broad. -->
          <div class="text-xs opacity-70 space-y-1">
            <div>Device: <code id="dev">&hellip;</code></div>
          </div>
          <p class="text-xs opacity-60">last <span id="n">0</span> entries</p>
          <div class="overflow-x-auto">
            <table class="table table-sm">
              <thead>
                <tr><th>#</th><th>Key</th><th>Value</th></tr>
              </thead>
              <tbody id="rows">
                <tr><td colspan="3" class="opacity-60">loading&hellip;</td></tr>
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  </main>

  <script>
    // Generic renderer: shows whatever flat JSON /api/status returns. Firmware
    // that wants a richer view ships its own page and passes it to
    // StatusServer::new instead of using this one.
    async function refresh() {
      try {
        const r = await fetch('/api/status');
        const j = await r.json();
        const entries = Object.entries(j);
        document.getElementById('dev').textContent = j.id ?? '(unnamed)';
        document.getElementById('n').textContent = entries.length;
        const rows = document.getElementById('rows');
        rows.innerHTML = '';
        entries.forEach(([k, v], i) => {
          const tr = document.createElement('tr');
          const idx = document.createElement('td');
          idx.textContent = i + 1;
          const key = document.createElement('td');
          key.className = 'opacity-70';
          key.textContent = k;
          const val = document.createElement('td');
          val.className = 'font-mono';
          // textContent, not innerHTML: values are firmware-supplied and this
          // page has no way to know they are markup-safe.
          val.textContent = typeof v === 'object' ? JSON.stringify(v) : String(v);
          tr.append(idx, key, val);
          rows.appendChild(tr);
        });
      } catch (_) { /* transient fetch error: keep the last good view */ }
    }
    refresh();
    setInterval(refresh, 2000);
  </script>
</body>
</html>