← Back to Home

Loading match intelligence…

_mainInit() { (function() { 'use strict'; const ui = window._matchUI || {}; const matchId = (function() { const m = location.pathname.match(/\/match\/(\d+)/); return m ? parseInt(m[1]) : null; })(); if (!matchId) { document.getElementById('app').innerHTML = '

' + (ui.invalidMatch || 'Invalid Match') + '

' + (ui.noMatchId || 'No match ID found in URL.') + '

'; return; } const STATE_ICONS = { injury: '\uD83E\uDE79', weather: '\uD83C\uDF27', lineup: '\uD83D\uDCCB', tactics: '\uD83E\uDDE0', transfer: '\uD83D\uDD04', referee: '\uD83C\uDFF3', odds_move: '\uD83D\uDCCA', news: '\uD83D\uDCF0' }; const STATE_LABELS = { injury: ui.injury || 'Injury', weather: ui.weather || 'Weather', lineup: ui.lineup || 'Lineup', tactics: ui.tactical || 'Tactical', transfer: ui.transfer || 'Transfer', referee: ui.referee || 'Referee', odds_move: ui.oddsMove || 'Odds Move', news: ui.news || 'News' }; const STATE_ORDER = ['injury', 'weather', 'lineup', 'tactics', 'transfer', 'referee', 'odds_move', 'news']; const RESULT_NAMES = { H: ui.homeWin || 'Home Win', D: ui.draw || 'Draw', A: ui.awayWin || 'Away Win' }; const RESULT_ZH = { H: '主胜', D: '平局', A: '客胜' }; function fmtDate(iso) { if (!iso) return ''; const loc = (document.documentElement.lang === 'zh-CN') ? 'zh-CN' : 'en-US'; const d = new Date(iso); return d.toLocaleDateString(loc, { weekday:'short', month:'short', day:'numeric', year:'numeric' }) + ' ' + d.toLocaleTimeString(loc, { hour:'2-digit', minute:'2-digit' }); } function fmtTime(iso) { if (!iso) return ''; const loc = (document.documentElement.lang === 'zh-CN') ? 'zh-CN' : 'en-US'; const d = new Date(iso); return d.toLocaleTimeString(loc, { hour:'2-digit', minute:'2-digit' }); } function impactClass(val) { if (val > 0.01) return 'pos'; if (val < -0.01) return 'neg'; return 'neutral'; } function impactDelta(val) { if (Math.abs(val) < 0.001) return '0%'; const pct = (val * 100).toFixed(1); return (val > 0 ? '+' : '') + pct + '%'; } function impactLevel(impact) { if (!impact) return 'low'; const maxVal = Math.max( Math.abs(impact.home_win || 0), Math.abs(impact.draw || 0), Math.abs(impact.away_win || 0) ); if (maxVal >= 0.03) return 'high'; if (maxVal >= 0.01) return 'medium'; return 'low'; } /* ── Fetch & Render ── */ async function fetchNarrative(mid) { const el = document.getElementById('sidebar'); if (!el) return; // Show loading el.innerHTML = '
🧠' + (ui.aiMatchReport || 'AI Match Report') + '
' + (ui.generatingAnalysis || 'Generating grounded match analysis…') + '
'; try { const res = await fetch('/api/match-narrative/' + mid); const data = await res.json(); if (!data.ok || !data.narrative) { el.innerHTML = '
🧠' + (ui.aiMatchReport || 'AI Match Report') + '
' + (ui.unavailable || 'Unavailable') + '
'; return; } renderNarrativeHTML(data); } catch (e) { el.innerHTML = '
🧠' + (ui.aiMatchReport || 'AI Match Report') + '
' + (ui.unavailable || 'Unavailable') + '
'; } } async function fetchConfidenceTimeline(mid) { try { const res = await fetch('/api/match/' + mid + '/confidence-timeline'); if (!res.ok) return; const data = await res.json(); if (!data.ok || !data.points || data.points.length === 0) return; renderConfidenceCurve(data); } catch (e) { // silent — confidence curve is optional } } function renderConfidenceCurve(data) { const ui = window._matchUI || {}; const container = document.getElementById('confidence-curve'); if (!container) return; const points = data.points; if (points.length === 0) return; // Single point: show simple confidence card (no curve needed) if (points.length === 1) { const p = points[0]; const c = p.confidence || Math.max(p.home_prob, p.draw_prob, p.away_prob); let html = '
'; html += '
'; html += '📈'; html += '' + (ui.confidenceTimeline || 'Confidence Timeline') + ''; html += '
'; html += '
'; html += '
' + (c * 100).toFixed(0) + '%
'; html += '
Model confidence — monitoring active
'; html += '
Curve appears when events impact predictions
'; html += '
'; container.innerHTML = html; return; } // Multi-point: draw SVG curve const confidences = points.map(p => { const c = p.confidence || Math.max(p.home_prob, p.draw_prob, p.away_prob); return c; }); const times = points.map(p => { const t = p.time || ''; return t.length >= 16 ? t.slice(5, 16) : t; // MM-DD HH:MM }); const minC = Math.min(...confidences) * 0.9; const maxC = Math.max(...confidences) * 1.05; const range = maxC - minC || 1; const W = 600, H = 140, padL = 50, padR = 30, padT = 15, padB = 30; const plotW = W - padL - padR; const plotH = H - padT - padB; const xScale = (i) => padL + (i / (points.length - 1)) * plotW; const yScale = (v) => padT + plotH - ((v - minC) / range) * plotH; // Build line path let lineD = ''; let fillD = ''; for (let i = 0; i < points.length; i++) { const x = xScale(i); const y = yScale(confidences[i]); if (i === 0) { lineD += 'M' + x.toFixed(1) + ',' + y.toFixed(1); fillD += 'M' + x.toFixed(1) + ',' + (padT + plotH).toFixed(1); fillD += ' L' + x.toFixed(1) + ',' + y.toFixed(1); } else { lineD += ' L' + x.toFixed(1) + ',' + y.toFixed(1); fillD += ' L' + x.toFixed(1) + ',' + y.toFixed(1); } } // Close fill path const lastX = xScale(points.length - 1); fillD += ' L' + lastX.toFixed(1) + ',' + (padT + plotH).toFixed(1) + ' Z'; // Calibration markers let calMarkers = ''; if (data.calibration_flags) { for (const f of data.calibration_flags) { if (!f.has_divergence) continue; const ft = f.time ? f.time.slice(5, 16) : ''; const idx = times.findIndex(t => t >= ft); if (idx >= 0) { const cx = xScale(idx); calMarkers += ''; calMarkers += ''; calMarkers += ''; } } } // X-axis labels (first, last, and significant changes) let xLabels = ''; if (points.length <= 6) { for (let i = 0; i < points.length; i++) { xLabels += '' + esc(times[i]) + ''; } } else { xLabels += '' + esc(times[0]) + ''; const mid = Math.floor(points.length / 2); xLabels += '' + esc(times[mid]) + ''; xLabels += '' + esc(times[points.length - 1]) + ''; } // Y-axis labels const ySteps = 3; let yLabels = ''; for (let i = 0; i <= ySteps; i++) { const val = minC + (range * i / ySteps); const y = yScale(val); yLabels += '' + (val * 100).toFixed(0) + '%'; } // Dots for each point let dots = ''; for (let i = 0; i < points.length; i++) { dots += ''; } // Title with calibration badge let titleExtra = ''; if (data.has_calibration) { const divCount = data.calibration_flags.filter(f => f.has_divergence).length; if (divCount > 0) { titleExtra = ' ' + divCount + ' ' + (ui.signalCount || 'signals') + ''; } } let html = '
'; html += '
'; html += '📈'; html += '' + (ui.confidenceTimeline || 'Confidence Timeline') + ''; html += titleExtra; html += '
'; html += '
'; html += ''; html += ''; // Grid lines for (let i = 0; i <= ySteps; i++) { const y = yScale(minC + (range * i / ySteps)); html += ''; } html += calMarkers; html += ''; html += ''; html += dots; html += yLabels; html += xLabels; html += ''; html += '
'; html += '
'; html += '
' + (ui.fmModelConfidence || 'FM Model Confidence') + '
'; if (data.has_calibration) { html += '
Calibration Signal
'; } html += '
'; html += '
'; container.innerHTML = html; } function renderNarrativeHTML(data) { const ui = window._matchUI || {}; const el = document.getElementById('sidebar'); if (!el) return; const narrative = typeof data === 'string' ? data : data.narrative; if (!narrative) { el.innerHTML = ''; return; } const paragraphs = narrative.split('\n\n').filter(p => p.trim()); let html = '
'; html += '
'; html += '🧠'; html += '' + (ui.aiReport || 'AI Match Report') + ''; html += '
'; html += '
'; for (const p of paragraphs) { html += '

' + esc(p.trim()) + '

'; } html += '
'; if (data.grounded_on && data.grounded_on.length > 0) { html += '
'; html += '' + (ui.groundedOn || 'Grounded on:') + ''; for (const g of data.grounded_on) { html += '#' + g.id + ' ' + esc(g.type) + ''; } html += '
'; } html += '
'; el.innerHTML = html; } function renderNarrativeFromText(text) { const ui = window._matchUI || {}; if (!text) return; const el = document.getElementById('sidebar'); if (!el) return; const paragraphs = text.split('\n\n').filter(p => p.trim()); let html = '
'; html += '
'; html += '🧠'; html += '' + (ui.aiReport || 'AI Match Report') + ''; html += '
'; html += '
'; for (const p of paragraphs) { html += '

' + esc(p.trim()) + '

'; } html += '
'; el.innerHTML = html; } /* ── Fetch & Render ── */ async function load() { // SSR preload: use server-injected data when available (no fetch needed) const preload = window.__PRELOADED_DATA__; const ui = (preload && preload.ui && preload.ui.detail) || {}; window._matchUI = ui; // Localize initial loading text const loadingEl = document.querySelector('#app .loading p'); if (loadingEl) loadingEl.textContent = ui.loading || loadingEl.textContent; if (preload) { try { render({ ok: true, match: preload.match, prediction: preload.prediction, state: preload.state, timeline: preload.timeline, sources: preload.sources, snapshots: preload.snapshots, signals: preload.signals, context: preload.context, }, preload.pulse); // Render cached narrative if available if (preload.narrative) { renderNarrativeFromText(preload.narrative); } else { fetchNarrative(matchId); // fallback to live fetch } // Fetch confidence timeline (not in SSR preload) fetchConfidenceTimeline(matchId); return; } catch (e) { console.warn('Preload render failed, falling back to fetch:', e.message); } } // Normal path: fetch from API try { const pageLang = (document.documentElement.lang || navigator.language || 'en').startsWith('zh') ? 'zh-CN' : 'en'; const [stateRes, pulseRes] = await Promise.all([ fetch('/api/match-state/' + matchId + '?lang=' + pageLang), fetch('/api/match-pulse/' + matchId), ]); if (!stateRes.ok) { const err = await stateRes.json().catch(() => ({ error: 'Failed to load' })); throw new Error(err.error || 'Match not found'); } const data = await stateRes.json(); if (!data.ok) throw new Error(data.error || 'Failed to load match'); // Pulse is optional — if it fails, still render without it let pulse = null; if (pulseRes.ok) { const pulseData = await pulseRes.json().catch(() => null); if (pulseData?.ok) pulse = pulseData.pulse; } render(data, pulse); // Narrative loads async — renders into placeholder when ready fetchNarrative(matchId); } catch (e) { document.getElementById('app').innerHTML = '

Error

' + e.message + '

'; } } function render(data, pulse) { const ui = window._matchUI || {}; const { match, prediction, state, timeline, sources, snapshots } = data; const resultKey = prediction?.predicted || 'D'; const predName = prediction?.predicted_name || RESULT_NAMES[resultKey]; const maxProb = prediction ? Math.max(prediction.home_win, prediction.draw, prediction.away_win) : 0; const topKey = prediction?.home_win === maxProb ? 'home' : prediction?.away_win === maxProb ? 'away' : 'draw'; const homeWin = prediction?.home_win?.toFixed(0) || '--'; const drawPct = prediction?.draw?.toFixed(0) || '--'; const awayWin = prediction?.away_win?.toFixed(0) || '--'; let html = ''; /* ── Module 0: Match Pulse (Signal Compression) ── */ if (pulse) { const tier = pulse.confidence_tier?.label || 'medium'; const dir = pulse.direction_label || {}; const hasDelta = pulse.deltas && (Math.abs(pulse.deltas.home) > 0.01 || Math.abs(pulse.deltas.draw) > 0.01 || Math.abs(pulse.deltas.away) > 0.01); html += '
'; html += '
'; html += '
'; html += ''; html += '' + (ui.matchPulse || 'Match Pulse') + ''; html += '
'; html += '' + tier + ' confidence'; html += '
'; html += '
'; html += '' + (dir.emoji || '➡️') + ''; html += '' + esc(pulse.headline) + ''; html += '
'; html += '
' + esc(pulse.why || 'Monitoring for changes.') + '
'; if (hasDelta) { html += '
'; const h = pulse.deltas.home, d = pulse.deltas.draw, a = pulse.deltas.away; // Normalize: API may return fractions (0-1) or scaled percentages const dScale = (Math.abs(h) + Math.abs(d) + Math.abs(a)) > 10 ? 0.01 : 1; html += 'H ' + (h > 0 ? '+' : '') + (h * dScale).toFixed(1) + '%'; html += 'D ' + (d > 0 ? '+' : '') + (d * dScale).toFixed(1) + '%'; html += 'A ' + (a > 0 ? '+' : '') + (a * dScale).toFixed(1) + '%'; html += '
'; } html += '
'; if (pulse.attribution_confidence != null) { html += 'Attribution:' + (pulse.attribution_confidence * 100).toFixed(0) + '%'; } html += 'Snapshots:' + (pulse.snapshot_count || 0) + ''; html += 'Events:' + (pulse.event_count || 0) + ''; html += '
'; html += '
'; /* end pulse card */ } /* ── Module 1: Prediction ── */ html += '
'; html += '
'; html += '
' + esc(match.home_team) + ' VS ' + esc(match.away_team) + '
'; html += '
'; html += '' + fmtDate(match.date) + ''; html += '' + (ui.matchHash || 'Match #') + match.id + ''; html += '
'; html += '
'; html += '' + predName + ''; html += '
'; html += '
'; html += '
'; html += '
' + homeWin + '%
'; html += '
Home
'; html += '
'; html += '
' + drawPct + '%
'; html += '
Draw
'; html += '
'; html += '
' + awayWin + '%
'; html += '
Away
'; html += '
'; if (prediction?.predicted_score) { html += '
'; html += (ui.predictedScore || 'Predicted Score: ') + '' + prediction.predicted_score + ''; if (prediction.confidence != null) { var conf = prediction.confidence > 1 ? prediction.confidence : prediction.confidence * 100; html += ' · ' + (ui.confidence || 'Confidence') + ': ' + conf.toFixed(1) + '%'; } html += '
'; } html += '
'; /* end prediction card */ /* ── Module 2: Match State ── */ html += '
'; html += '
'; html += '\uD83D\uDCCA'; html += '' + (ui.matchState || 'Match State') + ''; html += '' + (ui.liveIntelligence || 'Live Intelligence') + ''; html += '
'; let hasState = false; for (const evtType of STATE_ORDER) { const events = state[evtType] || []; if (!events.length) continue; hasState = true; html += '
'; html += '
'; html += '' + (STATE_ICONS[evtType] || '\uD83D\uDCF0') + ''; html += '' + (STATE_LABELS[evtType] || evtType) + ''; html += '' + events.length + ''; html += '
'; for (const e of events) { const level = impactLevel(e.impact); const hasImpact = e.impact && (e.impact.home_win || e.impact.draw || e.impact.away_win); html += '
'; html += '
'; html += '
' + esc(e.title) + '
'; if (e.summary) { html += '
' + esc(e.summary.slice(0, 180)) + '
'; } if (hasImpact) { const parts = []; if (e.impact.home_win) parts.push('H: ' + impactDelta(e.impact.home_win)); if (e.impact.draw) parts.push('D: ' + impactDelta(e.impact.draw)); if (e.impact.away_win) parts.push('A: ' + impactDelta(e.impact.away_win)); const impactClass = e.impact.home_win > 0 || e.impact.away_win > 0 ? 'pos' : e.impact.home_win < 0 || e.impact.away_win < 0 ? 'neg' : 'neutral'; html += '' + parts.join(' ') + ''; } if (e.source && !/api.?football/i.test(e.source)) { html += '
' + esc(e.source) + '
'; } html += '
'; } html += '
'; /* end state-group */ } if (!hasState) { html += '
' + (ui.noEvents || 'No event data yet — match state monitoring is active.') + '
'; } html += '
'; /* end state card */ /* ── Module 3: Match Signals (SIGNAL relevance only) ── */ const { signals: sigs = [], context: ctxEvents = [] } = data; const impactfulSignals = sigs.filter(e => e.impact && (e.impact.home_win || e.impact.draw || e.impact.away_win)); html += '
'; html += '
'; html += '📡'; html += '' + (ui.matchSignals || 'Match Signals') + ''; html += '' + sigs.length + ' ' + (ui.signalCount || 'signals') + ''; html += '
'; if (sigs.length > 0) { html += '
'; for (const e of sigs) { const isImpact = impactfulSignals.includes(e); const status = e.match_status; const breakdown = e.match_breakdown ? (typeof e.match_breakdown === 'string' ? JSON.parse(e.match_breakdown) : e.match_breakdown) : null; html += '
'; html += '
' + fmtTime(e.created_at) + ((e.source && !/api.?football/i.test(e.source)) ? ' · ' + esc(e.source) : '') + '
'; html += '
' + esc(e.title.slice(0, 80)) + '
'; if (status === 'pending') { html += '🟡 tentative'; } else if (status === 'auto') { html += '✅ confirmed'; } if (breakdown && breakdown.reason && breakdown.reason.length > 0) { html += '
'; html += breakdown.reason.map(r => '· ' + esc(r) + '').join(''); html += '(tm=' + (breakdown.team_match || 0) + ' dp=' + (breakdown.date_proximity || 0) + ')'; html += '
'; } if (isImpact && e.impact) { html += '
'; if (e.impact.home_win) html += 'Home ' + impactDelta(e.impact.home_win) + ''; if (e.impact.draw) html += 'Draw ' + impactDelta(e.impact.draw) + ''; if (e.impact.away_win) html += 'Away ' + impactDelta(e.impact.away_win) + ''; html += '
'; } html += '
'; } html += '
'; if (impactfulSignals.length > 0) { html += '
'; html += '
⚡ ' + impactfulSignals.length + ' ' + (ui.signalsWithImpact || 'signals with prediction impact') + '
'; for (const e of impactfulSignals) { html += '
'; html += '
' + esc(e.title.slice(0, 80)) + '
'; html += '
'; if (e.impact.home_win) html += 'Home ' + impactDelta(e.impact.home_win) + ''; if (e.impact.draw) html += 'Draw ' + impactDelta(e.impact.draw) + ''; if (e.impact.away_win) html += 'Away ' + impactDelta(e.impact.away_win) + ''; html += '
'; } html += '
'; } } else { html += '
' + (ui.noSignals || 'No match signals detected yet.') + '
'; } html += '
'; /* end signals card */ /* ── Module 4: Tournament Context (folded) ── */ if (ctxEvents.length > 0) { html += '
'; html += '
'; html += '🌐'; html += '' + (ui.tournamentContext || 'Tournament Context') + ''; html += '' + ctxEvents.length + ' items'; html += ''; html += '
'; html += '
'; } /* ── Module 4: Prediction History ── */ html += '
'; html += '
'; html += '\uD83D\uDCC8'; html += '' + (ui.predictionEvolution || 'Prediction Evolution') + ''; html += '' + (snapshots?.length || 0) + ' snapshots'; html += '
'; if (snapshots && snapshots.length > 0) { html += '
'; const reversed = [...snapshots].reverse(); // oldest first for (let i = 0; i < reversed.length; i++) { const s = reversed[i]; const prev = i > 0 ? reversed[i - 1] : null; const contributors = s.contributors_json ? JSON.parse(s.contributors_json) : []; const hRaw = s.home_win || 0, dRaw = s.draw || 0, aRaw = s.away_win || 0; // Normalize: API may return fractions (0-1) or percentages (0-100) const scale = (hRaw + dRaw + aRaw) < 3 ? 100 : 1; const hV = hRaw * scale, dV = dRaw * scale, aV = aRaw * scale; const total = Math.max(hV + dV + aV, 100); const hW = (hV / total * 100).toFixed(0); const dW = (dV / total * 100).toFixed(0); const aW = (aV / total * 100).toFixed(0); html += '
'; html += '
' + (s.created_at ? s.created_at.slice(11, 16) : '') + '
'; html += '
'; html += ''; html += ''; html += ''; html += '
'; html += '
'; html += 'H ' + hV.toFixed(1) + '%'; html += 'D ' + dV.toFixed(1) + '%'; html += 'A ' + aV.toFixed(1) + '%'; if (prev) { const hDiff = hV - (prev.home_win || 0) * (scale || 1); const dDiff = dV - (prev.draw || 0) * (scale || 1); const aDiff = aV - (prev.away_win || 0) * (scale || 1); if (Math.abs(hDiff) > 0.1 || Math.abs(dDiff) > 0.1 || Math.abs(aDiff) > 0.1) { html += ''; if (Math.abs(hDiff) > 0.1) html += 'H' + (hDiff > 0 ? '+' : '') + hDiff.toFixed(1) + '% '; if (Math.abs(dDiff) > 0.1) html += 'D' + (dDiff > 0 ? '+' : '') + dDiff.toFixed(1) + '% '; if (Math.abs(aDiff) > 0.1) html += 'A' + (aDiff > 0 ? '+' : '') + aDiff.toFixed(1) + '%'; html += ''; } } html += '
'; html += '
'; // Contributors if (contributors.length > 0) { html += '
'; html += '' + (ui.drivenBy || 'Driven by: ') + ''; for (let ci = 0; ci < contributors.length; ci++) { const c = contributors[ci]; const stateIcon = STATE_ICONS[c.type] || ''; const confPct = c.confidence_weight ? ' · ' + (c.confidence_weight * 100).toFixed(0) + '% conf' : ''; html += ''; html += stateIcon + ' ' + esc(c.title.slice(0, 40)) + ' · ' + (c.weight * 100).toFixed(0) + '%'; if (c.confidence_weight) { html += '(' + (c.confidence_weight * 100).toFixed(0) + '% conf)'; } html += ''; } html += '
'; } } html += '
'; } else { html += '
' + (ui.noSnapshots || 'No prediction history yet — snapshots are recorded when events impact predictions.') + '
'; } html += '
'; /* end prediction history card */ /* ── Confidence Timeline placeholder (filled async via API) ── */ html += '
'; /* ── Module 5: Sources ── */ html += '
'; html += '
'; html += '\uD83D\uDCD6'; html += '' + (ui.sources || 'Sources') + ''; html += '
'; if (sources.length > 0) { html += '
'; for (const s of sources) { html += (s.url ? '' + esc(s.source) + '' : '' + esc(s.source) + ''); } html += '
'; } else { html += '
' + (ui.noSources || 'No sources available.') + '
'; } html += '
'; /* end sources card */ document.getElementById('app').innerHTML = html; } function esc(s) { if (!s) return ''; return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } load(); })(); // ── Feedback Widget + Scroll Tracking ── (function(){ var matchId = (window.__PRELOADED_DATA__ && window.__PRELOADED_DATA__.match && window.__PRELOADED_DATA__.match.id) || null; if (!matchId) return; var ui = window._matchUI || {}; // Create feedback widget var fb = document.createElement('div'); fb.id = 'feedback-widget'; fb.innerHTML = '
' + (ui.feedbackQuestion || 'Was this explanation useful?') + '
'; document.querySelector('.page')?.appendChild(fb); var sent = false; function sendFeedback(type) { if (sent) return; sent = true; fetch('/api/feedback', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ match_id: matchId, feedback_type: type, page_url: location.href }) }).catch(function(){}); var btns = fb.querySelectorAll('.fb-btn'); btns.forEach(function(b){ b.disabled = true; b.style.opacity = '0.5'; }); fb.querySelector('.fb-q').textContent = type === 'useful' ? (ui.thanksUseful || 'Thanks for your feedback! 👍') : (ui.thanksImprove || "Thanks — we'll improve! 👎"); if (typeof gtag !== 'undefined') gtag('event', 'feedback', { event_category: 'engagement', event_label: type, match_id: matchId }); } fb.querySelector('.fb-yes').onclick = function(){ sendFeedback('useful'); }; fb.querySelector('.fb-no').onclick = function(){ sendFeedback('not_useful'); }; // Scroll depth tracking via IntersectionObserver var depths = {}; var sections = document.querySelectorAll('h2'); var observer = new IntersectionObserver(function(entries){ entries.forEach(function(e){ if (e.isIntersecting) { var text = e.target.textContent.trim(); if (!depths[text]) { depths[text] = true; // Send to GA if (typeof gtag !== 'undefined') gtag('event', 'scroll_depth', { event_category: 'engagement', event_label: text, match_id: matchId }); // Also send to our analytics endpoint fetch('/api/scroll', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ match_id: matchId, section_label: text }) }).catch(function(){}); } } }); }, { threshold: 0.5 }); sections.forEach(function(s){ observer.observe(s); }); // Copy link / Share button var share = document.createElement('div'); share.id = 'share-btn'; share.innerHTML = ''; share.querySelector('#share-link-btn').onclick = function(){ var btn = this; navigator.clipboard.writeText(location.href).then(function(){ btn.textContent = (ui.copiedLink || '📋 Copied!'); setTimeout(function(){ btn.textContent = '🔗 ' + (ui.share || 'Share'); }, 2000); }); }; fb.querySelector('.fb-inner').appendChild(share); })(); } // end _mainInit // Poll until _matchUI is ready, then run main init (function poll() { if (window._matchUIReady) { _mainInit(); } else { setTimeout(poll, 15); } })();