Momentum Tracker
:root{–bg:#0a0e1a;–card:#111827;–card2:#1a2235;–border:#1e2d45;–accent:#f97316;–accent2:#3b82f6;–green:#22c55e;–red:#ef4444;–yellow:#eab308;–text:#f1f5f9;–muted:#64748b;}
*{box-sizing:border-box;margin:0;padding:0;}
body{background:var(–bg);color:var(–text);font-family:’Segoe UI’,system-ui,sans-serif;min-height:100vh;padding:16px;}
header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(–border);}
.logo{display:flex;align-items:center;gap:10px;font-size:1.4rem;font-weight:700;}
.logo span{color:var(–accent);}
.status-bar{display:flex;align-items:center;gap:16px;font-size:0.8rem;color:var(–muted);}
.live-dot{width:8px;height:8px;background:var(–green);border-radius:50%;display:inline-block;}
50%{opacity:0.3}}
.refresh-btn{background:var(–card2);border:1px solid var(–border);color:var(–text);padding:6px 14px;border-radius:6px;cursor:pointer;font-size:0.8rem;}
.config-bar{background:var(–card);border:1px solid var(–border);border-radius:10px;padding:12px 16px;margin-bottom:16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
.config-input{background:var(–bg);border:1px solid var(–border);color:var(–text);padding:6px 10px;border-radius:6px;font-size:0.78rem;flex:1;min-width:200px;font-family:monospace;}
.save-btn{background:var(–accent);border:none;color:white;padding:6px 16px;border-radius:6px;cursor:pointer;font-size:0.78rem;font-weight:600;}
.main-layout{display:flex;gap:20px;align-items:flex-start;}
.games-col{width:310px;flex-shrink:0;}
.dash-col{flex:1;min-width:0;}
.section-title{font-size:0.7rem;font-weight:600;text-transform:uppercase;letter-spacing:1.5px;color:var(–muted);margin-bottom:10px;}
.games-list{display:flex;flex-direction:column;gap:8px;}
.game-card{border-radius:10px;padding:11px 13px;cursor:pointer;transition:border-color 0.2s;border:1px solid var(–border);background:var(–card);}
.game-card:hover,.game-card.active{border-color:var(–accent);}
.game-card.is-upcoming{border-style:dashed;opacity:0.85;}
.game-card.is-final{opacity:0.65;}
.game-card.card-upset{background:#0b2218;border-color:#22c55e !important;}
.game-card.card-underdog{background:#211c00;border-color:#eab308 !important;}
.card-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:7px;}
.status-pill{font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.8px;padding:2px 6px;border-radius:4px;flex-shrink:0;}
.sp-live{background:rgba(34,197,94,0.15);color:var(–green);}
.sp-halftime{background:rgba(234,179,8,0.15);color:var(–yellow);}
.sp-final{background:rgba(100,116,139,0.15);color:var(–muted);}
.sp-upcoming{background:rgba(59,130,246,0.15);color:var(–accent2);}
.card-clock{font-size:1rem;font-weight:900;color:#fff;}
.card-clock.halftime{color:var(–yellow);}
.card-clock.final{color:var(–muted);font-size:0.75rem;font-weight:400;}
.card-clock.upcoming{color:var(–accent2);font-size:0.72rem;font-weight:600;}
.tourn-banner{font-size:0.62rem;font-weight:600;color:var(–accent);margin-bottom:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.team-row{display:flex;align-items:center;gap:5px;padding:4px 0;}
.team-row+.team-row{border-top:1px solid rgba(30,45,69,0.7);}
.t-badge{min-width:22px;text-align:center;flex-shrink:0;font-size:0.72rem;}
.t-badge.rank-b{font-weight:900;color:#fff;}
.t-badge.seed-b{font-weight:400;color:var(–accent);}
.t-info{flex:1;min-width:0;}
.t-nameline{display:flex;align-items:center;gap:4px;}
.t-name{font-size:0.73rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.t-fav{font-size:0.75rem;flex-shrink:0;}
.t-rec{font-size:0.6rem;color:var(–muted);margin-top:1px;}
.t-score{font-size:1.25rem;font-weight:900;min-width:34px;text-align:right;flex-shrink:0;}
.s-win{color:var(–green);}
.s-tie{color:var(–accent2);}
.s-lose{color:var(–text);}
.card-footer{margin-top:7px;padding-top:6px;border-top:1px solid rgba(30,45,69,0.7);font-size:0.62rem;}
.game-location{color:var(–muted);margin-bottom:3px;}
.card-alerts{display:flex;flex-wrap:wrap;gap:4px;justify-content:space-between;align-items:center;margin-top:2px;}
.line-str{color:var(–accent);font-weight:700;}
.close-alert{color:var(–red);font-weight:700;}
.upset-str{color:var(–green);font-weight:700;}
.udog-str{color:var(–yellow);font-weight:700;}
50%{opacity:0.15}}
.dashboard{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.card{background:var(–card);border:1px solid var(–border);border-radius:12px;padding:16px;}
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;}
.card-title{font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(–muted);}
.badge-live{font-size:0.65rem;padding:2px 8px;border-radius:20px;font-weight:600;background:rgba(34,197,94,0.15);color:var(–green);}
.badge-foul{font-size:0.65rem;padding:2px 8px;border-radius:20px;font-weight:600;background:rgba(239,68,68,0.15);color:var(–red);}
.full-width{grid-column:1/-1;}
/* Scoreboard */
.scoreboard-inner{display:grid;grid-template-columns:1fr auto 1fr;gap:20px;align-items:center;}
.team-block{text-align:center;}
.seed-badge{display:inline-block;background:rgba(249,115,22,0.15);color:var(–accent);border:1px solid rgba(249,115,22,0.3);font-size:0.65rem;font-weight:700;padding:1px 7px;border-radius:4px;margin-bottom:4px;}
.rank-badge{display:inline-block;background:var(–accent);color:white;font-size:0.65rem;font-weight:700;padding:2px 7px;border-radius:4px;margin-bottom:6px;}
.team-name-lg{font-size:1.1rem;font-weight:800;margin-bottom:2px;}
.team-record{font-size:0.7rem;color:var(–muted);margin-bottom:6px;}
.big-score{font-size:3.5rem;font-weight:900;line-height:1;}
.team-icons{font-size:1rem;margin-top:6px;min-height:20px;}
.bonus-pill{display:inline-block;font-size:0.6rem;font-weight:700;padding:2px 8px;border-radius:10px;margin-top:5px;}
.bonus-single{background:rgba(234,179,8,0.2);color:var(–yellow);border:1px solid rgba(234,179,8,0.4);}
.bonus-double{background:rgba(239,68,68,0.2);color:var(–red);border:1px solid rgba(239,68,68,0.4);}
.clock-block{text-align:center;}
.clock{font-size:1.8rem;font-weight:800;color:var(–accent);}
.period-lbl{font-size:0.7rem;color:var(–muted);margin-top:4px;font-weight:600;text-transform:uppercase;}
/* Scoring chart */
.legend-dot{width:10px;height:3px;border-radius:2px;display:inline-block;margin-right:5px;vertical-align:middle;}
/* Report Card / Four Factors */
.ff-grid{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1px solid var(–border);border-radius:10px;overflow:hidden;}
.ff-team-header{padding:10px 14px;font-size:0.75rem;font-weight:700;border-bottom:1px solid var(–border);}
.ff-rows{padding:0 14px 10px;}
.ff-row{display:grid;grid-template-columns:1fr auto;align-items:center;padding:9px 0;border-bottom:1px solid rgba(30,45,69,0.5);}
.ff-row:last-child{border-bottom:none;}
.ff-factor-name{font-size:0.65rem;color:var(–muted);text-transform:uppercase;letter-spacing:0.8px;margin-bottom:3px;}
.ff-factor-desc{font-size:0.58rem;color:rgba(100,116,139,0.7);}
.ff-val{font-size:1.4rem;font-weight:900;text-align:right;}
.ff-bar-wrap{margin-top:5px;}
.ff-bar-bg{height:5px;background:rgba(255,255,255,0.07);border-radius:3px;overflow:hidden;}
.ff-bar-fill{height:100%;border-radius:3px;transition:width 0.7s ease;}
.ff-winner{font-size:0.58rem;font-weight:700;margin-top:3px;text-align:right;}
.ff-divider{width:1px;background:var(–border);}
.ff-legend{display:flex;gap:16px;font-size:0.62rem;color:var(–muted);margin-top:10px;padding:0 4px;}
.ff-legend span{display:flex;align-items:center;gap:4px;}
.ff-score-bar{height:6px;border-radius:3px;margin-top:8px;}
/* Top scorers */
.scorers-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.scorers-team{background:var(–card2);border-radius:10px;padding:12px 14px;border:1px solid var(–border);}
.scorers-team-name{font-size:0.72rem;font-weight:700;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(–border);}
.scorer-row{display:flex;align-items:center;gap:8px;padding:7px 0;border-bottom:1px solid rgba(30,45,69,0.5);}
.scorer-row:last-child{border-bottom:none;}
.scorer-rank{font-size:0.9rem;font-weight:900;color:var(–muted);min-width:18px;}
.scorer-info{flex:1;min-width:0;}
.scorer-name-line{font-size:0.75rem;font-weight:700;display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-bottom:2px;}
.scorer-shot-line{font-size:0.62rem;color:var(–muted);}
.scorer-pace-line{font-size:0.62rem;margin-top:2px;}
.starter-tag{font-size:0.58rem;color:var(–accent2);font-weight:600;}
.inj-alert{font-size:0.62rem;color:var(–red);font-weight:700;}
.scorer-pts{font-size:1.4rem;font-weight:900;}
.pace-hot{color:var(–green);}
.pace-cold{color:var(–red);}
.pace-avg{color:var(–muted);}
/* Players table */
.players-table{width:100%;border-collapse:collapse;font-size:0.75rem;}
.players-table th{text-align:center;font-size:0.62rem;font-weight:600;color:var(–muted);text-transform:uppercase;padding:0 4px 8px;border-bottom:1px solid var(–border);}
.players-table th:first-child{text-align:left;}
.players-table td{padding:6px 4px;border-bottom:1px solid rgba(30,45,69,0.5);text-align:center;}
.players-table td:first-child{text-align:left;}
.players-table tr:last-child td{border-bottom:none;}
.player-name{font-weight:600;}
.player-pos{font-size:0.62rem;color:var(–muted);}
.foul-badge{display:inline-block;padding:1px 5px;border-radius:4px;font-size:0.68rem;font-weight:700;}
.foul-danger{background:rgba(239,68,68,0.2);color:var(–red);}
.foul-warning{background:rgba(234,179,8,0.2);color:var(–yellow);}
.foul-ok{background:rgba(34,197,94,0.1);color:var(–green);}
/* Injury */
.injury-list{display:flex;flex-direction:column;gap:8px;}
.injury-row{display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(–card2);border-radius:8px;border:1px solid var(–border);}
.injury-status{font-size:0.65rem;font-weight:700;padding:2px 7px;border-radius:4px;white-space:nowrap;}
.inj-out{background:rgba(239,68,68,0.2);color:var(–red);}
.inj-questionable{background:rgba(234,179,8,0.2);color:var(–yellow);}
.inj-probable{background:rgba(34,197,94,0.1);color:var(–green);}
.inj-dtd{background:rgba(249,115,22,0.2);color:var(–accent);}
/* Info */
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.info-block{background:var(–card2);border-radius:8px;padding:10px 12px;}
.info-label{font-size:0.62rem;color:var(–muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;}
.info-value{font-size:0.95rem;font-weight:700;}
.h2h-row{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid rgba(30,45,69,0.5);font-size:0.78rem;}
.h2h-row:last-child{border-bottom:none;}
.loading{display:flex;align-items:center;justify-content:center;height:60px;color:var(–muted);font-size:0.8rem;gap:8px;}
.spinner{width:16px;height:16px;border:2px solid var(–border);border-top-color:var(–accent);border-radius:50%;animation:spin 0.8s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
.error-msg{background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:8px;padding:10px 14px;font-size:0.78rem;color:var(–red);margin-bottom:14px;}
.no-game{grid-column:1/-1;text-align:center;padding:40px;color:var(–muted);font-size:0.85rem;}
.game-card{cursor:pointer;padding:10px 12px;border-radius:8px;background:var(–card);border:1px solid var(–border);transition:border-color .15s;margin-bottom:6px}
.game-card:hover,.game-card.selected{border-color:var(–accent)}
.team-row{display:flex;align-items:center;justify-content:space-between;padding:3px 0}
.t-info{display:flex;flex-direction:column;gap:1px;flex:1;min-width:0}
.t-name{font-size:13px;font-weight:600;display:flex;align-items:center;gap:3px;overflow:hidden}
.t-rec{font-size:10px;color:var(–muted)}
.t-score{font-size:16px;font-weight:700;min-width:32px;text-align:right;padding-left:8px}
.t-fav{font-size:11px}.t-seed{font-size:10px;background:rgba(255,255,255,.1);border-radius:3px;padding:0 4px}
.home-icon{font-size:12px;opacity:.8}.s-win{color:var(–green)}.s-lose{color:var(–muted)}
.card-bottom{display:flex;justify-content:space-between;align-items:center;margin-top:6px;padding-top:5px;border-top:1px solid rgba(255,255,255,.06)}
.tourn-name{font-size:10px;color:var(–accent);font-weight:600}.venue-loc{font-size:10px;color:var(–muted)}
.card-odds{font-size:11px;color:var(–yellow);margin-top:3px}
.card-clock{font-size:11px;color:var(–muted);margin-top:2px;display:block}.card-clock.live{color:var(–green);font-weight:600}
.momentum-alert{padding:7px 10px;border-radius:6px;font-size:12px;cursor:pointer;margin-bottom:5px;display:block}
.momentum-alert:hover{opacity:.8}
.alert-forming{background:rgba(234,179,8,.12);border:1px solid rgba(234,179,8,.35);color:#fbbf24}
.alert-confirmed{background:rgba(239,68,68,.13);border:1px solid rgba(239,68,68,.45);color:#f87171}
.alert-game{opacity:.6;font-size:11px;margin-left:6px}
.momentum-monitoring{animation:m-pulse 1.8s ease-in-out infinite;display:inline-block}
@keyframes m-pulse{0%,100%{opacity:.35}50%{opacity:1}}
.ff-table{width:100%;border-collapse:collapse;font-size:12px}
.ff-table th{color:var(–muted);font-weight:600;padding:4px 6px;font-size:11px;text-align:center}
.ff-table td{padding:5px 6px}.ff-label{color:var(–muted);font-size:11px;text-align:center}
.ff-cell{text-align:center;color:var(–text)}
.ff-win{background:rgba(34,197,94,.18);border-radius:5px;color:#4ade80;font-weight:700}
.ff-grade{font-size:10px;border-radius:3px;padding:1px 4px;margin-left:3px}
.arr-up{color:#4ade80;font-size:10px;margin-left:2px}.arr-dn{color:#f87171;font-size:10px;margin-left:2px}
.scorers-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.scorers-col{display:flex;flex-direction:column;gap:3px}
.scorers-team-hdr{font-size:10px;font-weight:700;color:var(–muted);text-transform:uppercase;letter-spacing:.07em;padding-bottom:4px;border-bottom:1px solid var(–border);margin-bottom:3px}
.scorer-row{display:flex;align-items:center;gap:5px;font-size:12px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.03)}
.scorer-rank{color:var(–muted);font-size:10px;min-width:12px}
.scorer-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.scorer-pts{color:var(–accent);font-weight:700;min-width:44px;text-align:right;font-size:11px}
.scorer-reb,.scorer-ast{color:var(–muted);min-width:38px;text-align:right;font-size:11px}
.scorer-pts small,.scorer-reb small,.scorer-ast small{font-size:9px;opacity:.55}
.foul-section{margin-top:12px;padding-top:8px;border-top:1px solid var(–border)}
.section-sub-hdr{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:0 0 5px 0}
.foul-hdr{color:#f87171}
.foul-row{display:flex;align-items:center;gap:8px;font-size:12px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)}
.foul-name{flex:1;font-weight:500}.foul-count{font-weight:700;font-size:11px;padding:2px 7px;border-radius:4px}
.foul-danger{background:rgba(234,179,8,.2);color:#fbbf24}.foul-out{background:rgba(239,68,68,.22);color:#f87171}
.foul-stats{color:var(–muted);font-size:11px}
#gearBtn{opacity:.6}#gearBtn:hover{opacity:1}
π Momentum Tracker
Live
β
β» Refresh ⚙
RapidAPI Key:
Save & Load
π Select a game to load the dashboard
var allEvents = [];
var _alertSeen = {};
const HOST=’sports-information.p.rapidapi.com’;
let KEY=localStorage.getItem(‘mt_key’)||”;
let selGameId=null,autoTimer=null,selEvt=null,injuryData=[];
const momentumAlerts={};
// Snapshot store: { [gameId]: { away: {t0,t5,…}, home: {t0,t5,…} } }
// Keys are minutes elapsed: 0,5,10,15,20,25,30,35,40
const ffSnapshots={};
const FF_INTERVALS=[0,5,10,15,20,25,30,35,40];
function getSnapKey(mins){ // returns the interval label this reading belongs to
for(let i=FF_INTERVALS.length-1;i>=0;i–){if(mins>=FF_INTERVALS[i])return FF_INTERVALS[i];}
return 0;
}
function saveSnapshot(gameId,mins,awayFF,homeFF){
if(!gameId)return;
if(!ffSnapshots[gameId])ffSnapshots[gameId]={away:{},home:{}};
const key=getSnapKey(mins);
// Only save once per interval (don’t overwrite)
if(ffSnapshots[gameId].away[key]===undefined){
ffSnapshots[gameId].away[key]={eFG:awayFF.eFG,tovPct:awayFF.tovPct,orebPct:awayFF.orebPct,ftRate:awayFF.ftRate};
ffSnapshots[gameId].home[key]={eFG:homeFF.eFG,tovPct:homeFF.tovPct,orebPct:homeFF.orebPct,ftRate:homeFF.ftRate};
}
}
window.addEventListener(‘load’,()=>{
renderMomentumAlerts();if(KEY){document.getElementById(‘apiKeyInput’).value=KEY;loadGames();loadInjuries();}});
function saveApiKey(){KEY=document.getElementById(‘apiKeyInput’).value.trim();localStorage.setItem(‘mt_key’,KEY);loadGames();loadInjuries();}
function refreshAll(){loadGames();if(selGameId)loadGameDetails(selGameId);}
function showErr(m){const b=document.getElementById(‘errorBox’);b.style.display=’block’;b.textContent=’β οΈ ‘+m;setTimeout(()=>b.style.display=’none’,8000);}
async function api(path){const r=await fetch(‘
https://’+HOST+path ,{headers:{‘x-rapidapi-host’:HOST,’x-rapidapi-key’:KEY}});if(!r.ok)throw new Error(‘API ‘+r.status);return r.json();}
function getRec(team,type){return team?.records?.find(r=>r.type===type)?.summary||”;}
function winPct(rec){if(!rec)return 0.5;const p=rec.split(‘-‘);const w=parseInt(p[0])||0,l=parseInt(p[1])||0;return(w+l)===0?0.5:w/(w+l);}
function getFav(away,home,comp){
const odds=comp?.odds?.[0];
if(odds){if(odds.awayTeamOdds?.favorite===true)return’away’;if(odds.homeTeamOdds?.favorite===true)return’home’;const sp=parseFloat(odds.spread);if(!isNaN(sp))return spwinPct(getRec(home,’total’))?’away’:’home’;
}
function estimateLine(away,home){
const ap=winPct(getRec(away,’total’)),hp=winPct(getRec(home,’total’));
const diff=(hp-ap)*100;if(Math.abs(diff)0?home:away,dT=diff>0?away:home;
return (fT.team?.abbreviation||’?’)+’ -‘+spread+’ / ‘+(dT.team?.abbreviation||’?’)+’ +’+spread;
}
function getStatus(evt){
const desc=(evt.status?.type?.description||”).toLowerCase(),state=(evt.status?.type?.state||”).toLowerCase();
const name=(evt.status?.type?.name||”).toLowerCase(),det=(evt.status?.type?.shortDetail||”).toLowerCase();
if(name===’status_final’||desc.includes(‘final’)||state===’post’)return’final’;
if(desc.includes(‘half’)||det.includes(‘half’)||name.includes(‘halftime’))return’halftime’;
if(state===’in’||desc.includes(‘progress’))return’live’;
return’upcoming’;
}
function sortOrd(t){return{live:0,halftime:1,final:2,upcoming:3}[t]??4;}
function gameSort(a,b){
const sa=getStatus(a),sb=getStatus(b);
function key(evt,st){
const per=evt.status?.period||1;
const secs=clockSecs(evt.status?.displayClock);
if(st===’live’){
if(per>=3)return 0+secs; // OT β treat as most urgent
if(per===2)return 100+secs; // 2nd half: 1:17 < 7:28 < 18:27
return 2000+secs; // 1st half: least time left = furthest along
}
if(st==='halftime')return 1500;
if(st==='final')return 3000+(new Date(evt.date||0).getTime()?0:0); // flat, API order preserved
if(st==='upcoming'){
const ms=new Date(evt.date||'9999').getTime();
return 5000+(isNaN(ms)?999999:ms/1e9); // normalize to small offset
}
return 9999;
}
return key(a,sa)-key(b,sb);
}
function clockSecs(s){if(!s)return 999;const p=String(s||"").split(':');return(parseInt(p[0])||0)*60+(parseInt(p[1])||0);}
function minsElapsed(evt){
const st=getStatus(evt);const period=evt?.status?.period||1;const secs=clockSecs(evt?.status?.displayClock);
if(st==='final')return 40;
if(st==='halftime')return 20;
if(period===1)return Math.max(20-(secs/60),0.1);
if(period===2)return Math.max(20+(20-secs/60),0.1);
return Math.max(40+((period-2)*5)-(secs/60),0.1);
}
// Parse seed from notes headline e.g. "Big South Championship – Quarterfinal"
function getRoundLabel(headline){
if(!headline)return'';
const h=String(headline||"").toLowerCase();
if(h.includes('final')&&!h.includes('semi')&&!h.includes('quarter'))return'F';
if(h.includes('semifinal')||h.includes('semi-final'))return'SF';
if(h.includes('quarterfinal')||h.includes('quarter-final'))return'QF';
if(h.includes('first round')||h.includes('1st round'))return'R1';
if(h.includes('second round')||h.includes('2nd round'))return'R2';
if(h.includes('sweet 16'))return'S16';
if(h.includes('elite 8')||h.includes('elite eight'))return'E8';
if(h.includes('final four'))return'FF';
if(h.includes('championship'))return'CHAMP';
return'';
}
async function loadInjuries(){
try{const d=await api('/mbb/injuries');injuryData=d?.injuries||[];}catch(e){injuryData=[];}
}
async function loadGames(){
if(!KEY){showErr('Enter your API key.');return;}
const el=document.getElementById('gamesList');
el.innerHTML='
‘;
try{
const t=new Date(),y=t.getFullYear(),m=String(t.getMonth()+1).padStart(2,’0′),d=String(t.getDate()).padStart(2,’0′);
const base=’/mbb/scoreboard?year=’+y+’&month=’+m+’&day=’+d;
const [r1,r2,r3]=await Promise.allSettled([api(base+’&seasontype=2′),api(base+’&seasontype=3′),api(base+’&seasontype=4′)]);
const e1=r1.status===’fulfilled’?r1.value?.events||[]:[];
const e2=r2.status===’fulfilled’?r2.value?.events||[]:[];
const e3=r3.status===’fulfilled’?r3.value?.events||[]:[];
e1.forEach(e=>e._stype=2);e2.forEach(e=>e._stype=3);e3.forEach(e=>e._stype=4);
const ids1=new Set(e1.map(e=>e.id));const ids12=new Set([…ids1,…e2.map(e=>e.id)]);
allEvents=[…e1,…e2.filter(e=>!ids1.has(e.id)),…e3.filter(e=>!ids12.has(e.id))];
renderList({events:allEvents});
renderMomentumAlerts();
}catch(e){document.getElementById(‘gamesList’).innerHTML=’
Error: ‘+e.message+’
‘;showErr(‘Failed: ‘+e.message);}
}
function silentRefreshGames(){
if(!KEY)return;
const t=new Date(),y=t.getFullYear(),m=String(t.getMonth()+1).padStart(2,’0′),d=String(t.getDate()).padStart(2,’0′);
const base=’/mbb/scoreboard?year=’+y+’&month=’+m+’&day=’+d;
Promise.allSettled([api(base+’&seasontype=2′),api(base+’&seasontype=3′),api(base+’&seasontype=4′)]).then(([r1,r2,r3])=>{
const e1=r1.status===’fulfilled’?r1.value?.events||[]:[];
const e2=r2.status===’fulfilled’?r2.value?.events||[]:[];
const e3=r3.status===’fulfilled’?r3.value?.events||[]:[];
e1.forEach(e=>e._stype=2);e2.forEach(e=>e._stype=3);e3.forEach(e=>e._stype=4);
const ids1=new Set(e1.map(e=>e.id));const ids12=new Set([…ids1,…e2.map(e=>e.id)]);
const allEvts=[…e1,…e2.filter(e=>!ids1.has(e.id)),…e3.filter(e=>!ids12.has(e.id))];
const sorted=allEvts.sort((a,b)=>gameSort(a,b));
let needsRebuild=false;
sorted.forEach(evt=>{
const gid=evt.id;const newSt=getStatus(evt);
const card=document.querySelector(‘.game-card[data-gid=”‘+gid+’”]’);
if(!card){needsRebuild=true;return;}
if(card.dataset.status!==newSt){needsRebuild=true;return;}
// Patch clock in-place
const clkEl=card.querySelector(‘.card-clock’);
if(clkEl){
if(newSt===’live’){const per=evt.status?.period||1;const pl=per===1?’1ST’:per===2?’2ND’:’OT’+(per-2);clkEl.textContent=(evt.status?.displayClock||”)+’ ‘+pl;}
else if(newSt===’halftime’)clkEl.textContent=’HALFTIME’;
else if(newSt===’final’)clkEl.textContent=’FINAL’;
else clkEl.textContent=evt.status?.type?.shortDetail||”;
}
// Patch scores in-place
const comp=evt.competitions?.[0]||evt;const teams=comp.competitors||[];
const away=teams.find(t=>t.homeAway===’away’)||teams[0]||{};
const home=teams.find(t=>t.homeAway===’home’)||teams[1]||{};
const aScore=parseInt(away.score??-1),hScore=parseInt(home.score??-1);
const hasScores=(aScore>=0&&hScore>=0)&&(aScore>0||hScore>0||newSt===’final’);
const scoreEls=card.querySelectorAll(‘.t-score’);
if(scoreEls[0])scoreEls[0].textContent=hasScores?aScore:’β’;
if(scoreEls[1])scoreEls[1].textContent=hasScores?hScore:’β’;
if(hasScores&&scoreEls[0]&&scoreEls[1]){
const isTie=aScore===hScore;
scoreEls[0].className=’t-score ‘+(isTie?’s-tie’:aScore>hScore?’s-win’:’s-lose’);
scoreEls[1].className=’t-score ‘+(isTie?’s-tie’:hScore>aScore?’s-win’:’s-lose’);
}
// Patch upset/underdog styling
const udogWin=hasScores&&((getFav(away,home,comp)===’home’&&aScore>hScore)||(getFav(away,home,comp)===’away’&&hScore>aScore));
const det=(evt.status?.type?.shortDetail||”).toLowerCase();
const in2nd=det.includes(‘2nd’)||det.includes(‘ot’);
if((newSt===’final’&&udogWin)||((newSt===’live’||newSt===’halftime’)&&in2nd&&udogWin))card.classList.add(‘card-upset’);
});
if(needsRebuild){
const gl=document.querySelector(‘.games-list’);
const scrollTop=gl?gl.scrollTop:0;
renderList({events:sorted});
if(gl)gl.scrollTop=scrollTop;
}
document.getElementById(‘lastUpdate’).textContent=’Updated ‘+new Date().toLocaleTimeString();
}).catch(()=>{});
}
function renderList(data){
const el=document.getElementById(‘gamesList’);el.innerHTML=”;
const evts=data?.events||[];
if(!evts.length){el.innerHTML=’
No games today.
‘;return;}
[…evts].sort((a,b)=>gameSort(a,b)).forEach(evt=>el.appendChild(buildCard(evt)));
}
function buildCard(evt,isSel){
var comp=(evt.competitions&&evt.competitions[0])||evt;
var teams=comp.competitors||[];
var away=teams.find(function(t){return t.homeAway===”away”;})||teams[0]||{};
var home=teams.find(function(t){return t.homeAway===”home”;})||teams[1]||{};
var st=getStatus(evt);
if(st===”final”)return “”;
var tn=String((comp.notes&&comp.notes[0]&&comp.notes[0].headline)||””);
var venue=comp.venue||{},vAddr=venue.address||{};
var vState=String(vAddr.state||””).trim(),vCity=String(vAddr.city||””).trim();
var vLoc=[vCity,vState].filter(Boolean).join(“, “);
var favSide=getFav(away,home,comp);
var ls=estimateLine(away,home,comp.odds?comp.odds[0]:null);
var clkTxt=st===”pre”?key(evt):(getRoundLabel(evt)+” “+clockSecs(evt));
var clkCls=st===”pre”?”card-clock”:”card-clock live”;
function tRow(t,isFav){
var rec=getRec(t);
var confRec=t.records&&t.records.find(function(r){return r.type===”vsconf”;});
var cs=confRec?” (“+confRec.summary+”)”:””;
var sc=t.score||(st===”pre”?”β”:”0″);
var asc=Number(away.score||0),hsc=Number(home.score||0);
var scC=st===”pre”?””:(t===away?(asc>hsc?”s-win”:”s-lose”):(hsc>asc?”s-win”:”s-lose”));
var sd=t.seed?”
“+t.seed+” “:””;
var fv=isFav?”
β “:””;
var hc=(t.homeAway===”home”&&vState)?”
π “:””;
var nm=t.team?(t.team.displayName||t.team.name||””):””;
return “
“+sd+”“+hc+nm+fv+” “+rec+cs+”
“+sc+” “;
}
return “
“+tRow(away,favSide===”away”)+tRow(home,favSide===”home”)+”
“+tn+” “+vLoc+”
“+(ls?”
“+ls+”
“:””)+”
“+clkTxt+”
“;
}
async function selectGame(gid,evt,card){
document.querySelectorAll(‘.game-card’).forEach(c=>c.classList.remove(‘active’));
card.classList.add(‘active’);selGameId=gid;selEvt=evt;renderDash(evt);await loadGameDetails(gid);
if(autoTimer)clearInterval(autoTimer);
autoTimer=setInterval(()=>{silentRefreshGames();if(selGameId)loadGameDetails(selGameId);},8000);
}
function bonusLabel(fouls){const f=parseInt(fouls)||0;if(f>=10)return’DOUBLE BONUS’;if(f>=7)return’BONUS’;return”;}
function renderDash(evt){
const comp=evt.competitions?.[0]||evt,teams=comp.competitors||[];
const away=teams.find(t=>t.homeAway===’away’)||teams[0]||{};
const home=teams.find(t=>t.homeAway===’home’)||teams[1]||{};
const clock=evt.status?.displayClock||’–:–‘,period=evt.status?.period||1;
const pLabel=period===1?’1st Half’:period===2?’2nd Half’:’OT’+(period-2);
const aName=away.team?.location||away.team?.displayName||’Away’;
const hName=home.team?.location||home.team?.displayName||’Home’;
const aAbbr=away.team?.abbreviation||’AWY’;const hAbbr=home.team?.abbreviation||’HME’;
const neutral=!!(comp.neutralSite);
const fav=getFav(away,home,comp);const aFav=fav===’away’,hFav=fav===’home’;
const stype=evt._stype||2;
const tournNote=comp?.notes?.[0]?.headline||”;
// Seed badges for scoreboard
function seedBadge(team){
if((stype===3||stype===4)&&team.seed)return’
#’+team.seed+’ Seed
‘;
const rk=team.curatedRank?.current;if(rk&&rk<99&&rk<26)return'
#’+rk+’ AP
‘;
return”;
}
document.getElementById(‘mainDashboard’).innerHTML=
‘
‘+
‘‘+
‘
‘+
‘
‘+seedBadge(away)+
‘
‘+aName+’
‘+getRec(away,’total’)+’
‘+
‘
‘+(away.score??’β’)+’
‘+
‘
‘+(aFav?’β ‘:”)+’
‘+
‘
‘+
‘
‘+
‘
‘+seedBadge(home)+
‘
‘+hName+’
‘+getRec(home,’total’)+’
‘+
‘
‘+(home.score??’β’)+’
‘+
‘
‘+(neutral?”:(hFav?’π β ‘:’π ‘))+’
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘+
‘
Venue
‘+(comp.venue?.fullName||’β’)+’
‘+
‘
Attendance
‘+(comp.attendance?comp.attendance.toLocaleString():’β’)+’
‘+
‘
‘+
‘
‘+
‘
‘+
‘
‘;
setTimeout(()=>drawScoringChart([],[],0),100);
}
function drawScoringChart(awayPts,homePts,halftimeIdx){
const canvas=document.getElementById(‘scoringChart’);if(!canvas)return;
const W=canvas.offsetWidth||900,H=120;canvas.width=W;canvas.height=H;
const ctx=canvas.getContext(‘2d’);ctx.fillStyle=’#0a0e1a’;ctx.fillRect(0,0,W,H);
if(!awayPts.length&&!homePts.length){ctx.fillStyle=’#64748b’;ctx.font=’12px system-ui’;ctx.textAlign=’center’;ctx.fillText(‘Scoring chart loads as game progresses’,W/2,H/2+4);return;}
const maxScore=Math.max(…awayPts,…homePts,10);
const pad={l:8,r:30,t:10,b:8};const cW=W-pad.l-pad.r,cH=H-pad.t-pad.b;
const n=Math.max(awayPts.length,homePts.length,1);
const xOf=i=>pad.l+(i/(n-1||1))*cW;const yOf=v=>pad.t+cH-(v/maxScore)*cH;
if(halftimeIdx>0&&halftimeIdxi===0?ctx.moveTo(xOf(i),yOf(v)):ctx.lineTo(xOf(i),yOf(v)));ctx.stroke();
const last=pts[pts.length-1];
ctx.beginPath();ctx.fillStyle=color;ctx.arc(xOf(pts.length-1),yOf(last),3,0,Math.PI*2);ctx.fill();
ctx.fillStyle=color;ctx.font=’bold 11px system-ui’;ctx.textAlign=’left’;ctx.fillText(last,xOf(pts.length-1)+5,yOf(last)+4);
}
drawLine(awayPts,’#3b82f6′);drawLine(homePts,’#f97316′);
}
// —- FOUR FACTORS (Dean Oliver) —-
// eFG% = (FGM + 0.5*3PM) / FGA β shooting quality
// TOV% = TOV / (FGA + 0.44*FTA + TOV) β ball security
// OREB% = OREB / (OREB + OppDREB) β 2nd chance rate
// FT Rate = FTM / FGA β getting to the line
function getGrade(factor,val){
const scales={
eFG: [{v:58,g:’A+’},{v:55,g:’A’},{v:53,g:’A-‘},{v:51,g:’B+’},{v:49,g:’B’},{v:47,g:’B-‘},{v:45,g:’C+’},{v:43,g:’C’},{v:40,g:’D’},{v:0,g:’F’}],
tov: [{v:88,g:’F’},{v:74,g:’D’},{v:26,g:’F’}], // handled inverted below
oreb: [{v:40,g:’A+’},{v:37,g:’A’},{v:35,g:’A-‘},{v:32,g:’B+’},{v:29,g:’B’},{v:26,g:’B-‘},{v:23,g:’C+’},{v:20,g:’C’},{v:15,g:’D’},{v:0,g:’F’}],
ftRate:[{v:50,g:’A+’},{v:45,g:’A’},{v:42,g:’A-‘},{v:38,g:’B+’},{v:34,g:’B’},{v:30,g:’B-‘},{v:26,g:’C+’},{v:22,g:’C’},{v:17,g:’D’},{v:0,g:’F’}],
};
// TOV is inverted β lower is better
if(factor===’tov’){
if(val<12)return'A+';if(val<14)return'A';if(val<16)return'A-';
if(val<18)return'B+';if(val<20)return'B';if(val<22)return'B-';
if(val<24)return'C+';if(val<26)return'C';if(val=s.v)return s.g;}
return’F’;
}
function gradeColor(g){
if(g===’A+’||g===’A’)return’#22c55e’;
if(g===’A-‘||g===’B+’)return’#86efac’;
if(g===’B’||g===’B-‘)return’#eab308′;
if(g===’C+’||g===’C’)return’#f97316′;
return’#ef4444′;
}
function calcFourFactors(stats, oppStats){
function gs(st,name){const s=(st||[]).find(x=>x.name===name);return parseFloat(s?.displayValue||s?.value||0)||0;}
function parseFrac(v){const p=(v||’0-0′).toString().split(‘-‘);return{m:parseInt(p[0])||0,a:parseInt(p[1])||0};}
function getFrac(st,name){return parseFrac(gs(st,name)||’0-0′);}
// Need raw fraction values
function getFracStat(st,name){const s=(st||[]).find(x=>x.name===name);return parseFrac(s?.displayValue||’0-0′);}
const fg=getFracStat(stats,’fieldGoalsMade-fieldGoalsAttempted’);
const ft=getFracStat(stats,’freeThrowsMade-freeThrowsAttempted’);
const tp=getFracStat(stats,’threePointFieldGoalsMade-threePointFieldGoalsAttempted’);
const oreb=gs(stats,’offensiveRebounds’);
const dreb=gs(stats,’defensiveRebounds’);
const tov=gs(stats,’turnovers’)||gs(stats,’totalTurnovers’);
// Opponent defensive rebounds for OREB%
const oppDreb=gs(oppStats,’defensiveRebounds’);
const eFG=fg.a>0?((fg.m+0.5*tp.m)/fg.a)*100:0;
const tovPct=((fg.a+0.44*ft.a+tov)>0)?(tov/(fg.a+0.44*ft.a+tov))*100:0;
const orebPct=(oreb+oppDreb)>0?(oreb/(oreb+oppDreb))*100:0;
const ftRate=fg.a>0?(ft.a/fg.a)*100:0;
return{eFG,tovPct,orebPct,ftRate,fg,ft,tp,tov,oreb,dreb};
}
function renderFourFactors(el,aSnap,hSnap,aPrev,hPrev,aName,hName){
if(!el)return;
var f=calcFourFactors(aSnap,hSnap);if(!f){el.innerHTML=””;return;}
var prev=(aPrev&&hPrev)?calcFourFactors(aPrev,hPrev):null;
function arr(cur,pv,hi){if(!prev||!pv)return “”;return(hi?cur>pv:cur<pv)?"
β² “:”
βΌ “;}
function pct(v){return typeof v===”number”?(v*100).toFixed(1)+”%”:”–“;}
var factors=[{lbl:”Shooting”,ak:”awayEfg”,hk:”homeEfg”,hi:true},{lbl:”Ball Security”,ak:”awayTov”,hk:”homeTov”,hi:false},{lbl:”Off Reb”,ak:”awayOreb”,hk:”homeOreb”,hi:true},{lbl:”FT Rate”,ak:”awayFtRate”,hk:”homeFtRate”,hi:true}];
var rows=factors.map(function(r){
var av=f[r.ak],hv=f[r.hk],aw=r.hi?av>hv:avav:hv<av;
var ag=getGrade(r.ak,av),hg=getGrade(r.hk,hv),pav=prev?prev[r.ak]:0,phv=prev?prev[r.hk]:0;
return "
“+ pct(av)+arr(av,pav,r.hi)+” “+ag+” “+r.lbl+” “+ pct(hv)+arr(hv,phv,r.hi)+” “+hg+” “;
}).join(“”);
el.innerHTML=”
“+(aName||”Away”)+” “+(hName||”Home”)+” “+rows+”
“;
}
// —- TOP SCORERS with correct game-pace PPM —-
function renderTopScorers(el,aBox,hBox){
if(!el)return;
function gs(stats,n){var s=stats.find(function(s){return s.name===n||s.shortDisplayName===n.toUpperCase();});return s?(parseFloat(s.displayValue)||0):0;}
function top3(box){
if(!box||!box.players)return[];
var pl=[];box.players.forEach(function(pg){(pg.athletes||[]).forEach(function(a){pl.push(a);});});
return pl.filter(function(a){return a.statistics&&a.statistics.length;}).map(function(a){
var s=a.statistics;
return{name:a.athlete?(a.athlete.shortName||a.athlete.displayName||”?”):”?”,pts:gs(s,”points”)||gs(s,”PTS”),reb:gs(s,”rebounds”)||gs(s,”totalRebounds”)||gs(s,”REB”),ast:gs(s,”assists”)||gs(s,”AST”),fls:gs(s,”fouls”)||gs(s,”personalFouls”)||gs(s,”PF”)};
}).filter(function(p){return p.pts>0;}).sort(function(a,b){return b.pts-a.pts;}).slice(0,3);
}
function foulPl(box){
if(!box||!box.players)return[];
var pl=[];box.players.forEach(function(pg){(pg.athletes||[]).forEach(function(a){pl.push(a);});});
return pl.filter(function(a){return a.statistics&&a.statistics.length;}).map(function(a){
var s=a.statistics;
return{name:a.athlete?(a.athlete.shortName||”?”):”?”,fls:gs(s,”fouls”)||gs(s,”personalFouls”)||gs(s,”PF”),pts:gs(s,”points”)||gs(s,”PTS”),reb:gs(s,”rebounds”)||gs(s,”REB”),ast:gs(s,”assists”)||gs(s,”AST”)};
}).filter(function(p){return p.fls>=4;});
}
var aS=top3(aBox),hS=top3(hBox);
var fouls=foulPl(aBox).concat(foulPl(hBox)).sort(function(a,b){return b.fls-a.fls;});
function rows(list){if(!list.length)return “
No data yet
“;return list.map(function(p,i){return “
“+(i+1)+” “+p.name+” “+p.pts+” PTS “+p.reb+” REB “+p.ast+” AST
“;}).join(“”);}
var fh=””;
if(fouls.length){
fh=”
β οΈ Foul Trouble
“;
fouls.forEach(function(p){fh+=”
“+p.name+” =5?”foul-out”:”foul-danger”)+”‘>”+p.fls+” fouls “+p.pts+”pts “+p.reb+”reb “+p.ast+”ast
“;});
fh+=”
“;
}
el.innerHTML=”
“+fh;
}
// ββ MOMENTUM DETECTION ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Timing gate: only fire after 10min mark of 1st half or any time in 2nd half+
function inMomentumWindow(evt){
const period = evt?.status?.period||1;
if(period >= 2) return true;
if(period === 1){
const secs = clockSecs(evt?.status?.displayClock);
return secs {
const tid = String(p.team?.id||”);
const type = p.type?.text||”;
const scored = p.scoringPlay===true;
const val = p.scoreValue||0;
// Skip subs, timeouts, periods
if(type===’Substitution’||type===’End of Period’||type===’Start Period’||!tid) return;
// Track possession owner
if(!curTeam) curTeam = tid;
// If scoring play for current team
if(scored && tid === curTeam){
curPts += val;
curClock = p.clock?.displayValue||”;
curPeriod = p.period?.number||1;
curText = p.text||”;
// Free throws: only close possession on last FT
const isFT = type===’MadeFreeThrow’;
const isLastFT = !isFT || (p.text||”).match(/d+ of (d+)/) &&
RegExp.$1 === (p.text||”).match(/(d+) of d+/)?.[1];
if(!isFT || isLastFT){
possessions.push({teamId:curTeam, scored:true, pts:curPts, clock:curClock, period:curPeriod, text:curText});
curTeam = tid === awayTeamId ? homeTeamId : awayTeamId; // possession switches
curPts = 0; curText = ”;
}
return;
}
// Turnover = possession ends with 0 pts
if(TURNOVER_TYPES.some(t=>type.includes(t)) && tid === curTeam){
possessions.push({teamId:curTeam, scored:false, pts:0, clock:p.clock?.displayValue||”, period:p.period?.number||1, text:p.text||”});
curTeam = tid === awayTeamId ? homeTeamId : awayTeamId;
curPts = 0; curText = ”;
return;
}
// Defensive rebound = opponent missed, this team gets ball
if(type===’Defensive Rebound’){
// The team getting the rebound = possession changes TO them
if(curTeam && curTeam !== tid && curPts === 0){
possessions.push({teamId:curTeam, scored:false, pts:0, clock:p.clock?.displayValue||”, period:p.period?.number||1, text:’Miss’});
}
curTeam = tid;
curPts = 0; curText = ”;
}
});
return possessions;
}
// Detect momentum shift in last N possessions
// Returns null or {teamId, state:’forming’|’confirmed’, run:{ourPts,theirPts,ourConv,theirConv}, startClock, startPeriod}
function detectMomentum(possessions, awayTeamId, homeTeamId){
if(possessions.length p.teamId===focusTeam).slice(-3);
const theirPoss = recent.filter(p=>p.teamId===oppTeam).slice(-3);
if(ourPoss.length < 2 || theirPoss.length p.scored).length;
const theirConv = theirPoss.filter(p=>p.scored).length;
const ourPts = ourPoss.reduce((s,p)=>s+p.pts,0);
const theirPts = theirPoss.reduce((s,p)=>s+p.pts,0);
// FORMING: our last 2 possessions scored, their last 2 did not
// (1 stop + 1 score = earliest signal)
const ourLast2 = ourPoss.slice(-2);
const theirLast2 = theirPoss.slice(-2);
const ourLast2Conv = ourLast2.filter(p=>p.scored).length;
const theirLast2Conv = theirLast2.filter(p=>p.scored).length;
if(ourLast2Conv >= 2 && theirLast2Conv === 0){
const startPoss = recent.find(p=>p.teamId===focusTeam&&p.scored);
return {
teamId: focusTeam,
side: focusTeam === awayTeamId ? ‘away’ : ‘home’,
state: (ourConv >= 3 && theirConv <= 1) ? 'confirmed' : 'forming',
run: {ourPts, theirPts, ourConv, theirConv},
clock: theirPoss[theirPoss.length-1]?.clock||'',
period: theirPoss[theirPoss.length-1]?.period||1,
};
}
}
return null;
}
function playAlertSound(type){
try{
var AC=window.AudioContext||window.webkitAudioContext;if(!AC)return;
var ctx=new AC();
function t(f,s,d,v){var o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);o.frequency.value=f;g.gain.setValueAtTime(v||0.3,ctx.currentTime+s);g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+s+d);o.start(ctx.currentTime+s);o.stop(ctx.currentTime+s+d+0.05);}
if(type==="confirmed"){t(523,0,0.12);t(659,0.1,0.12);t(784,0.2,0.12);t(1047,0.32,0.3,0.4);t(1319,0.58,0.35,0.35);}
else{t(440,0,0.15);t(554,0.16,0.15);t(659,0.32,0.25,0.35);}
}catch(e){}
}
function renderMomentumAlerts(){
var el=document.getElementById("momentumAlertsList");
if(!el)return;
var live=(allEvents||[]).filter(function(e){var s=getStatus(e);return s==="live"||s==="halftime";});
if(!live.length){el.innerHTML="
π Analyzingβ¦ “;return;}
var alerts=[];
live.forEach(function(evt){
var comp=(evt.competitions&&evt.competitions[0])||evt;
var snaps=(window.snapshots&&window.snapshots[getSnapKey(evt)])||[];
if(snaps.length<2)return;
var teams=comp.competitors||[];
var away=teams.find(function(t){return t.homeAway==="away";});
var home=teams.find(function(t){return t.homeAway==="home";});
var result=detectMomentum(buildPossessions(snaps),away&&away.team?away.team.id:null,home&&home.team?home.team.id:null,teams);
if(result)alerts.push({evt:evt,result:result});
});
if(!alerts.length){el.innerHTML="
π Analyzingβ¦ “;return;}
var html=””;
alerts.forEach(function(item){
var r=item.result,cls=r.status===”confirmed”?”alert-confirmed”:”alert-forming”,icon=r.status===”confirmed”?”π₯”:”β‘”,lbl=r.status===”confirmed”?”CONFIRMED”:”FORMING”,gid=item.evt.id;
html+=”
“;
html+=icon+” “+r.teamName+” “+lbl+” β “+r.desc+” “+(item.evt.shortName||””)+”
“;
var k=gid+r.status;
if(!_alertSeen[k]){_alertSeen[k]=true;playAlertSound(r.status);}
});
el.innerHTML=html;
}
function jumpToGame(gid){
const card = document.querySelector(‘.game-card[data-gid=”‘+gid+’”]’);
if(card){card.scrollIntoView({behavior:’smooth’,block:’center’});card.click();}
}
async function loadGameDetails(gid){
try{
const[box,summ]=await Promise.allSettled([api(‘/mbb/box-score/’+gid),api(‘/mbb/summary/’+gid)]);
const boxVal=box.status===’fulfilled’?box.value:null;
const summVal=summ.status===’fulfilled’?summ.value:null;
if(boxVal)renderBox(boxVal);
if(boxVal)renderTopScorers(boxVal);
if(summVal){
renderOdds(summVal);renderH2H(summVal);renderScoringChart(summVal);
renderFourFactors(summVal,boxVal);
// Momentum detection β runs on every refresh, no extra API calls
if(selEvt && inMomentumWindow(selEvt)){
const comp=selEvt.competitions?.[0]||selEvt;
const teams=comp.competitors||[];
const awayComp=teams.find(t=>t.homeAway===’away’)||teams[0]||{};
const homeComp=teams.find(t=>t.homeAway===’home’)||teams[1]||{};
const awayId=String(awayComp.team?.id||”);
const homeId=String(homeComp.team?.id||”);
const plays=summVal.plays||[];
if(plays.length){
const possessions=buildPossessions(plays,awayId,homeId);
const result=detectMomentum(possessions,awayId,homeId);
const st=getStatus(selEvt);
if(result && (st===’live’||st===’halftime’)){
momentumAlerts[gid]={
…result,
awayName:awayComp.team?.abbreviation||’Away’,
homeName:homeComp.team?.abbreviation||’Home’,
gameId:gid,
};
} else {
// Clear if no longer active
if(momentumAlerts[gid]) delete momentumAlerts[gid];
}
renderMomentumAlerts();
}
}
} else if(boxVal){
renderFourFactors(null,boxVal);
}
renderInjuryReport();
document.getElementById(‘lastUpdate’).textContent=’Updated ‘+new Date().toLocaleTimeString();
}catch(e){showErr(‘Game error: ‘+e.message);}
}
function renderScoringChart(data){
const plays=data?.plays||[];if(!plays.length){drawScoringChart([],[],0);return;}
const scored=plays.filter(p=>p.awayScore!=null&&p.homeScore!=null);
const awayPts=scored.map(p=>parseInt(p.awayScore)||0);
const homePts=scored.map(p=>parseInt(p.homeScore)||0);
const htIdx=scored.findIndex(p=>p.period?.number===2);
drawScoringChart(awayPts,homePts,htIdx>0?htIdx:0);
}
function renderBox(data){
const tl=data?.players||data?.teams||[];const teams=Array.isArray(tl)?tl:Object.values(tl);
[‘awayPlayers’,’homePlayers’].forEach((id,idx)=>{
const el=document.getElementById(id);if(!el)return;
const td=teams[idx];if(!td){el.innerHTML=’
No data
‘;return;}
const roster=td?.statistics?.[0]?.athletes||[];
if(!roster.length){el.innerHTML=’
No data yet
‘;return;}
const sorted=roster.filter(a=>a?.stats?.length>0).map(a=>{
const s=a.stats||[];
return{name:a.athlete?.shortName||’β’,pos:a.athlete?.position?.abbreviation||”,min:s[0]||’0′,pts:parseInt(s[1])||0,fg:s[2]||’0-0′,tp:s[3]||’0-0′,reb:s[5]||’0′,ast:s[6]||’0′,fouls:parseInt(s[12]||0),starter:a.starter===true};
}).sort((a,b)=>b.pts-a.pts);
let h=’
Player Min PTS REB AST FG 3PT PF ‘;
sorted.forEach(p=>{const fc=p.fouls>=4?’foul-danger’:p.fouls===3?’foul-warning’:’foul-ok’;
h+=’‘+p.name+(p.starter?’ (S) ‘:”)+’
‘+p.pos+’
‘+p.min+’ ‘+p.pts+’ ‘+p.reb+’ ‘+p.ast+’ ‘+p.fg+’ ‘+p.tp+’ ‘+p.fouls+’ ‘;});
el.innerHTML=h+’
‘;
});
}
function renderInjuryReport(){
const el=document.getElementById(‘injurySection’);if(!el)return;
if(!injuryData.length){el.innerHTML=’
No injury data available.
‘;return;}
const comp=selEvt?.competitions?.[0];const teamIds=new Set((comp?.competitors||[]).map(c=>String(c.team?.id||”)));
const relevant=injuryData.filter(i=>{const tid=String(i.team?.id||”);return!tid||teamIds.has(tid)||teamIds.size===0;});
if(!relevant.length){el.innerHTML=’
No injuries for these teams.
‘;return;}
const sc=s=>{const sl=(s||”).toLowerCase();if(sl.includes(‘out’))return’inj-out’;if(sl.includes(‘quest’))return’inj-questionable’;if(sl.includes(‘prob’))return’inj-probable’;return’inj-dtd’;};
el.innerHTML=’
‘+relevant.slice(0,10).map(i=>{
const status=i.status||’Injured’;const name=i.athlete?.displayName||’Unknown’;const team=i.team?.abbreviation||”;const detail=i.details||i.injury?.description||”;
return`
${status} ${name}
${detail?’
‘+detail+’
‘:”}
${team?’
‘+team+’
‘:”}
`;
}).join(”)+’
‘;
}
function renderOdds(data){const odds=data?.pickcenter||data?.odds||[];const s=document.getElementById(‘spreadVal’),o=document.getElementById(‘ouVal’);if(!s||!o)return;s.textContent=odds[0]?.details||’β’;o.textContent=odds[0]?.overUnder?odds[0].overUnder.toFixed(1):’β’;}
function renderH2H(data){const el=document.getElementById(‘h2hSection’);if(!el)return;const h=data?.headToHeadMatchups||data?.h2h||[];if(!h.length){el.innerHTML=’
No H2H data
‘;return;}el.innerHTML=h.slice(0,5).map(g=>’
‘+(g.shortName||’β’)+’ ‘+(g.score||’β’)+’ ‘+(g.date||”)+’
‘).join(”);}