SpinShare Referee Bot refbot.ellite.dev/overlay

update overlay

+185 -179
+177 -178
overlay/script.js
··· 4 4 5 5 const defaultWsUrl = (() => { 6 6 const scheme = location.protocol === 'https:' ? 'wss' : 'ws'; 7 - // In production behind Traefik (HTTPS), prefer connecting to the same host (no port): wss://refbot.ellite.dev 8 7 if (location.protocol === 'https:') return `${scheme}://${location.host}`; 9 - // In dev (HTTP) prefer an explicit WS port when provided (e.g. docker compose mapped port) 10 8 if (typeof window.WS_PORT !== 'undefined' && window.WS_PORT) return `${scheme}://${location.hostname}:${window.WS_PORT}`; 11 - // Fallback to localhost default for simple local dev 12 9 return `${scheme}://localhost:8080`; 13 10 })(); 14 11 ··· 17 14 connect(); 18 15 }); 19 16 20 - // track full pool so we can infer bans from shrinking currentMapPool 21 - let fullMapPool = []; 22 - // name -> chart data object, populated from any pool data we receive 23 - const chartDataCache = {}; 24 - // name of the currently picked/playing chart 17 + let mappool = []; 25 18 let currentChartName = null; 26 - // set of names that have been played (finished with a result) 27 - let playedChartNames = new Set(); 28 - // set of names currently in the active map pool (shrinks as bans happen) 29 - let activePoolNames = new Set(); 30 - // set of names that have been explicitly banned (from match.ban events) 31 - let bannedChartNames = new Set(); 32 - // whether a chart is actively being played right now 33 19 let chartIsLive = false; 34 - // ready state during ready check phase 35 20 let p1Ready = false; 36 21 let p2Ready = false; 37 22 38 - function entryName(e) { 39 - return typeof e === 'string' ? e : (e.csvName ?? e.displayName ?? e.name ?? '?'); 23 + function getEntry(csvName) { 24 + return mappool.find(e => e.csvName === csvName) ?? null; 40 25 } 41 26 42 27 function entryDisplay(e) { 43 - return typeof e === 'string' ? e : (e.title ?? e.displayName ?? e.name ?? '?'); 44 - } 45 - 46 - function cacheChartData(pool) { 47 - (pool ?? []).forEach(e => { 48 - if (typeof e === 'object') { 49 - const n = entryName(e); 50 - if (n && n !== '?') chartDataCache[n] = e; 51 - } 52 - }); 28 + if (!e) return '?'; 29 + if (typeof e === 'string') return e; 30 + return e.title ?? e.displayName ?? e.csvName ?? '?'; 53 31 } 54 32 55 33 function connect() { 56 34 if (ws) ws.close(); 57 35 const url = document.getElementById('ws-url').value.trim() || 'ws://localhost:8080'; 58 36 setStatus('connecting'); 59 - // If the page is served over HTTPS, force a secure WebSocket scheme. 60 37 let connectUrl = url; 61 38 if (location.protocol === 'https:' && connectUrl.startsWith('ws://')) { 62 39 connectUrl = connectUrl.replace(/^ws:/, 'wss:'); ··· 74 51 setStatus('connected'); 75 52 document.getElementById('waiting').innerHTML = '<div>Connected! Waiting for a match to start...</div><div class="sub">No match is currently in progress</div>'; 76 53 try { 77 - const httpPort = window.HTTP_PORT ?? '8081'; 78 - const res = await fetch(`http://${location.hostname}:${httpPort}/state`); 54 + const stateUrl = location.protocol === 'https:' 55 + ? `${location.origin}/state` 56 + : `http://${location.hostname}:${window.HTTP_PORT}/state`; 57 + const res = await fetch(stateUrl); 79 58 const snapshot = await res.json(); 80 59 if (snapshot) handleMessage(snapshot); 81 60 } 82 61 catch (error) { 83 - // Silently fail - initial state fetch is optional 84 62 console.debug('Failed to fetch initial state:', error); 85 63 } 86 64 }; ··· 108 86 dot.className = ''; 109 87 if (s === 'connected') { 110 88 dot.classList.add('connected'); 111 - // Start the hide timer when connected 112 89 hideHeaderAfterDelay(); 113 90 } 114 91 else if (s === 'error') { 115 92 dot.classList.add('error'); 116 - // Show header on error 117 93 showHeader(); 118 94 } 119 95 else { 120 - // Show header on disconnect 121 96 showHeader(); 122 97 } 123 98 label.textContent = s; ··· 126 101 function handleMessage({ event, data }) { 127 102 if (!data) return; 128 103 showMatchView(); 129 - cacheChartData(data.currentMapPool); 130 - cacheChartData(data.playedCharts); 131 - cacheChartData(data.fullMapPool); 104 + 105 + if (data.mappool?.length) mappool = data.mappool; 132 106 133 107 switch (event) { 134 108 case 'match.checkIn': ··· 140 114 addFeed('Check-in approved - match starting!', 'feed-pick'); 141 115 break; 142 116 143 - case 'match.snapshot': 144 - // restore full state on reconnect 145 - if (data.fullMapPool?.length) { 146 - fullMapPool = [...data.fullMapPool]; 147 - cacheChartData(fullMapPool); 148 - } 149 - if (data.bannedCharts) bannedChartNames = new Set(data.bannedCharts.map(b => typeof b === 'string' ? b : b.name)); 150 - playedChartNames = new Set((data.playedCharts ?? []).map(c => typeof c === 'string' ? c : c.name)); 151 - activePoolNames = new Set((data.currentMapPool ?? []).map(entryName)); 152 - currentChartName = data.currentChart ? entryName(data.currentChart) : null; 153 - chartIsLive = !!data.currentChart; 117 + case 'match.snapshot': { 118 + currentChartName = data.currentChart ?? null; 119 + chartIsLive = data.progressLevel === 'playing'; 120 + p1Ready = data.players?.[0]?.ready ?? false; 121 + p2Ready = data.players?.[1]?.ready ?? false; 122 + updateReadyState(); 154 123 updateScoreboard(data); 155 - if (data.currentChart) updateCurrentChart(data.currentChart); 124 + if (currentChartName) updateCurrentChart(getEntry(currentChartName)); 125 + else clearCurrentChart(); 156 126 renderMapPool(); 127 + updatePhaseBarFromState(data); 128 + showMatchView(); 129 + break; 130 + } 131 + 132 + case 'match.start': 133 + document.getElementById('checkin-banner').classList.remove('visible'); 134 + document.getElementById('end-banner').style.display = 'none'; 135 + currentChartName = null; 136 + chartIsLive = false; 137 + p1Ready = false; 138 + p2Ready = false; 139 + updateReadyState(); 140 + updateScoreboard(data); 141 + renderMapPool(); 142 + clearCurrentChart(); 143 + addFeed('Match started - ban phase beginning', 'feed-pick'); 157 144 showMatchView(); 158 145 break; 159 146 160 147 case 'match.banOrderDecided': { 161 - const firstBanner = data.currentBanner ?? data.players?.find(p => p.discordId === data.firstBannerDiscordId); 162 - const fbn = firstBanner?.displayName ?? firstBanner?.name ?? '?'; 148 + const firstBanner = data.players?.find(p => p.discordId === data.firstBannerDiscordId) 149 + ?? data.players?.find(p => p.discordId === data.banPhase?.currentBannerDiscordId); 150 + const fbn = firstBanner?.displayName ?? '?'; 163 151 addFeed(`${fbn} will ban first`, 'feed-ban'); 164 152 updatePhaseBar('banning', `${fbn} is banning...`); 165 153 updateScoreboard(data); ··· 168 156 } 169 157 170 158 case 'match.ban': { 171 - activePoolNames = new Set((data.currentMapPool ?? []).map(entryName)); 172 - if (data.bannedChart) bannedChartNames.add(data.bannedChart); 173 159 const banner = data.players?.find(p => p.discordId === data.bannedByDiscordId); 174 - const bannerName = banner?.displayName ?? banner?.name ?? 'Someone'; 175 - addFeed(`${bannerName} banned ${data.bannedChart}`, 'feed-ban'); 176 - const nextBanner = data.currentBanner; 177 - if (nextBanner?.discordId) { 178 - const nbn = nextBanner.displayName ?? nextBanner.name ?? '?'; 179 - updatePhaseBar('banning', `${nbn} is banning...`); 160 + const bannerName = banner?.displayName ?? 'Someone'; 161 + const bannedEntry = data.mappool?.find(e => e.csvName === data.bannedChart); 162 + const bannedDisplay = bannedEntry ? entryDisplay(bannedEntry) : data.bannedChart; 163 + addFeed(`${bannerName} banned ${bannedDisplay}`, 'feed-ban'); 164 + const nextBanner = data.players?.find(p => p.discordId === data.banPhase?.currentBannerDiscordId); 165 + if (nextBanner) { 166 + updatePhaseBar('banning', `${nextBanner.displayName} is banning...`); 180 167 } 181 168 else { 182 169 updatePhaseBar(null); ··· 186 173 break; 187 174 } 188 175 189 - case 'match.start': 190 - document.getElementById('checkin-banner').classList.remove('visible'); 191 - document.getElementById('end-banner').style.display = 'none'; 192 - fullMapPool = [...(data.fullMapPool?.length ? data.fullMapPool : (data.currentMapPool ?? []))]; 193 - cacheChartData(fullMapPool); 194 - activePoolNames = new Set((data.currentMapPool ?? []).map(entryName)); 195 - playedChartNames = new Set(); 196 - bannedChartNames = new Set(); 197 - currentChartName = null; 176 + case 'match.firstChartDetermined': { 177 + const entry = getEntry(data.chart ?? data.currentChart); 178 + currentChartName = data.chart ?? data.currentChart ?? null; 198 179 chartIsLive = false; 180 + p1Ready = false; 181 + p2Ready = false; 182 + updateReadyState(); 199 183 updateScoreboard(data); 184 + if (entry) updateCurrentChart(entry); 200 185 renderMapPool(); 201 - clearCurrentChart(); 202 - addFeed('Match started - ban phase beginning', 'feed-pick'); 203 - showMatchView(); 186 + addFeed(`Last map standing: ${entry ? entryDisplay(entry) : currentChartName}`, 'feed-pick'); 187 + updatePhaseBar('playing', `Ready check: ${entry ? entryDisplay(entry) : '...'}`); 204 188 break; 189 + } 205 190 206 191 case 'match.pickPhaseStart': { 207 - activePoolNames = new Set((data.currentMapPool ?? []).map(entryName)); 208 - const firstPicker = data.currentPicker; 209 - const fpn = firstPicker?.displayName ?? firstPicker?.name ?? '?'; 192 + const firstPicker = data.players?.find(p => p.discordId === data.currentPickerDiscordId); 193 + const fpn = firstPicker?.displayName ?? '?'; 210 194 updatePhaseBar('picking', `${fpn} is picking...`); 211 195 updateScoreboard(data); 212 196 renderMapPool(); ··· 216 200 } 217 201 218 202 case 'match.pick': { 219 - activePoolNames = new Set((data.currentMapPool ?? []).map(entryName)); 220 - currentChartName = data.currentChart ? entryName(data.currentChart) : null; 203 + currentChartName = data.currentChart ?? null; 221 204 chartIsLive = false; 222 205 p1Ready = false; 223 206 p2Ready = false; 224 207 updateReadyState(); 225 208 updateScoreboard(data); 226 - if (data.currentChart) updateCurrentChart(data.currentChart); 209 + const pickedEntry = currentChartName ? getEntry(currentChartName) : null; 210 + if (pickedEntry) updateCurrentChart(pickedEntry); 227 211 renderMapPool(); 228 - if (data.currentChart) { 212 + if (pickedEntry) { 229 213 const picker = data.players?.find(p => p.discordId === data.pickedByDiscordId); 230 - const pn = picker?.displayName ?? picker?.name ?? '?'; 231 - const cn = entryDisplay(data.currentChart); 232 - addFeed(`${pn} picked ${cn}`, 'feed-pick'); 214 + const pn = picker?.displayName ?? '?'; 215 + addFeed(`${pn} picked ${entryDisplay(pickedEntry)}`, 'feed-pick'); 233 216 } 234 - updatePhaseBar('playing', `Playing: ${data.currentChart ? entryDisplay(data.currentChart) : '...'}`); 217 + updatePhaseBar('playing', `Ready check: ${pickedEntry ? entryDisplay(pickedEntry) : '...'}`); 235 218 break; 236 219 } 237 220 238 - case 'match.playerReady': 239 - p1Ready = data.p1Ready ?? false; 240 - p2Ready = data.p2Ready ?? false; 221 + case 'match.playerReady': { 222 + const prevP1Ready = p1Ready; 223 + const prevP2Ready = p2Ready; 224 + p1Ready = data.players?.[0]?.ready ?? false; 225 + p2Ready = data.players?.[1]?.ready ?? false; 241 226 updateReadyState(); 242 - addFeed(`${data.p1Ready && !data.p2Ready 243 - ? (data.players?.[0]?.displayName ?? data.players?.[0]?.name ?? 'P1') 244 - : (data.players?.[1]?.displayName ?? data.players?.[1]?.name ?? 'P2')} is ready!`, 'feed-win'); 227 + const newlyReadyName = !prevP1Ready && p1Ready 228 + ? (data.players?.[0]?.displayName ?? 'P1') 229 + : !prevP2Ready && p2Ready 230 + ? (data.players?.[1]?.displayName ?? 'P2') 231 + : null; 232 + if (newlyReadyName) addFeed(`${newlyReadyName} is ready!`, 'feed-win'); 245 233 break; 234 + } 246 235 247 - case 'match.chartStart': 236 + case 'match.chartStart': { 248 237 p1Ready = true; 249 238 p2Ready = true; 250 239 updateReadyState(); 251 - // both players readied up 252 - currentChartName = data.currentChart ? entryName(data.currentChart) : currentChartName; 240 + currentChartName = data.currentChart ?? currentChartName; 253 241 chartIsLive = true; 254 242 updateScoreboard(data); 255 - if (data.currentChart) updateCurrentChart(data.currentChart); 243 + const liveEntry = currentChartName ? getEntry(currentChartName) : null; 244 + if (liveEntry) updateCurrentChart(liveEntry); 256 245 renderMapPool(); 257 - if (data.currentChart) { 258 - const n = entryDisplay(data.currentChart); 259 - addFeed(`Now playing: ${n}`, 'feed-pick'); 260 - } 246 + if (liveEntry) addFeed(`Now playing: ${entryDisplay(liveEntry)}`, 'feed-pick'); 247 + updatePhaseBar('playing', `Playing: ${liveEntry ? entryDisplay(liveEntry) : '...'}`); 261 248 break; 249 + } 262 250 263 251 case 'match.chartResult': { 264 252 chartIsLive = false; ··· 266 254 p2Ready = false; 267 255 updateReadyState(); 268 256 updateScoreboard(data); 269 - if (data.chart) playedChartNames.add(entryName(data.chart)); 270 - activePoolNames = new Set((data.currentMapPool ?? []).map(entryName)); 257 + const resultEntry = (data.mappool ?? []) 258 + .filter(e => e.status?.played && e.result) 259 + .sort((a, b) => new Date(b.status.playedAt) - new Date(a.status.playedAt))[0] ?? null; 271 260 currentChartName = null; 272 261 clearCurrentChart(); 273 262 renderMapPool(); 274 - const chartTitle = data.chart ? entryDisplay(data.chart) : 'Chart'; 275 - const p1n = data.players?.[0]?.displayName ?? data.players?.[0]?.name ?? 'P1'; 276 - const p2n = data.players?.[1]?.displayName ?? data.players?.[1]?.name ?? 'P2'; 277 - const s1 = fmtScore(data.score1, data.fc1, data.pfc1); 278 - const s2 = fmtScore(data.score2, data.fc2, data.pfc2); 279 - addFeed(`${chartTitle}: ${p1n} ${s1} vs ${p2n} ${s2} - ${data.winner} wins!`, 'feed-win'); 280 - const nextPicker = data.currentPicker; 281 - if (nextPicker?.discordId) { 282 - const npn = nextPicker.displayName ?? nextPicker.name ?? '?'; 283 - updatePhaseBar('picking', `${npn} is picking...`); 284 - } 263 + const chartTitle = resultEntry ? entryDisplay(resultEntry) : 'Chart'; 264 + const p1n = data.players?.[0]?.displayName ?? 'P1'; 265 + const p2n = data.players?.[1]?.displayName ?? 'P2'; 266 + const result = resultEntry?.result ?? {}; 267 + const s1 = fmtScore(result.score1, result.fc1, result.pfc1); 268 + const s2 = fmtScore(result.score2, result.fc2, result.pfc2); 269 + const winnerPlayer = data.players?.find(p => p.discordId === result.winnerDiscordId); 270 + addFeed(`${chartTitle}: ${p1n} ${s1} vs ${p2n} ${s2} - ${winnerPlayer?.displayName ?? 'someone'} wins!`, 'feed-win'); 271 + const nextPicker = data.players?.find(p => p.discordId === data.currentPickerDiscordId); 272 + if (nextPicker) updatePhaseBar('picking', `${nextPicker.displayName} is picking...`); 285 273 break; 286 274 } 287 275 288 276 case 'match.end': 289 277 chartIsLive = false; 278 + p1Ready = false; 279 + p2Ready = false; 280 + updateReadyState(); 290 281 updateScoreboard(data); 291 - if (data.chart) playedChartNames.add(entryName(data.chart)); 292 282 currentChartName = null; 293 283 clearCurrentChart(); 294 284 renderMapPool(); ··· 301 291 302 292 default: 303 293 updateScoreboard(data); 304 - if (data.currentChart) updateCurrentChart(data.currentChart); 305 - if (data.currentMapPool) activePoolNames = new Set(data.currentMapPool.map(entryName)); 294 + if (data.currentChart) { 295 + currentChartName = data.currentChart; 296 + updateCurrentChart(getEntry(currentChartName)); 297 + } 306 298 renderMapPool(); 307 299 } 308 300 } 309 301 302 + function updatePhaseBarFromState(data) { 303 + const level = data.progressLevel; 304 + if (level === 'ban-phase') { 305 + const banner = data.players?.find(p => p.discordId === data.banPhase?.currentBannerDiscordId); 306 + if (banner) updatePhaseBar('banning', `${banner.displayName} is banning...`); 307 + else updatePhaseBar(null); 308 + } 309 + else if (level === 'picking-post-result') { 310 + const picker = data.players?.find(p => p.discordId === data.currentPickerDiscordId); 311 + if (picker) updatePhaseBar('picking', `${picker.displayName} is picking...`); 312 + else updatePhaseBar(null); 313 + } 314 + else if (level === 'playing') { 315 + const entry = data.currentChart ? getEntry(data.currentChart) : null; 316 + updatePhaseBar('playing', `Playing: ${entry ? entryDisplay(entry) : '...'}`); 317 + } 318 + else { 319 + updatePhaseBar(null); 320 + } 321 + } 322 + 310 323 function fmtScore(score, fc, pfc) { 311 324 if (score == null) return '?'; 312 325 let s = Number(score).toLocaleString(); ··· 375 388 function updateScoreboard(data) { 376 389 if (data.players?.[0]) { 377 390 const p = data.players[0]; 378 - if (p.displayName ?? p.name) document.getElementById('p1-name').textContent = p.displayName ?? p.name; 379 - if (p.name) document.getElementById('p1-label').textContent = p.name; 380 - if (p.username) document.getElementById('p1-username').textContent = `@${p.username}`; 391 + if (p.displayName) document.getElementById('p1-name').textContent = p.displayName; 392 + if (p.discordDisplayName) document.getElementById('p1-label').textContent = p.discordDisplayName; 393 + if (p.discordUsername) document.getElementById('p1-username').textContent = `@${p.discordUsername}`; 381 394 const av = document.getElementById('p1-avatar'); 382 - if (p.avatar) { av.src = p.avatar; av.style.display = 'block'; } 395 + if (p.avatarUrl) { av.src = p.avatarUrl; av.style.display = 'block'; } 383 396 } 384 397 if (data.players?.[1]) { 385 398 const p = data.players[1]; 386 - if (p.displayName ?? p.name) document.getElementById('p2-name').textContent = p.displayName ?? p.name; 387 - if (p.name) document.getElementById('p2-label').textContent = p.name; 388 - if (p.username) document.getElementById('p2-username').textContent = `@${p.username}`; 399 + if (p.displayName) document.getElementById('p2-name').textContent = p.displayName; 400 + if (p.discordDisplayName) document.getElementById('p2-label').textContent = p.discordDisplayName; 401 + if (p.discordUsername) document.getElementById('p2-username').textContent = `@${p.discordUsername}`; 389 402 const av = document.getElementById('p2-avatar'); 390 - if (p.avatar) { av.src = p.avatar; av.style.display = 'block'; } 403 + if (p.avatarUrl) { av.src = p.avatarUrl; av.style.display = 'block'; } 391 404 } 392 - if (data.score) document.getElementById('score-display').textContent = `${data.score[0]} - ${data.score[1]}`; 393 - if (data.round) document.getElementById('round-label').textContent = data.round; 394 - if (data.bestOf) document.getElementById('bo-label').textContent = `Best of ${data.bestOf}`; 405 + const p0pts = data.players?.[0]?.points ?? 0; 406 + const p1pts = data.players?.[1]?.points ?? 0; 407 + document.getElementById('score-display').textContent = `${p0pts} - ${p1pts}`; 408 + if (data.meta?.round) document.getElementById('round-label').textContent = data.meta.round; 409 + if (data.meta?.bestOf) document.getElementById('bo-label').textContent = `Best of ${data.meta.bestOf}`; 395 410 } 396 411 397 - function updateCurrentChart(chart) { 398 - if (!chart) return; 412 + function updateCurrentChart(entry) { 413 + if (!entry) return; 399 414 const card = document.getElementById('current-chart-card'); 400 415 card.classList.add('active'); 401 416 const thumb = document.getElementById('chart-thumb'); 402 - const imgSrc = chart.thumbnailUrl ?? chart.cover ?? ''; 417 + const imgSrc = entry.thumbnailUrl ?? entry.cover ?? ''; 403 418 if (imgSrc) { thumb.src = imgSrc; thumb.style.display = 'block'; } 404 419 else { thumb.style.display = 'none'; } 405 - document.getElementById('chart-title').textContent = chart.title ?? chart.displayName ?? 'Unknown'; 406 - document.getElementById('chart-artist').textContent = chart.artist ?? ''; 407 - document.getElementById('chart-charter').textContent = chart.charter ? `charted by ${chart.charter}` : ''; 420 + document.getElementById('chart-title').textContent = entry.title ?? entry.displayName ?? 'Unknown'; 421 + document.getElementById('chart-artist').textContent = entry.artist ?? ''; 422 + document.getElementById('chart-charter').textContent = entry.charter ? `charted by ${entry.charter}` : ''; 408 423 const diff = document.getElementById('chart-difficulty'); 409 - if (chart.difficulty != null) { diff.style.display = 'block'; diff.textContent = `Diff ${chart.difficulty}`; } 424 + if (entry.difficulty != null) { diff.style.display = 'block'; diff.textContent = `Diff ${entry.difficulty}`; } 410 425 else { diff.style.display = 'none'; } 411 426 } 412 427 ··· 423 438 const grid = document.getElementById('mappool-grid'); 424 439 grid.innerHTML = ''; 425 440 426 - // use fullMapPool as the source of truth for what to show 427 - // fall back to activePoolNames contents if fullMapPool is empty (e.g. mid-session connect) 428 - const source = fullMapPool.length > 0 429 - ? fullMapPool 430 - : [...activePoolNames].map(n => chartDataCache[n] ?? n); 431 - 432 - source.forEach(entry => { 433 - const name = entryName(entry); 434 - const cached = chartDataCache[name] ?? (typeof entry === 'object' ? entry : null); 435 - const display = cached ? entryDisplay(cached) : name; 436 - const artist = cached?.artist ?? ''; 437 - const thumb = cached?.thumbnailUrl ?? cached?.cover ?? ''; 441 + if (!mappool.length) return; 438 442 439 - const isPlayed = playedChartNames.has(name); 440 - const isBanned = !isPlayed && (bannedChartNames.has(name) || (!activePoolNames.has(name) && fullMapPool.length > 0)); 441 - // during chart play, dim everything except the current chart 442 - const isDimmed = chartIsLive && currentChartName && name !== currentChartName; 443 - const isCurrent = name === currentChartName; 443 + mappool.forEach(entry => { 444 + const { csvName, title, artist, thumbnailUrl, cover, displayName, status, result } = entry; 445 + const display = title ?? displayName ?? csvName ?? '?'; 446 + const thumb = thumbnailUrl ?? cover ?? ''; 447 + const isBanned = status?.banned ?? false; 448 + const isPlayed = status?.played ?? false; 449 + const isLive = status?.isBeingPlayed ?? false; 450 + const isCurrent = csvName === currentChartName; 444 451 445 452 const chip = document.createElement('div'); 446 - const classes = ['map-chip']; 447 - if (isPlayed) classes.push('played'); 448 - else if (isBanned) classes.push('banned'); 449 - else if (isDimmed) classes.push('dimmed'); 450 - if (isCurrent) classes.push('current'); 451 - chip.className = classes.join(' '); 453 + chip.className = 'map-chip'; 454 + 455 + if (isBanned) chip.classList.add('banned'); 456 + else if (isPlayed) chip.classList.add('played'); 457 + else if (isLive || isCurrent) chip.classList.add('active'); 452 458 453 459 if (thumb) { 454 460 const img = document.createElement('img'); 455 461 img.className = 'map-chip-thumb'; 456 462 img.src = thumb; 457 - img.alt = display; 458 463 chip.appendChild(img); 459 464 } 460 465 461 - if (isBanned) { 462 - const tag = document.createElement('div'); 463 - tag.className = 'chip-tag banned-tag'; 464 - tag.textContent = 'BANNED'; 465 - chip.appendChild(tag); 466 + if (isPlayed && result) { 467 + const res = document.createElement('div'); 468 + res.className = 'map-chip-result'; 469 + const s1 = fmtScore(result.score1, result.fc1, result.pfc1); 470 + const s2 = fmtScore(result.score2, result.fc2, result.pfc2); 471 + res.textContent = `${s1} / ${s2}`; 472 + chip.appendChild(res); 466 473 } 467 - else if (isPlayed) { 474 + else if (isBanned) { 468 475 const tag = document.createElement('div'); 469 - tag.className = 'chip-tag played-tag'; 470 - tag.textContent = 'PLAYED'; 476 + tag.className = 'map-chip-tag'; 477 + tag.textContent = 'BANNED'; 471 478 chip.appendChild(tag); 472 479 } 473 - else if (isCurrent) { 480 + else if (isLive || isCurrent) { 474 481 const tag = document.createElement('div'); 475 - tag.className = 'chip-tag current-tag'; 482 + tag.className = 'map-chip-tag'; 476 483 tag.textContent = chartIsLive ? 'LIVE' : 'PICKED'; 477 484 chip.appendChild(tag); 478 485 } ··· 510 517 } 511 518 512 519 function hideHeaderAfterDelay() { 513 - if (headerHideTimeout) { 514 - clearTimeout(headerHideTimeout); 515 - } 520 + if (headerHideTimeout) clearTimeout(headerHideTimeout); 516 521 headerHideTimeout = setTimeout(() => { 517 522 const header = document.getElementById('main-header'); 518 523 const wsDot = document.getElementById('ws-dot'); 519 - // Only hide if connected 520 - if (wsDot.classList.contains('connected')) { 521 - header.classList.add('hidden'); 522 - } 524 + if (wsDot.classList.contains('connected')) header.classList.add('hidden'); 523 525 }, 1000); 524 526 } 525 527 526 528 function showHeader() { 527 529 const header = document.getElementById('main-header'); 528 530 header.classList.remove('hidden'); 529 - // Reset the hide timer 530 531 hideHeaderAfterDelay(); 531 532 } 532 - // Add mouse move listener to show header when mouse moves near top 533 + 533 534 document.addEventListener('mousemove', (e) => { 534 - if (e.clientY < 50) { 535 - showHeader(); 536 - } 537 - }); 535 + if (e.clientY < 50) showHeader(); 536 + });
+8 -1
state/http.js
··· 63 63 }; 64 64 res.setHeader('Content-Type', contentTypes[ext] ?? 'application/octet-stream'); 65 65 res.writeHead(200); 66 - fs.createReadStream(overlayPath).pipe(res); 66 + if (ext === '.html') { 67 + const html = fs.readFileSync(overlayPath, 'utf8'); 68 + const injected = html.replace('<head>', `<head>\n <script>window.WS_PORT=${process.env.WS_PORT ?? 8080};window.HTTP_PORT=${process.env.HTTP_PORT ?? 8081};</script>`); 69 + res.end(injected); 70 + } 71 + else { 72 + fs.createReadStream(overlayPath).pipe(res); 73 + } 67 74 return; 68 75 } 69 76