<!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 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: var(--color-primary)"></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: var(--color-primary)"></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">…</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…</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>
neiam
/neiam-portal
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.8 KiB · text
History
7f2cedd