You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
67 lines
2.6 KiB
67 lines
2.6 KiB
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Laundry status</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: #fff; color: #111; font-family: system-ui, sans-serif; }
|
|
main { max-width: 44rem; margin: 0 auto; padding: 3rem 1.25rem; text-align: center; }
|
|
h1 { margin: 1rem 0 2rem; font-size: clamp(3rem, 12vw, 5rem); line-height: 1.1; overflow-wrap: anywhere; }
|
|
.running { color: #b91c1c; }
|
|
.done { color: #1a7f37; }
|
|
.waiting, .incomplete, .unavailable { color: #666; }
|
|
.label { color: #666; font-size: 1rem; }
|
|
#readings { display: flex; flex-wrap: wrap; justify-content: center; gap: 1.25rem; padding: 0; list-style: none; color: #000; font-size: 1rem; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
#readings li { min-width: 0; max-width: 100%; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<p class="label">Laundry status</p>
|
|
<section aria-live="polite" aria-atomic="true">
|
|
<h1 id="status" class="waiting">Waiting for data...</h1>
|
|
<p class="label">Latest power readings · oldest first</p>
|
|
<ul id="readings" aria-label="Latest power readings"></ul>
|
|
</section>
|
|
<noscript>Enable JavaScript to see the current laundry status.</noscript>
|
|
</main>
|
|
<script>
|
|
const statusText = document.getElementById('status');
|
|
const readings = document.getElementById('readings');
|
|
let refreshing = false;
|
|
|
|
async function refresh() {
|
|
if (refreshing) return;
|
|
refreshing = true;
|
|
try {
|
|
const response = await fetch('/api/status', { cache: 'no-store', signal: AbortSignal.timeout(10000) });
|
|
if (!response.ok) throw new Error('Status request failed');
|
|
const data = await response.json();
|
|
const items = data.readings.map(reading => {
|
|
const item = document.createElement('li');
|
|
item.textContent = reading.valid ? reading.power.toFixed(2) + 'W' : '--';
|
|
return item;
|
|
});
|
|
statusText.textContent = data.message;
|
|
statusText.className = data.state;
|
|
readings.replaceChildren(...items);
|
|
} catch {
|
|
statusText.textContent = 'Status unavailable';
|
|
statusText.className = 'unavailable';
|
|
readings.replaceChildren();
|
|
} finally {
|
|
refreshing = false;
|
|
}
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 60000);
|
|
if (typeof EventSource !== 'undefined') {
|
|
const events = new EventSource('/api/events');
|
|
events.addEventListener('update', refresh);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|