Homepage live strip: Knoebels line (live crowd verdict / best-day outlook)

Second row in the live strip fed by api.c0smere.net/knoebels/{now,outlook}
(knb-oracle pipeline): while the park is open, live avg wait + verdict with
a crowd-colored dot; while closed, the best upcoming day. Fail-closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wes
2026-07-22 10:05:32 -04:00
co-authored by Claude Fable 5
parent 97d302e9af
commit 01d7ce8aa8
+96 -3
View File
@@ -80,9 +80,15 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
</div>
{
isHome && (
<div id="live-stats" class="live-stats" hidden>
<span class="live-dot" aria-hidden="true" />
<span id="live-spotify" />
<div id="live-strips" class="live-strips" hidden>
<div id="live-stats" class="live-stats" hidden>
<span class="live-dot" aria-hidden="true" />
<span id="live-spotify" />
</div>
<div id="knb-stats" class="live-stats" hidden>
<span class="live-dot" id="knb-dot" aria-hidden="true" />
<span id="live-knb" />
</div>
</div>
)
}
@@ -185,12 +191,82 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
put(s.top_genre_month, true);
}
strip.hidden = false;
document.getElementById('live-strips')!.hidden = false;
} catch {
/* API down or blocked: strip stays hidden */
}
}
</script>
<script>
// Homepage Knoebels strip: live crowd verdict while the park is open
// (knb-oracle nowcast), next-best-day outlook when it's closed.
// Fail-closed: any error leaves the row hidden.
const knbRow = document.getElementById('knb-stats');
if (knbRow) {
try {
const API = 'https://api.c0smere.net/knoebels';
const slot = document.getElementById('live-knb')!;
const dot = document.getElementById('knb-dot')!;
const put = (text: string, strong = false) => {
const el = document.createElement('span');
if (strong) el.className = 'v';
el.textContent = text;
slot.append(el);
};
const dayName = (iso: string) => {
const [y, m, d] = iso.split('-').map(Number);
const when = new Date(y, m - 1, d);
const t = new Date();
const diff = Math.round(
(when.getTime() -
new Date(t.getFullYear(), t.getMonth(), t.getDate()).getTime()) /
86400000,
);
if (diff === 0) return 'today';
if (diff === 1) return 'tomorrow';
return when.toLocaleDateString('en-US', { weekday: 'long' });
};
const res = await fetch(`${API}/now`);
if (!res.ok) throw new Error(String(res.status));
const now = await res.json();
if (now.is_open && now.crowd_now != null) {
dot.classList.add(`knb-dot-${now.crowd_level ?? 'typical'}`);
put('knoebels right now: ');
put(`${now.crowd_now} min lines`, true);
put(` (${now.crowd_level})`);
if (now.verdict_label === 'jump-in-the-car') {
put(' · ');
put('jump in the car', true);
} else if (now.next_hours?.length) {
const last = now.next_hours[now.next_hours.length - 1];
put(` · likely ${last.level} through ${last.target_time}`);
}
} else if (!now.is_open) {
const ores = await fetch(`${API}/outlook`);
if (!ores.ok) throw new Error(String(ores.status));
const days = (await ores.json()).days.filter(
(d: any) => d.is_open && d.verdict_score != null,
);
if (!days.length) throw new Error('no outlook');
const best = days.reduce((a: any, b: any) =>
b.verdict_score > a.verdict_score ? b : a,
);
dot.classList.add('knb-dot-closed');
put('knoebels is closed · best day: ');
put(dayName(best.date), true);
put(` — ${best.crowd_level} crowds, ${best.hi_f}°F`);
} else {
throw new Error('open, no line data yet');
}
knbRow.hidden = false;
document.getElementById('live-strips')!.hidden = false;
} catch {
/* API down or park data missing: row stays hidden */
}
}
</script>
<script>
// Odometer: fetch totals once, then animate forward at the lifetime
// average rate. Looks live, but deliberately never exposes current
@@ -475,6 +551,23 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
.live-stats .v {
color: var(--fg);
}
.live-strips {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: -0.75rem 0 1.75rem;
padding-bottom: 0.85rem;
border-bottom: 1px solid var(--hair);
}
.live-strips .live-stats {
margin: 0;
padding-bottom: 0;
border-bottom: none;
}
.knb-dot-quiet { background: #2f9e44; }
.knb-dot-typical { background: #e8a13c; }
.knb-dot-packed { background: #d9480f; }
.knb-dot-closed { background: var(--muted); animation: none; }
.live-dot {
flex: none;
width: 7px;