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:
@@ -80,10 +80,16 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
|
||||
</div>
|
||||
{
|
||||
isHome && (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user