openvibers/openvibe.live Commits

Fix the hero stat board again, and stop a throw in it from going unnoticed

OP OpenVibers committed 16 Sept 2026 · bc166ed28e · 2 files, +15 −2

Second temporal dead zone in the same function: `const CC` was declared between the full board,
which is built first, and the headline strip. Both read it, so building the board threw and the
hero kept its loading placeholder forever. CC is declared with the other top-of-function locals
now, above every use.

Also wrapped the renderer so a throw can never silently leave a skeleton on screen again — it
logs and clears the placeholder instead of failing invisibly, which is why this took two
attempts to spot.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/index.html b/public/index.html
iindex fe09a01f..1b2ed5b8 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -5131,7 +5131,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/live-notify.js?v=3" defer></script>
<script src="/js/ov-skeleton.js?v=1" defer></script>
<script src="/js/ov-odometer.js?v=3" defer></script>
--<script src="/js/app.js?v=164" defer></script>
++<script src="/js/app.js?v=165" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex e78861f5..76e62760 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -1389,6 +1389,16 @@ function _heroCountUp(el, target) {
requestAnimationFrame(step);
}
function renderHeroStats(stats) {
++ try { _renderHeroStats(stats); }
++ catch (err) {
++ // A throw in here used to leave the hero showing a skeleton forever, with nothing in the
++ // console anyone would notice. Surface it and clear the placeholder instead.
++ console.error('[hero] stat board failed to render', err);
++ const w = document.getElementById('hero-stats');
++ if (w) w.innerHTML = '';
++ }
++}
++function _renderHeroStats(stats) {
const wrap = document.getElementById('hero-stats');
if (!wrap || !stats) return;
// One stat BOARD: every themed group is a full-width row (kicker on the left, chips
@@@ -1396,6 +1406,10 @@ function renderHeroStats(stats) {
// differently-sized islands. Chips keep short uniform labels (full meaning in the
// title tooltip) so a long label never dwarfs its number.
const R = stats.recent || {};
++ // Declared here, above every use: both the full board and the headline strip read it, and the
++ // board is built first. A const declared between them is a temporal dead zone, which is how
++ // this function came to render an empty board twice today.
++ const CC = stats.concurrency || {};
const groups = [];
// ── Right now ────────────────────────────────────────────────
@@@ -1530,7 +1544,6 @@ function renderHeroStats(stats) {
// Seven live numbers, three to a row. These are the ones that move — every other number on
// the board is an all-time total that changes once an hour at best, so it lives under the
// toggle. Each is keyed so the poller below can roll it to a new value without a re-render.
-- const CC = stats.concurrency || {};
const HEADLINE = [
{ key: 'liveNow', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', num: stats.liveNow, label: 'Live', title: 'Streams live right now', deltaHTML: _baselineDeltaHTML(stats.liveNow, CC.liveAvg24h, CC.livePeak24h) },
{ key: 'viewersNow', icon: 'fa-eye', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now', deltaHTML: _baselineDeltaHTML(stats.viewersNow, CC.viewersAvg24h, CC.viewersPeak24h) },