/* ========================================================= Match Intelligence UI - Phase 5.2 Tabs always render. Safe when fixture JSON is missing. ========================================================= */ let playerCache = {}; /* TRANSLATION HELPER */ function t(key){ return (window.DW_I18N && window.DW_I18N[key]) || key } document.addEventListener("DOMContentLoaded", async () => { // WAIT for translations while (!window.DW_I18N) { await new Promise(r => setTimeout(r, 50)) } // Load player image cache try { const res = await fetch("/data/player_media_cache.json?ts=" + Date.now()); if (res.ok) { playerCache = await res.json(); console.log("[CACHE] Loaded player media:", Object.keys(playerCache).length); } } catch (e) { console.warn("[CACHE] Failed to load player cache"); } const fixtureId = new URLSearchParams(location.search).get("matchId") if (!fixtureId) return const anchor = document.getElementById("match-container") if (!anchor) return const root = document.createElement("section") root.id = "match-intel-root" root.className = "match-intel" anchor.after(root) let fx = null try { const r = await fetch(`/data/fixture_details/fixture_${fixtureId}.json?ts=${Date.now()}`) if (r.ok) fx = (await r.json())?.response?.[0] || null } catch {} let h2hData = null try { const r2 = await fetch(`/data/h2h_data.json?ts=${Date.now()}`) if (r2.ok) { const all = await r2.json() h2hData = all[fixtureId] || null } } catch {} const index = fx ? indexPlayers(fx.players || []) : {} const subs = fx ? indexSubs(fx.events || []) : { out:{}, in:{} } root.innerHTML = `
${fx ? fx.teams.home.name + " " + t("vs") + " " + fx.teams.away.name : t("unavailable")}
No momentum data available
"}" + t("summaryUnavailable") + "
"}" + t("statsUnavailable") + "
"}Loading H2H...
No H2H data
` } else { h2hPanel.innerHTML = renderH2H(h2hData) } } document.dispatchEvent(new Event("lang:changed")) /* =============================== LIVE SCORE POLLER (20s) ================================ */ let scorePollerId = null if (fx?.fixture?.status?.short && !["FT","AET","PEN","NS"].includes(fx.fixture.status.short)) { scorePollerId = setInterval(async () => { try { const r = await fetch(`/data/fixture_details/fixture_${fixtureId}.json?ts=${Date.now()}`) if (!r.ok) return const fresh = (await r.json())?.response?.[0] if (!fresh) return // Update score only const scoreEl = document.getElementById("live-score-display") if (scoreEl) { const hg = fresh.goals?.home ?? "–" const ag = fresh.goals?.away ?? "–" scoreEl.textContent = hg + " – " + ag } // Update live badge const badgeEl = document.querySelector("#intel-momentum .momentum-live-badge") if (badgeEl) { const st = fresh.fixture?.status?.short const el = fresh.fixture?.status?.elapsed || 0 const isFinished = ["FT","AET","PEN"].includes(st) const isLive = ["1H","2H","HT","ET","P"].includes(st) if (isFinished) { badgeEl.style.color = "#aaa" badgeEl.innerHTML = "⬛ Full Time" clearInterval(scorePollerId) scorePollerId = null } else if (isLive) { badgeEl.innerHTML = ` LIVE · ${el}'` } } } catch {} }, 20000) } /* =============================== LIVE FIXTURE POLLER ================================ */ let pollerId = null let lastEventCount = fx?.events?.length || 0 let lastStatus = fx?.fixture?.status?.short || null if (lastStatus && lastStatus !== "FT") { pollerId = setInterval(async () => { try { const r = await fetch(`/data/fixture_details/fixture_${fixtureId}.json?ts=${Date.now()}`) if (!r.ok) return const fresh = (await r.json())?.response?.[0] if (!fresh) return fx = fresh if (Array.isArray(fresh.events) && fresh.events.length > lastEventCount) { const newEvents = fresh.events.slice(lastEventCount) const timeline = document.querySelector(".summary-timeline") if (timeline) { newEvents.forEach(e => { timeline.insertAdjacentHTML("beforeend", renderEventRow(e, fresh.teams)) }) } lastEventCount = fresh.events.length } if (Array.isArray(fresh.players)) { const newIndex = indexPlayers(fresh.players) document.querySelectorAll(".player, .bench-player").forEach(el => { const d = newIndex[el.dataset.id] if (!d) return const rating = formatRating(d.stats?.games?.rating) const badge = el.querySelector(".badge, .b-rate") if (badge) { badge.textContent = rating badge.className = badge.className.replace(/rate-\w+/g, "") badge.classList.add(ratingClass(rating)) } }) } const statsPanel = document.getElementById("intel-stats") if (statsPanel && Array.isArray(fresh.statistics)) { statsPanel.innerHTML = renderStats(fresh.statistics) } const momPanel = document.getElementById("intel-momentum") if (momPanel) { momPanel.innerHTML = renderMomentum(fresh) } const newStatus = fresh.fixture?.status?.short if (newStatus === "FT") { clearInterval(pollerId) pollerId = null } lastStatus = newStatus } catch {} }, 120000) } /* ✅ CORRECT POSITION */ document.addEventListener("lang:changed", () => { if (!fx) return const statsPanel = document.getElementById("intel-stats") if (statsPanel && Array.isArray(fx.statistics)) { statsPanel.innerHTML = renderStats(fx.statistics) } }) }) /* =========================== GENERIC LINEUP (no fixture) =========================== */ function renderGenericLineup() { const ICON = "https://cdn-icons-png.flaticon.com/512/847/847969.png" const formation442 = [ { row: [{ x: 50, y: 12 }] }, { row: [{ x: 15, y: 32 }, { x: 38, y: 32 }, { x: 62, y: 32 }, { x: 85, y: 32 }] }, { row: [{ x: 15, y: 54 }, { x: 38, y: 54 }, { x: 62, y: 54 }, { x: 85, y: 54 }] }, { row: [{ x: 30, y: 74 }, { x: 70, y: 74 }] } ] let html = "" + t("lineupsUnavailable") + "
" return html } /* =========================== FORMATIONS & POSITIONS =========================== */ const FORMATIONS = { "4-4-2":[1,4,4,2],"4-3-3":[1,4,3,3],"4-2-3-1":[1,4,2,3,1], "4-3-1-2":[1,4,3,1,2],"4-1-4-1":[1,4,1,4,1], "3-5-2":[1,3,5,2],"5-3-2":[1,5,3,2], "5-4-1":[1,5,4,1],"3-4-3":[1,3,4,3] } const ROW_Y_HOME = [12,32,54,74,92] const ROW_Y_AWAY = [92,72,50,28,8] function renderFormation(team, side, index, subs) { if (!team?.startXI) return "" const layout = FORMATIONS[team.formation] || FORMATIONS["4-4-2"] let players = [...team.startXI] let html = "" let row = 0 layout.forEach(count => { const rowPlayers = players.splice(0, count) const y = side === "home" ? ROW_Y_HOME[row] : ROW_Y_AWAY[row] const xs = getSlots(count, side) rowPlayers.forEach((p,i)=>{ const d = index[p.player.id] || {} const s = d.stats || {} const rating = formatRating(s.games?.rating) const subOut = subs.out[p.player.id] const isCap = d.stats?.games?.captain === true html += `${t("unavailable")}
${t("noEvents")}
` } let htInserted = false const rows = events.map(e => { let divider = "" if (!htInserted && e.time?.elapsed > 45) { htInserted = true divider = `${t("statsUnavailable")}
` } const home = normalizeStats(stats[0].statistics) const away = normalizeStats(stats[1].statistics) const keys = Object.keys(home) return `No momentum data available
` } const homeId = fx.teams.home.id const awayId = fx.teams.away.id const homeName = fx.teams.home.name const awayName = fx.teams.away.name const homeLogo = fx.teams.home.logo const awayLogo = fx.teams.away.logo const homeColour = fx.lineups?.[0]?.team?.colors?.player?.primary ? "#" + fx.lineups[0].team.colors.player.primary : "#e41e2c" const awayColour = fx.lineups?.[1]?.team?.colors?.player?.primary ? "#" + fx.lineups[1].team.colors.player.primary : "#1532c1" const status = fx.fixture?.status?.short || "NS" const elapsed = fx.fixture?.status?.elapsed || 0 const isLive = ["1H","2H","HT","ET","P"].includes(status) const isFinished = ["FT","AET","PEN"].includes(status) const homeGoals = fx.goals?.home ?? "–" const awayGoals = fx.goals?.away ?? "–" // ── Baseline from statistics ────────────────────────────── const stats = Array.isArray(fx.statistics) && fx.statistics.length === 2 ? fx.statistics : null function getStat(teamStats, label) { const entry = teamStats.statistics?.find(s => s.type === label) const v = entry?.value if (v === null || v === undefined) return 0 if (typeof v === "string" && v.endsWith("%")) return parseFloat(v) return parseFloat(v) || 0 } const BUCKET = 5 const NUM_BUCKETS = 19 const homeBuckets = new Array(NUM_BUCKETS).fill(0) const awayBuckets = new Array(NUM_BUCKETS).fill(0) // ── Step 1: place event spikes first ──────────────────────── const WEIGHTS = { "Goal": 6, "Card": 1, "Var": 2, "subst": 0.5 } const goalEvents = [] let homeScore = 0, awayScore = 0 const eventBuckets = new Set() fx.events.forEach(e => { const min = (e.time?.elapsed || 0) + (e.time?.extra || 0) * 0.1 const idx = Math.min(Math.floor(min / BUCKET), NUM_BUCKETS - 1) const w = WEIGHTS[e.type] || 0.3 const isHome = e.team?.id === homeId const isAway = e.team?.id === awayId if (isHome) homeBuckets[idx] += w if (isAway) awayBuckets[idx] += w eventBuckets.add(idx) if (e.type === "Goal") { if (isHome) homeScore++ if (isAway) awayScore++ goalEvents.push({ minute: Math.round(min), side: isHome ? "home" : "away", score: homeScore + "-" + awayScore, player: e.player?.name || "" }) } }) // ── Step 2: baseline ONLY on quiet buckets (no events) ─────── if (stats) { const hPoss = getStat(stats[0], "Ball Possession") const aPoss = getStat(stats[1], "Ball Possession") const hShots = getStat(stats[0], "Shots on Goal") const aShots = getStat(stats[1], "Shots on Goal") const hCorner = getStat(stats[0], "Corner Kicks") const aCorner = getStat(stats[1], "Corner Kicks") const hRaw = (hPoss * 0.5) + (hShots * 3) + (hCorner * 2) const aRaw = (aPoss * 0.5) + (aShots * 3) + (aCorner * 2) const total = hRaw + aRaw || 1 // Gentle baseline max 12 — only fills quiet periods const hBase = (hRaw / total) * 12 const aBase = (aRaw / total) * 12 for (let i = 0; i < NUM_BUCKETS; i++) { if (eventBuckets.has(i)) continue // skip buckets that already have events const variance = () => 0.60 + Math.random() * 0.80 // wide variance for natural look homeBuckets[i] += hBase * variance() awayBuckets[i] += aBase * variance() } } // Normalize const allVals = [...homeBuckets, ...awayBuckets] const maxVal = Math.max(...allVals, 1) const norm = v => Math.round((v / maxVal) * 100) const W_SVG = 340, H_SVG = 160, CHART_H = 70 const BAR_W = Math.floor(W_SVG / NUM_BUCKETS) - 1 const MID_Y = H_SVG / 2 let svgBars = "" for (let i = 0; i < NUM_BUCKETS; i++) { const x = i * (BAR_W + 1) + 2 const hH = Math.round((norm(homeBuckets[i]) / 100) * CHART_H) const aH = Math.round((norm(awayBuckets[i]) / 100) * CHART_H) if (hH > 0) svgBars += `
${m.home}