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:
@@ -37,9 +37,11 @@ Defaults to the kotov vault copy. On cyrion use `deploy/build.sh`
|
||||
`docker compose -f deploy/docker-compose.yml up -d` serves `dist/` on
|
||||
port 18100 (`garden-next.c0smere.net` while staging).
|
||||
|
||||
## Homepage extras
|
||||
## Layout extras
|
||||
|
||||
`src/pages/index.astro` renders the vault's `index.md` and appends the
|
||||
bandwidth odometer, fed by `https://api.c0smere.net/bandwidth/odometer`.
|
||||
The widget animates at the lifetime average rate from a fetched baseline —
|
||||
it never reflects live throughput, and hides itself if the API is down.
|
||||
Every page carries a left sidebar tree of the whole garden (folders
|
||||
collapsible, current page highlighted; flows after the footer on narrow
|
||||
screens) and the bandwidth odometer in the footer, fed by
|
||||
`https://api.c0smere.net/bandwidth/odometer`. The widget animates at the
|
||||
lifetime average rate from a fetched baseline — it never reflects live
|
||||
throughput, and hides itself if the API is down.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,100 +10,4 @@ const title = home?.data.title ?? 'Wesley Ray';
|
||||
|
||||
<Base title={title} description={home?.data.description}>
|
||||
{Content && <Content />}
|
||||
|
||||
<section id="odometer" hidden>
|
||||
<h2>Bandwidth odometer</h2>
|
||||
<p class="odo-lede">
|
||||
Every byte in or out of the homelab since <span id="odo-since"></span>:
|
||||
</p>
|
||||
<div class="odo-display" id="odo-digits" aria-live="off"></div>
|
||||
<p class="odo-foot">
|
||||
ticking at the lifetime average rate · served by
|
||||
<a href="https://api.c0smere.net/docs">api.c0smere.net</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<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 section 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();
|
||||
// group into 3s for readability, each digit in its own cell
|
||||
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); // bytes/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: section stays hidden */
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#odometer {
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--faint);
|
||||
}
|
||||
.odo-lede {
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.odo-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
font-family: ui-monospace, 'Cascadia Mono', 'JetBrains Mono', monospace;
|
||||
font-size: clamp(1.1rem, 4vw, 1.8rem);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.odo-display :global(.odo-digit) {
|
||||
display: inline-block;
|
||||
min-width: 1.35ch;
|
||||
padding: 0.15em 0.1em;
|
||||
text-align: center;
|
||||
background: #1c1c1e;
|
||||
color: #f2f2f3;
|
||||
border-radius: 4px;
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
.odo-display :global(.odo-sep) {
|
||||
width: 0.35ch;
|
||||
}
|
||||
.odo-foot {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
</Base>
|
||||
|
||||
Reference in New Issue
Block a user