const state = { teams: [], simulations: [], selectedDetailId: null, pollHandle: null, }; const $ = (id) => document.getElementById(id); function setStatus(message) { $("statusText").textContent = message; } async function request(path, options = {}) { const response = await fetch(path, options); if (!response.ok) { let msg = `${response.status} ${response.statusText}`; try { const body = await response.json(); if (body && body.error) { msg = body.error; } } catch (_) {} throw new Error(msg); } if (response.status === 204) { return null; } const contentType = response.headers.get("content-type") || ""; if (contentType.includes("application/json")) { return response.json(); } return response.text(); } function openModal(modalId) { const modal = $(modalId); if (!modal) return; modal.classList.add("open"); modal.setAttribute("aria-hidden", "false"); } function closeModal(modalId) { const modal = $(modalId); if (!modal) return; modal.classList.remove("open"); modal.setAttribute("aria-hidden", "true"); } function getModeTeamCount(mode) { return mode === "single" ? 2 : 4; } function renderTeamSelectors() { const wrap = $("teamSelectWrap"); const mode = $("modeSelect").value; const required = getModeTeamCount(mode); const autoFill = $("autoFill").checked; $("teamCount").innerHTML = ``; if (state.teams.length === 0) { wrap.innerHTML = "

Loading teams...

"; return; } const options = state.teams .map((team) => ``) .join(""); const fields = []; for (let i = 0; i < required; i++) { fields.push(` `); } wrap.innerHTML = fields.join(""); } function getCreatePayload() { const mode = $("modeSelect").value; const autoFill = $("autoFill").checked; const required = getModeTeamCount(mode); if (autoFill) { return { mode, auto_fill: true }; } const picks = [...$("teamSelectWrap").querySelectorAll("select")].map((s) => s.value); const uniqueCount = new Set(picks).size; if (picks.length !== required || uniqueCount !== picks.length) { throw new Error("Please select unique teams for this mode"); } return { mode, auto_fill: false, teams: picks }; } function cardActions(sim) { const common = ` `; if (sim.status === "pending") { return ` ${common} `; } if (sim.status === "completed") { return ` ${common} `; } return common; } function createCardElement(sim) { const article = document.createElement("article"); article.className = "card"; article.dataset.id = String(sim.id); article.innerHTML = `

Progress:

Outcome:

`; return article; } function setTextIfChanged(element, nextValue) { if (!element) return; if (element.textContent !== nextValue) { element.textContent = nextValue; } } function updateCardElement(card, sim) { setTextIfChanged(card.querySelector('[data-role="title"]'), sim.title); setTextIfChanged(card.querySelector('[data-role="idline"]'), `sim-${sim.id} | ${sim.mode} | seed=${sim.seed}`); setTextIfChanged(card.querySelector('[data-role="progress"]'), sim.progress); setTextIfChanged(card.querySelector('[data-role="scoreboard"]'), sim.scoreboard); setTextIfChanged(card.querySelector('[data-role="outcome"]'), sim.outcome); const pill = card.querySelector('[data-role="pill"]'); const nextPillClass = `pill ${sim.status}`; if (pill.className !== nextPillClass) { pill.className = nextPillClass; } setTextIfChanged(pill, sim.status); if (card.dataset.status !== sim.status) { card.dataset.status = sim.status; card.querySelector('[data-role="actions"]').innerHTML = cardActions(sim); } } function renderDashboard() { const root = $("dashboard"); if (!root) return; if (state.simulations.length === 0) { for (const card of root.querySelectorAll("article.card")) { card.remove(); } if (!$("dashboardEmpty")) { root.innerHTML = `
No simulation instances yet. Create one to start the web simulation flow.
`; } return; } const emptyNode = $("dashboardEmpty"); if (emptyNode) { emptyNode.remove(); } const existingCards = new Map( [...root.querySelectorAll("article.card")].map((card) => [Number(card.dataset.id), card]), ); const activeIds = new Set(state.simulations.map((sim) => sim.id)); for (let index = 0; index < state.simulations.length; index += 1) { const sim = state.simulations[index]; let card = existingCards.get(sim.id); if (!card) { card = createCardElement(sim); root.appendChild(card); } updateCardElement(card, sim); if (root.children[index] !== card) { root.insertBefore(card, root.children[index] || null); } } for (const [id, card] of existingCards.entries()) { if (!activeIds.has(id)) { card.remove(); } } } function renderStats() { const total = state.simulations.length; const running = state.simulations.filter((s) => s.status === "running").length; const completed = state.simulations.filter((s) => s.status === "completed").length; $("statInstances").textContent = String(total); $("statRunning").textContent = String(running); $("statCompleted").textContent = String(completed); $("statTeams").textContent = String(state.teams.length); } async function refreshSimulations() { state.simulations = await request("/api/simulations"); renderStats(); renderDashboard(); if (state.selectedDetailId !== null) { const exists = state.simulations.some((s) => s.id === state.selectedDetailId); if (!exists) { state.selectedDetailId = null; closeModal("detailModal"); return; } await loadDetail(state.selectedDetailId); } } function textOrPlaceholder(lines, fallback) { if (!Array.isArray(lines) || lines.length === 0) { return fallback; } return lines.join("\n"); } async function loadDetail(id) { const detail = await request(`/api/simulations/${id}`); state.selectedDetailId = id; $("detailTitle").textContent = `sim-${detail.id} - ${detail.title}`; $("detailScoreboard").textContent = `${detail.scoreboard} | ${detail.outcome}`; $("detailLogs").textContent = textOrPlaceholder(detail.logs, "No events yet."); $("detailStats").textContent = textOrPlaceholder(detail.stats_lines, "No stats available yet."); $("detailCompetition").textContent = textOrPlaceholder(detail.competition_lines, "No standings/bracket available yet."); $("detailHistory").textContent = textOrPlaceholder(detail.history_lines, "No history recorded yet."); } async function createSimulation() { try { const payload = getCreatePayload(); const created = await request("/api/simulations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); closeModal("createModal"); setStatus(`Created sim-${created.id}`); await refreshSimulations(); } catch (error) { setStatus(`Create failed: ${error.message}`); } } async function startSimulation(id) { try { await request(`/api/simulations/${id}/start`, { method: "POST" }); setStatus(`Started sim-${id}`); await refreshSimulations(); } catch (error) { setStatus(`Start failed: ${error.message}`); } } async function cloneSimulation(id) { try { const created = await request(`/api/simulations/${id}/clone`, { method: "POST" }); setStatus(`Cloned sim-${id} to sim-${created.id}`); await refreshSimulations(); } catch (error) { setStatus(`Clone failed: ${error.message}`); } } async function deleteSimulation(id) { try { await request(`/api/simulations/${id}`, { method: "DELETE" }); if (state.selectedDetailId === id) { state.selectedDetailId = null; closeModal("detailModal"); } setStatus(`Deleted sim-${id}`); await refreshSimulations(); } catch (error) { setStatus(`Delete failed: ${error.message}`); } } function exportSimulation(id) { window.location.href = `/api/simulations/${id}/export.csv`; setStatus(`Exporting sim-${id} CSV...`); } function bindEvents() { $("openCreateBtn").addEventListener("click", () => openModal("createModal")); $("createBtn").addEventListener("click", createSimulation); $("modeSelect").addEventListener("change", renderTeamSelectors); $("autoFill").addEventListener("change", renderTeamSelectors); document.querySelectorAll("[data-close]").forEach((button) => { button.addEventListener("click", () => { closeModal(button.dataset.close); if (button.dataset.close === "detailModal") { state.selectedDetailId = null; } }); }); ["createModal", "detailModal"].forEach((id) => { const modal = $(id); modal.addEventListener("click", (event) => { if (event.target === modal) { closeModal(id); if (id === "detailModal") { state.selectedDetailId = null; } } }); }); $("dashboard").addEventListener("click", async (event) => { const button = event.target.closest("button[data-action]"); if (!button) return; const id = Number(button.dataset.id); const action = button.dataset.action; if (action === "start") return startSimulation(id); if (action === "clone") return cloneSimulation(id); if (action === "delete") return deleteSimulation(id); if (action === "export") return exportSimulation(id); if (action === "view") { try { await loadDetail(id); openModal("detailModal"); } catch (error) { setStatus(`Failed to load detail: ${error.message}`); } } }); } async function boot() { bindEvents(); try { setStatus("Loading teams and simulations..."); state.teams = await request("/api/teams"); renderTeamSelectors(); await refreshSimulations(); setStatus("Connected to SoccerCloud backend on port 9009."); } catch (error) { setStatus(`Startup failed: ${error.message}`); return; } if (state.pollHandle) { clearInterval(state.pollHandle); } state.pollHandle = setInterval(async () => { try { await refreshSimulations(); } catch (error) { setStatus(`Polling issue: ${error.message}`); } }, 500); } boot();