import { html, reactive } from "/assets/vendor/arrow-core.js" const state = reactive({ loading: true, error: "", busy: false, data: null, selectedSlot: "", }) let initialized = false function getApiUrl(path) { const normalizedPath = String(path || "").replace(/^\/+/, "") const currentUrl = new URL(window.location.href) currentUrl.hash = "" currentUrl.search = "" currentUrl.pathname = currentUrl.pathname.replace(/\/+$/, "") || "/" return new URL(normalizedPath, currentUrl).toString() } async function parseApiPayload(response, fallbackMessage) { const contentType = String(response.headers.get("content-type") || "").toLowerCase() if (contentType.includes("application/json")) { const payload = await response.json() return { payload, message: payload?.error || payload?.message || fallbackMessage, } } const text = (await response.text()).trim() const cleaned = text.replace(/\s+/g, " ").slice(0, 200) return { payload: null, message: cleaned || fallbackMessage, } } function slotId(slot) { return String(slot?.id || "").trim() } function isUnusedSlot(slot) { const name = String(slot?.name || "").trim().toLowerCase() return name === "unused" || name.startsWith("unused ") } function getSelectableSlots() { const selectable = Array.isArray(state.data?.selectableSlots) ? state.data.selectableSlots : [] if (selectable.length) return selectable const slots = Array.isArray(state.data?.slots) ? state.data.slots : [] return slots.filter((slot) => !isUnusedSlot(slot)) } function getVisibleSlotLines() { return getSelectableSlots().map((slot) => `${slot.id}. ${slot.name}`) } function getSelectedSlot() { const slots = Array.isArray(state.data?.slots) ? state.data.slots : [] const selectedId = String(state.selectedSlot || "").trim() if (!selectedId) return null return slots.find((slot) => slotId(slot) === selectedId) || null } function getActiveSlot() { const slots = Array.isArray(state.data?.slots) ? state.data.slots : [] const activeId = String(state.data?.activeSlot || "").trim() if (!activeId) return null return slots.find((slot) => slotId(slot) === activeId) || null } function getVariantLabels(slot) { if (!slot || typeof slot !== "object") { return { A: "A" } } const labels = {} const rawVariantLabels = slot.variantLabels if (rawVariantLabels && typeof rawVariantLabels === "object") { Object.entries(rawVariantLabels).forEach(([rawMode, rawLabel]) => { const mode = String(rawMode || "").trim().toUpperCase() const label = String(rawLabel || "").trim() if (mode.length === 1 && /^[A-Z]$/.test(mode) && label) { labels[mode] = label } }) } const aLabel = String(slot.aLabel || "").trim() const bLabel = String(slot.bLabel || "").trim() if (aLabel) labels.A = labels.A || aLabel if (bLabel) labels.B = labels.B || bLabel if (!labels.A) labels.A = "A" return Object.keys(labels).sort().reduce((acc, key) => { acc[key] = labels[key] return acc }, {}) } function getVariantModes(slot) { return Object.keys(getVariantLabels(slot)) } function getDefaultMode(slot) { const modes = getVariantModes(slot) if (modes.includes("A")) return "A" return modes[0] || "A" } function toModeLabel(slot, mode) { const labels = getVariantLabels(slot) return labels[String(mode || "").trim().toUpperCase()] || String(mode || "").trim().toUpperCase() || "A" } function isModeActive(mode) { const normalizedMode = String(mode || "").trim().toUpperCase() return String(state.data?.activeSlot || "").trim() === String(state.selectedSlot || "").trim() && String(state.data?.activeVariant || "").trim().toUpperCase() === normalizedMode } function getSelectedMode() { const selectedSlot = getSelectedSlot() if (!selectedSlot) return "Not active" if (String(state.data?.activeSlot || "").trim() === String(state.selectedSlot || "").trim()) { const activeMode = String(state.data?.activeVariant || "").trim().toUpperCase() || getDefaultMode(selectedSlot) return String(state.data?.activeVariantLabel || "").trim() || toModeLabel(selectedSlot, activeMode) } return "Not active" } function modeButtonClass(mode) { const normalizedMode = String(mode || "").trim().toUpperCase() return [ "testingGroundModeButton", `testingGroundModeButton${normalizedMode}`, isModeActive(normalizedMode) ? "active" : "", ].filter(Boolean).join(" ") } async function fetchTestingGrounds() { try { const response = await fetch(getApiUrl("api/testing_grounds")) const { payload, message } = await parseApiPayload(response, "Failed to load testing grounds") if (!response.ok) { throw new Error(message || response.statusText || "Failed to load testing grounds") } if (!payload || typeof payload !== "object") { throw new Error(message || "Failed to load testing grounds") } state.data = payload state.error = "" const selectable = Array.isArray(payload.selectableSlots) ? payload.selectableSlots : [] const hasCurrentSelection = selectable.some((slot) => slotId(slot) === String(state.selectedSlot || "").trim()) if (!hasCurrentSelection) { const activeSlot = String(payload.activeSlot || "").trim() const activeSelectable = selectable.some((slot) => slotId(slot) === activeSlot) state.selectedSlot = activeSelectable ? activeSlot : slotId(selectable[0] || {}) } } catch (error) { const message = error?.message || "Failed to load testing grounds" state.error = message } } async function applySelection(slotValue, mode, showToast = true) { const normalizedSlot = String(slotValue || "").trim() const normalizedMode = String(mode || "").trim().toUpperCase() if (!normalizedSlot || !normalizedMode) return false state.busy = true try { const response = await fetch(getApiUrl("api/testing_grounds/select"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slotId: normalizedSlot, variant: normalizedMode }), }) const { payload, message } = await parseApiPayload(response, "Failed to update testing ground mode") if (!response.ok) { throw new Error(message || response.statusText || "Failed to update testing ground mode") } if (!payload || typeof payload !== "object") { throw new Error(message || "Failed to update testing ground mode") } state.data = payload state.error = "" state.selectedSlot = normalizedSlot if (showToast) { const selectedSlot = Array.isArray(payload.slots) ? payload.slots.find((slot) => slotId(slot) === normalizedSlot) : null const selectedLabel = String(payload.activeVariantLabel || "").trim() || toModeLabel(selectedSlot, normalizedMode) showSnackbar(payload.message || `Testing Ground ${normalizedSlot} set to ${selectedLabel}.`) } return true } catch (error) { const message = error?.message || "Failed to update testing ground mode" state.error = message if (showToast) { showSnackbar(message, "error") } return false } finally { state.busy = false } } async function selectMode(mode) { if (state.busy) return const slot = getSelectedSlot() const slotValue = slotId(slot) if (!slotValue) { showSnackbar("Select a testing ground first.", "error") return } await applySelection(slotValue, mode, true) } async function selectSlot(slotValue) { const normalizedSlot = String(slotValue || "").trim() if (!normalizedSlot || state.busy) return state.selectedSlot = normalizedSlot } function initialize() { if (initialized) return initialized = true fetchTestingGrounds().finally(() => { state.loading = false }) } export function TestingGround() { initialize() return html`

Testing Ground

${() => state.loading ? html`
Loading testing ground state...
` : ""} ${() => !state.loading ? html`

A/B Tuning Sandbox. If you don't know what this is, you probably shouldn't be here ;)

Current Test Slots
    ${getVisibleSlotLines().map((line) => html`
  1. ${line}
  2. `)}

Selected Slot: ${getSelectedSlot()?.name || "Unknown"}

Selected Mode: ${getSelectedMode()}

Onroad: ${state.data?.isOnroad ? "Yes" : "No"}

Selectable Slots: ${getSelectableSlots().length}

Only one Testing Ground can be active at a time. Switching slots only changes what you're viewing, and the active test stays enabled until you explicitly choose another mode.

${() => getSelectedSlot() ? html`

${getSelectedSlot().name}

${() => getSelectedSlot().description ? html`

${getSelectedSlot().description}

` : ""}
` : ""} ${() => getSelectedSlot() ? html`
${() => getVariantModes(getSelectedSlot()).map((mode) => html` `)}
` : ""} ${() => state.error ? html`

Error: ${state.error}

` : ""} ${() => getActiveSlot() ? html`

Currently active: ${getActiveSlot().id}. ${getActiveSlot().name} in mode ${state.data?.activeVariantLabel || toModeLabel(getActiveSlot(), state.data?.activeVariant || "A")}.

` : ""}
` : ""}
` }