"use strict"; const $ = (id) => document.getElementById(id); const REFRESH_INTERVAL = 60_000; const REQUEST_TIMEOUT = 15_000; const ICONS = Object.freeze({ alert: ``, copy: ``, shield: ``, balance: ``, gift: `` }); const TICKET_PROFILES = Object.freeze({ safe: { title: "SAFE", subtitle: "Exposição controlada", risk: "RISCO MENOR", icon: ICONS.shield }, balanced: { title: "BALANCEADO", subtitle: "Risco e retorno ajustados", risk: "RISCO MÉDIO", icon: ICONS.balance }, freebet: { title: "FREEBET", subtitle: "Retorno potencial elevado", risk: "RISCO MAIOR", icon: ICONS.gift } }); const safe = (value) => String(value ?? "").replace(/[&<>'"]/g, (char) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ })[char]); const finite = (value) => { if (value === null || value === undefined || value === "") return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; }; const num = (value, digits = 1) => { const parsed = finite(value); return parsed === null ? "—" : parsed.toLocaleString("pt-BR", { minimumFractionDigits: digits, maximumFractionDigits: digits }); }; const pct = (value) => { const parsed = finite(value); return parsed === null ? "—" : (parsed * 100).toLocaleString("pt-BR", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + "%"; }; const integer = (value) => { const parsed = finite(value); return parsed === null ? "—" : Math.round(parsed).toLocaleString("pt-BR"); }; function localDate(value) { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" }); } function teamInitials(name) { const words = String(name || "?").trim().split(/\s+/).filter(Boolean); const initials = words.length > 1 ? words[0][0] + words[words.length - 1][0] : words[0]?.slice(0, 2); return String(initials || "?").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase(); } function boundedScore(value) { const parsed = finite(value); return parsed === null ? 0 : Math.max(0, Math.min(100, parsed)); } function setStatus(status) { const pill = $("system-pill"); const statuses = { ok: { tone: "ok", pill: "SISTEMA ONLINE", engine: "MOTOR DE PRECISÃO ONLINE", console: "ONLINE", flow: "" }, degraded: { tone: "warning", pill: "OPERAÇÃO PARCIAL", engine: "MOTOR ONLINE COM ALERTAS", console: "ATENÇÃO", flow: "paused" }, scanning: { tone: "ok", pill: "ANALISANDO", engine: "VARREDURA EM ANDAMENTO", console: "PROCESSANDO", flow: "" }, waiting: { tone: "neutral", pill: "AGUARDANDO", engine: "AGUARDANDO PRIMEIRO SCAN", console: "STANDBY", flow: "paused" }, interrupted: { tone: "bad", pill: "INTERROMPIDO", engine: "VARREDURA INTERROMPIDA", console: "ATENÇÃO", flow: "failed" }, configuration_error: { tone: "bad", pill: "CONFIGURAÇÃO", engine: "CONFIGURAÇÃO NECESSÁRIA", console: "ATENÇÃO", flow: "failed" }, error: { tone: "bad", pill: "INDISPONÍVEL", engine: "MOTOR INDISPONÍVEL", console: "ERRO", flow: "failed" } }; const current = statuses[status] || { tone: "neutral", pill: String(status || "AGUARDANDO").toUpperCase(), engine: "MOTOR DE PRECISÃO EM ESPERA", console: "STANDBY", flow: "paused" }; pill.className = `pill ${current.tone}`; pill.innerHTML = `${safe(current.pill)}`; const engineLabel = $("engine-label"); const engineState = $("engine-state"); const engineFlow = $("engine-flow"); const stateClass = current.tone === "bad" ? "danger" : current.tone === "neutral" || current.tone === "warning" ? "paused" : ""; if (engineLabel) { engineLabel.className = `live-label ${stateClass}`.trim(); engineLabel.innerHTML = `${safe(current.engine)}`; } if (engineState) { engineState.className = `console-state ${stateClass}`.trim(); engineState.innerHTML = `${safe(current.console)}`; } if (engineFlow) engineFlow.className = `engine-flow ${current.flow}`.trim(); } function warnings(items) { const values = Array.isArray(items) ? items.filter((item) => item !== null && item !== undefined && item !== "") : []; $("warnings").innerHTML = values.map((item) => `
${ICONS.alert}${safe(item)}
`).join(""); } function modelRow(label, value) { return `
${label}${value}
`; } function pickCard(pick, index) { const p = pick && typeof pick === "object" ? pick : {}; const detail = p.model_detail && typeof p.model_detail === "object" ? p.model_detail : {}; const score = boundedScore(p.safe_score); const reasons = Array.isArray(p.reasons) ? p.reasons.map((reason) => `${safe(reason)}`).join("") : ""; const oddInputId = `market-odd-${index}`; const selection = p.selection ? `${p.selection} vence` : "Seleção indisponível"; const probability = finite(p.probability); const modelRows = [ modelRow("Dixon-Coles / Poisson", pct(detail.poisson)), modelRow("Elo", pct(detail.elo)), modelRow("Forma ponderada", pct(detail.form)), modelRow("Modelo interno", pct(p.raw_model_probability)), modelRow("Mercado sem margem", pct(p.market_probability)), modelRow("Peso do modelo interno", pct(detail.internal_weight)), modelRow("Pesos Poisson / Elo / Forma", `${pct(detail.weight_poisson)} / ${pct(detail.weight_elo)} / ${pct(detail.weight_form)}`), modelRow("Amostra walk-forward / holdout", `${integer(detail.tuning_total_samples)} / ${integer(detail.tuning_samples)}`), modelRow("Brier Skill vs. climatologia", detail.tuning_brier_skill == null ? "—" : pct(detail.tuning_brier_skill)), modelRow("Validação do modelo", detail.model_validation == null ? "—" : pct(detail.model_validation)), modelRow("Piso Poisson / Elo", detail.core_model_floor == null ? "—" : pct(detail.core_model_floor)), modelRow("Ganho Brier tuning", detail.tuning_gain == null ? "—" : num(detail.tuning_gain, 4)), modelRow("Qualidade dos dados", pct(p.quality)), modelRow("Confiabilidade composta", pct(p.reliability)), modelRow("Casas no consenso", integer(p.market_bookmakers)), modelRow("Dispersão do mercado", pct(p.market_dispersion)), modelRow("Confiança no nome do time", pct(p.name_confidence)), modelRow("Calibração forward", p.calibration_delta == null ? "—" : `${num(Number(p.calibration_delta) * 100, 1)} pp`), modelRow("Edge vs. mercado", pct(p.edge)) ].join(""); return `
#${String(index + 1).padStart(2, "0")} · ${safe(p.label || "SAFE")} ${safe(p.competition || "Competição")} Início · ${safe(localDate(p.kickoff))}
${safe(p.home || "Mandante")}
VS
${safe(p.away || "Visitante")}
SELEÇÃO DO MODELO${safe(selection)}
APROVADA
PROBABILIDADE${pct(p.probability)}
CONSERVADORA${pct(p.conservative_probability)}
ODD MERCADO${num(p.odd, 2)}
ODD JUSTA${num(p.fair_odd, 2)}
FORÇA DO SINAL ${num(score, 1)}% ${num(score, 1)}/100
${reasons ? `
${reasons}
` : ""}
Auditoria de modelos e qualidade
${modelRows}
Digite uma odd para estimar o valor esperado.
`; } function radarSignalValues(value) { const entries = Array.isArray(value) ? value : value == null ? [] : [value]; return entries.map((entry) => { if (typeof entry === "string" || typeof entry === "number") return String(entry).trim(); if (!entry || typeof entry !== "object") return ""; return String(entry.message ?? entry.reason ?? entry.label ?? entry.code ?? "").trim(); }).filter(Boolean).slice(0, 6); } function radarTags(items, className = "") { return items.map((item) => `${safe(item)}`).join(""); } function radarCard(item, index) { const p = item && typeof item === "object" ? item : {}; const blockers = radarSignalValues(p.blockers); const reasons = radarSignalValues(p.reasons); const scoreValue = finite(p.safe_score); const score = scoreValue === null ? null : Math.max(0, Math.min(100, scoreValue)); const selection = p.selection ? `${p.selection} vence` : "Seleção em análise"; const blockerMarkup = blockers.length ? radarTags(blockers, "blocker") : `Critérios finais do Risk Gate não atendidos`; const reasonGroup = reasons.length ? `
SINAIS OBSERVADOS
${radarTags(reasons)}
` : ""; return `
WATCH ${String(index + 1).padStart(2, "0")} ${safe(p.competition || "Competição")} Início · ${safe(localDate(p.kickoff))}
${safe(p.home || "Mandante")}
VS
${safe(p.away || "Visitante")}
LEITURA EM ANÁLISE${safe(selection)}
EM OBSERVAÇÃO — NÃO APROVADA
PROBABILIDADE${pct(p.probability)}
CONSERVADORA${pct(p.conservative_probability)}
SAFE SCORE${score === null ? "—" : num(score, 1)}
ODD MERCADO${num(p.odd, 2)}
ODD JUSTA${num(p.fair_odd, 2)}
EV DO MODELO${pct(p.model_ev)}
POR QUE NÃO PASSOU
${blockerMarkup}
${reasonGroup}
`; } function renderRadar(state) { const source = Array.isArray(state.radar) ? state.radar : Array.isArray(state.radar?.items) ? state.radar.items : []; const items = source.filter((item) => item && typeof item === "object" && item.approved === false).slice(0, 5); const container = $("radar-cards"); if (!container) return; container.innerHTML = items.length ? items.map(radarCard).join("") : `
Nenhum palpite em observaçãoO radar está vazio nesta leitura porque nenhum evento chegou à etapa de modelagem.
`; container.setAttribute("aria-busy", "false"); } function emptyTicket(profileName) { const profile = TICKET_PROFILES[profileName]; return `
${profile.icon}

${profile.title}

${profile.subtitle}
${profile.risk}
Bilhete ainda indisponívelSão necessárias mais seleções aprovadas para montar este perfil.
`; } function ticketCard(profileName, ticket) { const profile = TICKET_PROFILES[profileName]; if (!ticket || typeof ticket !== "object") return emptyTicket(profileName); const legs = Array.isArray(ticket.legs) ? ticket.legs : []; const legMarkup = legs.map((leg) => `
${safe(leg?.selection)} vence @ ${num(leg?.odd, 2)} ${safe(leg?.match)} · score ${num(leg?.safe_score, 1)}
`).join(""); const targetMet = ticket.target_met === true; return `
${profile.icon}

${profile.title}

${profile.subtitle}
${profile.risk}
ODD COMBINADA@ ${num(ticket.total_odd, 2)}
PROB. EST.${pct(ticket.joint_probability)}
STRESS${pct(ticket.joint_conservative)}
EV EST.${pct(ticket.estimated_ev)}
${targetMet ? "Meta atingida" : "Abaixo da meta"} · alvo @ ${num(ticket.target_odd, 1)}
${legMarkup || `
Nenhuma perna disponível.
`}
`; } function healthCard(label, value, note = "") { return `
${label}${value}${note ? `${note}` : ""}
`; } function providerValue(isOnline, isDegraded) { const tone = isOnline ? "online" : isDegraded ? "partial" : ""; const label = isOnline ? "ONLINE" : isDegraded ? "PARCIAL" : "AGUARDANDO"; return `${label}`; } function renderHealth(state) { const quota = state.providers?.odds_api?.quota || {}; const degraded = state.status === "degraded"; const cards = [ healthCard("FOOTBALL DATA", providerValue(Boolean(state.providers?.football_data?.ok), degraded), "dados de jogos"), healthCard("ODDS API", providerValue(Boolean(state.providers?.odds_api?.ok), degraded), "consenso de mercado"), healthCard("CRÉDITOS ODDS", safe(integer(quota.remaining)), "consultas restantes"), healthCard("BASE HISTÓRICA", `${safe(integer(state.summary?.historical_matches))} jogos`, "amostra disponível"), healthCard("PALPITES LIQUIDADOS", safe(integer(state.performance?.settled)), "forward tracking"), healthCard("ROI FORWARD", safe(pct(state.performance?.roi)), "retorno observado"), healthCard("BRIER SCORE", safe(num(state.performance?.brier_score, 3)), "erro probabilístico"), healthCard("CALIBRAÇÃO ECE", safe(pct(state.performance?.ece)), "desvio de calibração"), healthCard("VERSÃO DO MODELO", safe(state.model_version || "—"), "motor em execução") ]; $("health").innerHTML = cards.join(""); $("health").setAttribute("aria-busy", "false"); } function renderState(payload) { const state = payload && typeof payload === "object" ? payload : {}; const picks = Array.isArray(state.picks) ? state.picks : []; const radarCount = Array.isArray(state.radar) ? state.radar.length : Array.isArray(state.radar?.items) ? state.radar.items.length : 0; setStatus(state.status || "waiting"); $("s-events").textContent = integer(state.summary?.events); $("s-approved").textContent = integer(state.summary?.approved ?? picks.length); $("s-best").textContent = picks.length ? num(Math.max(...picks.map((item) => boundedScore(item?.safe_score))), 1) : "—"; $("s-winrate").textContent = state.performance?.win_rate == null ? "—" : pct(state.performance.win_rate); $("last-update").textContent = `Atualizado · ${localDate(state.generated_at)}`; warnings(state.warnings); $("picks").innerHTML = picks.length ? picks.map(pickCard).join("") : `
Nenhuma aposta SAFE foi liberadaO Risk Gate não força entradas. ${radarCount ? `${radarCount} jogo${radarCount === 1 ? '' : 's'} está${radarCount === 1 ? '' : 'ão'} em observação no Radar.` : 'Os times monitorados ficam no Radar, sem recomendação de aposta.'}Ver times no Radar
`; $("picks").setAttribute("aria-busy", "false"); renderRadar(state); const tickets = state.tickets && typeof state.tickets === "object" ? state.tickets : {}; $("tickets").innerHTML = ["safe", "balanced", "freebet"].map((key) => ticketCard(key, tickets[key])).join(""); $("tickets").setAttribute("aria-busy", "false"); renderHealth(state); } function setRegionsBusy(isBusy) { ["picks", "radar-cards", "tickets", "health"].forEach((id) => $(id)?.setAttribute("aria-busy", String(isBusy))); } let activeController = null; let toastTimer = null; function showToast(message) { const toast = $("toast"); if (!toast) return; toast.textContent = message; toast.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove("show"), 2300); } async function load({ force = false, announce = false } = {}) { if (activeController && !force) return; if (activeController) activeController.abort(); const controller = new AbortController(); activeController = controller; const refreshButton = $("refresh-ui"); setRegionsBusy(true); refreshButton.classList.add("loading"); refreshButton.disabled = true; refreshButton.setAttribute("aria-label", "Atualizando painel"); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); try { const response = await fetch("/api/state", { cache: "no-store", headers: { Accept: "application/json" }, signal: controller.signal }); if (!response.ok) throw new Error(`servidor respondeu ${response.status}`); const state = await response.json(); renderState(state); if (announce) showToast("Painel atualizado com sucesso."); } catch (error) { if (error.name === "AbortError" && activeController !== controller) return; setStatus("error"); const message = error.name === "AbortError" ? "A atualização demorou mais que o esperado. Tente novamente." : `Falha ao carregar o painel: ${error.message}`; warnings([message]); setRegionsBusy(false); if (announce) showToast("Não foi possível atualizar o painel."); } finally { clearTimeout(timeout); if (activeController === controller) { activeController = null; refreshButton.classList.remove("loading"); refreshButton.disabled = false; refreshButton.setAttribute("aria-label", "Atualizar painel"); } } } window.evalOdd = function evalOdd(input, probability) { if (!input) return; const result = input.closest(".oddbox")?.querySelector(".oddresult"); if (!result) return; const odd = finite(input.value); const p = finite(probability); result.className = "oddresult"; if (odd === null || odd <= 1) { result.textContent = input.value ? "Digite uma odd válida, acima de 1,00." : "Digite uma odd para estimar o valor esperado."; return; } if (p === null || p <= 0) { result.textContent = "Probabilidade indisponível para esta comparação."; return; } const expectedValue = p * odd - 1; const fairOdd = 1 / p; if (expectedValue >= .03) { result.classList.add("good"); result.textContent = `Valor positivo · EV +${num(expectedValue * 100, 1)}% · odd justa ${num(fairOdd, 2)}`; } else if (expectedValue >= -.02) { result.classList.add("fair"); result.textContent = `Preço próximo do justo · EV ${num(expectedValue * 100, 1)}% · justa ${num(fairOdd, 2)}`; } else { result.classList.add("weak"); result.textContent = `Preço abaixo do modelo · EV ${num(expectedValue * 100, 1)}% · justa ${num(fairOdd, 2)}`; } }; async function writeClipboard(text) { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return; } const field = document.createElement("textarea"); field.value = text; field.setAttribute("readonly", ""); field.className = "clipboard-fallback"; document.body.appendChild(field); field.select(); const copied = document.execCommand("copy"); field.remove(); if (!copied) throw new Error("cópia indisponível"); } window.copyText = async function copyText(button) { if (!button) return; const text = button.dataset.copy || ""; if (!text) return; try { await writeClipboard(text); showToast("Copiado para a área de transferência."); } catch (_error) { showToast("Não foi possível copiar automaticamente."); } }; document.addEventListener("click", (event) => { const copyButton = event.target.closest(".copybtn"); if (copyButton) window.copyText(copyButton); const scrollButton = event.target.closest("[data-scroll-target]"); if (scrollButton) $(scrollButton.dataset.scrollTarget)?.scrollIntoView({ behavior: "smooth", block: "start" }); }); document.addEventListener("input", (event) => { const input = event.target.closest(".market-odd-input"); if (input) window.evalOdd(input, input.dataset.probability); }); $("refresh-ui").addEventListener("click", () => load({ force: true, announce: true })); load(); setInterval(() => { if (!document.hidden) load(); }, REFRESH_INTERVAL);