// Fantabeto PWA — Auction Assistant const API = "/api"; const state = { tab: "auction", players: [], roster: [], budgetSpent: 0, budgetTotal: 500, selectedPlayer: null, search: "", roleFilter: "", bidAmount: 0, online: true, }; // ── Init ──────────────────────────────────────────────────────── async function init() { registerSW(); await loadRoster(); await searchPlayers(); await checkHealth(); setInterval(checkHealth, 30000); } function registerSW() { if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js"); } } async function api(url, opts = {}) { try { const res = await fetch(url, opts); state.online = true; updatePing(); return await res.json(); } catch (e) { state.online = false; updatePing(); return null; } } async function checkHealth() { const r = await api(API + "/stats"); document.getElementById("ping").className = state.online ? "ping" : "ping offline"; document.getElementById("ping").textContent = state.online ? "● connected" : "○ offline"; } function updatePing() { const el = document.getElementById("ping"); if (!el) return; el.className = state.online ? "ping" : "ping offline"; el.textContent = state.online ? "● connected" : "○ offline"; } // ── Search ────────────────────────────────────────────────────── async function searchPlayers() { const params = new URLSearchParams({ limit: 60, sort: "projected_points" }); if (state.search) params.set("search", state.search); if (state.roleFilter) params.set("role", state.roleFilter); const data = await api(API + "/players?" + params); state.players = data?.players || []; renderPlayerList(); } // ── Roster ────────────────────────────────────────────────────── async function loadRoster() { const data = await api(API + "/roster"); if (data) { state.roster = data.roster || []; state.budgetSpent = data.budget_spent || 0; } renderBudget(); renderRoster(); } async function addToRoster(name, price) { const data = await api(API + "/roster/add", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, price }), }); if (data?.error) { toast(data.error, "error"); return false; } if (data?.ok) { state.roster = data.roster.roster; state.budgetSpent = data.roster.budget_spent; renderBudget(); renderRoster(); renderPlayerList(); toast(`${name} added · ${price} cr`); return true; } return false; } async function removeFromRoster(name) { const data = await api(API + "/roster/remove", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); if (data?.ok) { state.roster = data.roster.roster; state.budgetSpent = data.roster.budget_spent; renderBudget(); renderRoster(); renderPlayerList(); toast(`Removed ${name}`); } } async function resetRoster() { if (!confirm("Reset entire roster?")) return; await api(API + "/roster/reset", { method: "POST" }); state.roster = []; state.budgetSpent = 0; renderBudget(); renderRoster(); renderPlayerList(); } // ── Player detail ─────────────────────────────────────────────── async function showPlayer(name) { const data = await api(API + "/player/" + encodeURIComponent(name)); if (!data) return; state.selectedPlayer = data; state.bidAmount = data.recommended_bid || 0; renderDetail(); } function closeDetail() { state.selectedPlayer = null; renderDetail(); } function setBid(amount) { state.bidAmount = amount; document.getElementById("bidInput").value = amount; document.querySelectorAll(".bid-quick button").forEach((b) => { const v = parseInt(b.dataset.bid); b.className = v === amount ? "active" : ""; }); } function formatBidButtons(player) { const bids = []; const rec = player.recommended_bid || 0; const max = player.max_bid || 0; const mkt = player.market_value || 0; if (mkt > 0) bids.push(mkt); if (rec > 0 && rec !== mkt) bids.push(rec); if (max > 0 && max !== rec && max !== mkt) bids.push(max); // Ensure at least 3 options if (bids.length < 3 && mkt > 10) bids.push(Math.round(mkt * 1.3)); return [...new Set(bids)].sort((a, b) => a - b); } // ── Render ────────────────────────────────────────────────────── function renderBudget() { const spent = state.budgetSpent; const total = state.budgetTotal; const pct = Math.min((spent / total) * 100, 100); document.getElementById("budgetSpent").textContent = spent; document.getElementById("budgetRemaining").textContent = total - spent; document.getElementById("budgetFill").style.width = pct + "%"; document.getElementById("budgetFill").style.background = pct > 90 ? "var(--red)" : pct > 70 ? "var(--gold)" : "var(--green)"; } function renderPlayerList() { const el = document.getElementById("playerList"); const inRoster = new Set(state.roster.map((p) => p.name)); if (!state.players.length) { el.innerHTML = '
No players found
'; return; } el.innerHTML = state.players .map((p) => { const added = inRoster.has(p.name); return `
${p.role}
${p.name}
${p.team || ""}
FV ${p.projected_points?.toFixed(1) || "—"} Start ${Math.round((p.starter_pct || 0) * 100)}% Games ${p.games_played || "—"}
${p.market_value} cr
${added ? "OWNED" : "market"}
`; }) .join(""); } function renderRoster() { const el = document.getElementById("rosterList"); if (!state.roster.length) { el.innerHTML = '
No players drafted yet. Search and add players.
'; return; } const roleOrder = { P: "🧤 Goalkeepers", D: "🛡 Defenders", C: "⚙ Midfielders", A: "⚡ Forwards" }; let html = ""; for (const [role, label] of Object.entries(roleOrder)) { const players = state.roster.filter((p) => p.role === role); if (!players.length) continue; html += `

${label} (${players.length})

`; html += players .map( (p) => `
${p.role}
${p.name}
${p.team}
${p.price} cr
` ) .join(""); html += "
"; } el.innerHTML = html; } function renderDetail() { const el = document.getElementById("detailOverlay"); if (!state.selectedPlayer) { el.style.display = "none"; return; } const p = state.selectedPlayer; const bidButtons = formatBidButtons(p); const remaining = state.budgetTotal - state.budgetSpent; el.style.display = "flex"; el.innerHTML = `
${p.role}
${p.name}
${p.team}
Projected Fantavoto
Median (P50)${p.p50}
Floor (P10)${p.p10}
Ceiling (P90)${p.p90}
Risk Assessment
Downside Risk P(FV < 5.5)${(p.downside_risk * 100).toFixed(0)}%
Starter Probability${(p.starter_prob * 100).toFixed(0)}%
Games Played${p.games_played}
Auction Intelligence
Market Value${p.market_value} cr
ML Recommended Bid${p.recommended_bid} cr
Estimated Opponent Bid${p.opponent_bid} cr
Max Rational Bid${p.max_bid} cr
Value: ${p.p50 / Math.max(p.recommended_bid || 1, 1).toFixed(3)} FV/cr Budget left: ${remaining} cr
${bidButtons.map((b) => { const over = b > remaining; return ``; }).join("")}
${state.roster.some((r) => r.name === p.name) ? `
` : ""}
`; // Focus bid input setTimeout(() => { const inp = document.getElementById("bidInput"); if (inp) { inp.focus(); inp.select(); } }, 100); } // ── Actions ───────────────────────────────────────────────────── async function doBid(name) { const success = await addToRoster(name, state.bidAmount); if (success) { state.bidAmount = 0; closeDetail(); } } function toast(msg, type = "") { const el = document.createElement("div"); el.className = "toast" + (type === "error" ? " error" : ""); el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 2000); } // ── Tab switching ─────────────────────────────────────────────── function switchTab(tab) { state.tab = tab; document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.tab === tab)); document.getElementById("auctionView").style.display = tab === "auction" ? "block" : "none"; document.getElementById("rosterView").style.display = tab === "roster" ? "block" : "none"; document.getElementById("statsView").style.display = tab === "stats" ? "block" : "none"; if (tab === "stats") loadStats(); if (tab === "roster") loadRoster(); } async function loadStats() { const data = await api(API + "/stats"); if (!data) return; document.getElementById("statsView").innerHTML = `
${data.total_players}
Players
${data.elite_count}
Elite (>100cr)
${data.median_price}
Median Price (cr)
${data.price_range[0]}–${data.price_range[1]}
Price Range (cr)
${Object.entries(data.role_counts).map(([r, c]) => `
${c}
${r === 'P' ? 'Goalkeepers' : r === 'D' ? 'Defenders' : r === 'C' ? 'Midfielders' : 'Forwards'}
`).join("")}
`; } // ── Event bindings ────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", init);