minor fixes

This commit is contained in:
fishtank-dashboard
2026-03-17 15:15:30 -07:00
committed by GitHub
parent 2f50144fe7
commit 1c63a3c95f
2 changed files with 196 additions and 5 deletions

View File

@@ -735,6 +735,118 @@
font-size: 12px;
}
/* Camera reconnecting overlay */
.cam-reconnecting {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.7);
gap: 10px;
pointer-events: none;
z-index: 5;
}
.cam-reconnecting-spinner {
width: 24px;
height: 24px;
border: 2px solid rgba(255,255,255,0.15);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.cam-reconnecting-label {
font-family: 'Share Tech Mono', monospace;
font-size: 9px;
color: var(--muted);
letter-spacing: 1px;
}
.cam-featured-wrap .cam-reconnecting-spinner {
width: 36px;
height: 36px;
}
.cam-featured-wrap .cam-reconnecting-label {
font-size: 11px;
}
/* Viewer count badge */
.cam-viewers {
position: absolute;
top: 4px;
right: 4px;
background: rgba(0,0,0,0.65);
color: #fff;
font-family: 'Share Tech Mono', monospace;
font-size: 9px;
padding: 2px 5px;
border-radius: 3px;
pointer-events: none;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 3px;
}
.cam-viewers::before {
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent2);
flex-shrink: 0;
}
.cam-featured-wrap .cam-viewers {
top: 8px;
right: 8px;
font-size: 11px;
padding: 3px 8px;
}
/* Camera reconnecting overlay */
.cam-reconnecting {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.7);
gap: 10px;
pointer-events: none;
z-index: 5;
}
.cam-reconnecting-spinner {
width: 24px;
height: 24px;
border: 2px solid rgba(255,255,255,0.15);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.cam-reconnecting-label {
font-family: 'Share Tech Mono', monospace;
font-size: 9px;
color: var(--muted);
letter-spacing: 1px;
}
.cam-featured-wrap .cam-reconnecting-spinner {
width: 36px;
height: 36px;
}
.cam-featured-wrap .cam-reconnecting-label {
font-size: 11px;
}
/* Viewer count badge */
.cam-viewers {
position: absolute;
@@ -1103,6 +1215,8 @@
<div id="apiControl" style="display:flex;align-items:center;gap:8px;">
<div class="dot" id="apiDot" title="API status"></div>
<span id="apiStatus" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--muted);">NO TOKEN</span>
<div class="dot" id="wsDot" title="WebSocket status" style="margin-left:4px;"></div>
<span id="wsLabel" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--muted);">WS —</span>
<input id="inlineToken" type="password" placeholder="Paste token or full cookie value..." style="display:none;background:var(--panel);border:1px solid var(--accent);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:10px;padding:4px 10px;border-radius:4px;width:260px;outline:none;" onkeydown="if(event.key==='Enter')inlineConnect();if(event.key==='Escape')closeInlineToken();" />
<button id="linkApiBtn" onclick="toggleInlineToken()" style="background:transparent;border:1px solid var(--green);color:var(--green);font-family:'Share Tech Mono',monospace;font-size:10px;padding:4px 12px;border-radius:4px;cursor:pointer;letter-spacing:1px;">LINK API</button>
<button id="connectInlineBtn" onclick="inlineConnect()" style="display:none;background:var(--green);color:var(--bg);border:none;font-family:'Share Tech Mono',monospace;font-size:10px;padding:4px 12px;border-radius:4px;cursor:pointer;letter-spacing:1px;">CONNECT</button>
@@ -1115,10 +1229,6 @@
<option value="120000">2m</option>
<option value="300000">5m</option>
</select>
<div class="status-dot">
<div class="dot" id="wsDot"></div>
<span id="wsLabel" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--muted);">WS —</span>
</div>
<span id="lastUpdated" style="font-size:13px;color:#e2e8f0;font-family:'Share Tech Mono',monospace;"></span>
</div>
</header>
@@ -1756,6 +1866,22 @@
// directorCell = which grid index currently shows the Director stream (null = director is in featured)
let directorCell = null;
function showReconnecting(video) {
const cell = video.closest('.cam-cell') || video.closest('.cam-featured-wrap');
if (!cell || cell.querySelector('.cam-reconnecting')) return;
const overlay = document.createElement('div');
overlay.className = 'cam-reconnecting';
overlay.innerHTML = '<div class="cam-reconnecting-spinner"></div><div class="cam-reconnecting-label">RECONNECTING</div>';
cell.appendChild(overlay);
}
function hideReconnecting(video) {
const cell = video.closest('.cam-cell') || video.closest('.cam-featured-wrap');
if (!cell) return;
const overlay = cell.querySelector('.cam-reconnecting');
if (overlay) overlay.remove();
}
function makeHls(slug, video, muted, retryDelay) {
retryDelay = retryDelay || 2000;
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
@@ -1764,11 +1890,12 @@
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.muted = muted;
video.play().catch(() => {});
hideReconnecting(video);
});
hls.on(Hls.Events.ERROR, (e, d) => {
if (!d.fatal) return;
showReconnecting(video);
hls.destroy();
// Retry indefinitely with backoff — cap at 10s, keep trying forever
const nextDelay = Math.min(retryDelay * 1.5, 10000);
setTimeout(() => {
if (!video.isConnected) return;
@@ -1817,6 +1944,9 @@
if (typeof updateContestantOverlays === 'function' && window._lastContestantData && Object.keys(window._lastContestantData).length) {
updateContestantOverlays(window._lastContestantData);
}
if (typeof updateViewerCounts === 'function' && window._lastPresenceData) {
updateViewerCounts(window._lastPresenceData);
}
}
@@ -2474,6 +2604,10 @@
if (msg.event === 'contestants:cams') {
updateContestantOverlays(msg.data.data || msg.data);
}
if (msg.event === 'presence') {
window._lastPresenceData = msg.data;
updateViewerCounts(msg.data);
}
if (msg.event === 'poll:vote') {
// data is [{value, score}, ...] — update scores in cached poll
@@ -2599,6 +2733,45 @@
};
// ── End stocks panel collapse ─────────────────────────────────
// ── Viewer counts ────────────────────────────────────────────
window.updateViewerCounts = function(data) {
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman-5') : -1;
if (typeof CAMERAS !== 'undefined') {
CAMERAS.forEach(([name, slug], i) => {
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
if (i === cammanIdx) return;
const cell = document.getElementById('cam-' + i);
if (cell) setViewerBadge(cell, data[slug]);
});
}
const featWrap = document.getElementById('camFeaturedWrap');
if (featWrap && typeof CAMERAS !== 'undefined' && typeof featuredIdx !== 'undefined') {
const slug = CAMERAS[featuredIdx] ? CAMERAS[featuredIdx][1] : null;
const featCell = featWrap.querySelector('.cam-cell') || featWrap;
if (slug) {
const dirSlug = CAMERAS[typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1][1];
setViewerBadge(featCell, data[slug]);
}
}
};
function setViewerBadge(cell, count) {
let badge = cell.querySelector('.cam-viewers');
if (count === undefined || count === null || count === 0) {
if (badge) badge.remove();
return;
}
if (!badge) {
badge = document.createElement('div');
badge.className = 'cam-viewers';
cell.appendChild(badge);
}
badge.textContent = count >= 1000
? (count / 1000).toFixed(1) + 'k'
: count.toString();
}
// ── End viewer counts ─────────────────────────────────────────
// ── Contestant cam overlays ──────────────────────────────────
const AVATAR_CDN = 'https://cdn.fishtank.live/images/contestants/s5/';

View File

@@ -89,6 +89,22 @@ function sendSubscriptions() {
sendBinary(buildSubscribeFrame('presence'));
}
// Presence is client-driven — must re-request every 30s to get updated counts
let presenceTimer = null;
function startPresencePolling() {
if (presenceTimer) clearInterval(presenceTimer);
presenceTimer = setInterval(() => {
if (ftSocket && ftSocket.readyState === 1) {
sendBinary(buildSubscribeFrame('presence'));
}
}, 30000);
}
function stopPresencePolling() {
if (presenceTimer) { clearInterval(presenceTimer); presenceTimer = null; }
}
// ── Simple msgpack decoder (enough for fishtank events) ─────
function mpDecode(buf, offset = 0) {
if (offset >= buf.length) return [null, offset];
@@ -205,6 +221,7 @@ function handleBinaryFrame(buf) {
namespaceReady = true;
broadcast({ _ft: 'ws_status', status: 'connected' });
sendSubscriptions();
startPresencePolling();
return;
}
@@ -286,6 +303,7 @@ function connectFishtankWS(token) {
console.log(`[WS] Disconnected (${code} ${reason || ''}). Reconnecting in 5s...`);
broadcast({ _ft: 'ws_status', status: 'disconnected' });
namespaceReady = false;
stopPresencePolling();
reconnectTimer = setTimeout(() => connectFishtankWS(null), 5000);
});