Move odometer into the shared footer
Widget + script + styles live in Base.astro now, so every page ticks; index.astro is back to just rendering the vault homepage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,14 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
<a href="https://git.c0smere.net/wes">git</a> ·
|
||||
<a href="https://resume.c0smere.net">resume</a>
|
||||
</p>
|
||||
<div id="odometer" hidden>
|
||||
<div class="odo-display" id="odo-digits" aria-live="off"></div>
|
||||
<p class="odo-caption">
|
||||
bytes in + out of the homelab since <span id="odo-since"></span> ·
|
||||
ticking at the lifetime average ·
|
||||
<a href="https://api.c0smere.net/docs">api</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<aside class="sidebar">
|
||||
@@ -48,6 +56,53 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Odometer: fetch totals once, then animate forward at the lifetime
|
||||
// average rate. Looks live, but deliberately never exposes current
|
||||
// throughput. Fail-closed: any error leaves the footer widget hidden.
|
||||
const API = 'https://api.c0smere.net/bandwidth/odometer';
|
||||
const section = document.getElementById('odometer')!;
|
||||
const digitsEl = document.getElementById('odo-digits')!;
|
||||
const sinceEl = document.getElementById('odo-since')!;
|
||||
|
||||
function renderDigits(total: bigint) {
|
||||
const s = total.toString();
|
||||
let html = '';
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (i > 0 && (s.length - i) % 3 === 0)
|
||||
html += '<span class="odo-sep"></span>';
|
||||
html += `<span class="odo-digit">${s[i]}</span>`;
|
||||
}
|
||||
digitsEl.innerHTML = html;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(API);
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const data = await res.json();
|
||||
const base = BigInt(data.total_bytes);
|
||||
const rate = Number(data.avg_bytes_per_sec);
|
||||
const asOf = Date.parse(data.as_of);
|
||||
if (!Number.isFinite(rate) || !Number.isFinite(asOf))
|
||||
throw new Error('bad payload');
|
||||
|
||||
sinceEl.textContent = new Date(data.since).toLocaleDateString(
|
||||
undefined,
|
||||
{ year: 'numeric', month: 'long', day: 'numeric' },
|
||||
);
|
||||
|
||||
const tick = () => {
|
||||
const elapsed = (Date.now() - asOf) / 1000;
|
||||
renderDigits(base + BigInt(Math.floor(elapsed * rate)));
|
||||
};
|
||||
tick();
|
||||
section.hidden = false;
|
||||
setInterval(tick, 100);
|
||||
} catch {
|
||||
/* API down or blocked: widget stays hidden */
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// mermaid: only load the (large) module on pages that need it
|
||||
const blocks = document.querySelectorAll('pre > code.language-mermaid');
|
||||
@@ -199,6 +254,31 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.odo-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
font-family: ui-monospace, 'Cascadia Mono', 'JetBrains Mono', monospace;
|
||||
font-size: 0.95rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.odo-display .odo-digit {
|
||||
display: inline-block;
|
||||
min-width: 1.3ch;
|
||||
padding: 0.12em 0.08em;
|
||||
text-align: center;
|
||||
background: #1c1c1e;
|
||||
color: #f2f2f3;
|
||||
border-radius: 3px;
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
.odo-display .odo-sep {
|
||||
width: 0.3ch;
|
||||
}
|
||||
.odo-caption {
|
||||
margin-top: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
Reference in New Issue
Block a user