function formatBackupSize(bytes) { const value = Math.max(0, Number(bytes || 0)); if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; return `${(value / (1024 * 1024)).toFixed(1)} MB`; } export function renderSettings(deps) { const { dom, ammc, state, t, backend, settingsStorageNotice, settingsStorageNoticeIsError, getManagedWsUrl, getBackendUrl, getDefaultBackendUrl, escapeHtml, saveState, renderView, connectDecoder, playPassingBeep, playFinishSiren, playStartCue, speakText, getSpeechVoices, openOverlayWindow, buildOverlayUrl, normalizeObsOverlaySettings, DEFAULT_OBS_OVERLAY_SETTINGS, OBS_LAYOUTS, OBS_THEMES, pingBackend, syncStateToBackend, buildDataExportPayload, downloadJsonFile, importDataPayload, buildDirectoryExportPayload, downloadCsvFile, buildDriversCsvRows, buildCarsCsvRows, importDirectoryCsv, clearDataScope, buildExportMeta, normalizeStoredRacePreset, saveAmmcConfigToBackend, refreshAmmcStatus, startManagedAmmc, stopManagedAmmc, normalizeRaceAudioSettings, raceAudioEventDefinitions, triggerRaceAudio, serverBackups, serverBackupsLoaded, createManualServerBackup, createSafetyServerBackup, refreshServerBackups, } = deps; const runWithSafetyBackup = async (action) => { try { await createSafetyServerBackup(); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!window.confirm(t("settings.safety_backup_failed_confirm", { msg: message }))) return false; } await action(); return true; }; const ammcStatus = ammc.status || {}; const ammcOutput = Array.isArray(ammcStatus.lastOutput) ? ammcStatus.lastOutput : []; const suggestedWsUrl = getManagedWsUrl("main"); const suggestedSplitWsUrl = getManagedWsUrl("split"); const suggestedPitEntryWsUrl = getManagedWsUrl("pit_entry"); const suggestedPitExitWsUrl = getManagedWsUrl("pit_exit"); const running = Boolean(ammcStatus.running); const speechVoices = getSpeechVoices(); const raceAudio = normalizeRaceAudioSettings(state.settings.raceAudio); const speechVoiceOptions = speechVoices .map((voice) => ``) .join(""); const raceAudioVoiceOptions = speechVoices .map((voice) => ``) .join(""); const raceAudioEventRows = raceAudioEventDefinitions.map((definition) => ` ${escapeHtml(definition.key.replaceAll("_", " "))}${escapeHtml(definition.group)} · ${escapeHtml(definition.priority)} `).join(""); dom.view.innerHTML = `

${t("settings.decoder")}

${t("settings.decoder_setup_title")}

${t("settings.decoder_setup_hint")}

${t("settings.decoder_roles_hint")}

${t("timing.main_decoder")} ${t("settings.role_required")}

${t("settings.main_role_hint")}

${t("timing.split_decoder")}

${t("settings.split_role_hint")}

${t("timing.pit_decoders")}

${t("settings.pit_role_hint")}

${t("settings.connection_title")}

${t("settings.backend_url_hint")}

${t("settings.expected_json")}: {"msg":"PASSING", "transponder":232323, "rtc_time":"..."}

${t("dashboard.live_note")}

${t("audio.race_sounds")}

${t("audio.note")}

${raceAudioEventRows}
Event${t("audio.effect")}${t("audio.voice")}

${t("settings.audio")} · Legacy

${t("settings.audio_note")}

${t("settings.voice_note")}

${t("settings.branding")}

${t("settings.branding_note")}

${t("settings.logo")}

${t("settings.logo_note")}

${ state.settings.logoDataUrl ? `
logo
` : "" }

${t("overlay.obs_config")}

${t("overlay.obs_public_hint")}

${t("settings.managed_ammc")}

${t("settings.managed_channels_hint")}

${t("settings.managed_ammc_sub")}

${t("settings.bundled_hint")}

${t("settings.ammc_status")}: ${running ? t("settings.running") : t("settings.stopped")}

${t("settings.server_platform")}: ${escapeHtml(String(ammcStatus.serverPlatform || "-"))}

${t("settings.pid")}: ${escapeHtml(String(ammcStatus.pid || "-"))}

${t("settings.started_at")}: ${ammcStatus.startedAt ? new Date(ammcStatus.startedAt).toLocaleString() : "-"}

${t("settings.stopped_at")}: ${ammcStatus.stoppedAt ? new Date(ammcStatus.stoppedAt).toLocaleString() : "-"}

${t("settings.executable_path")}: ${escapeHtml(String(ammcStatus.resolvedExecutablePath || ammc.config.executablePath || "-"))}

${ammcStatus.executableExists ? t("settings.executable_found") : t("settings.executable_missing")}

${t("settings.decoder_host")}: ${escapeHtml(String(ammc.config.decoderHost || "-"))}

${t("settings.ws_port")}: ${escapeHtml(String(ammc.config.wsPort || 9000))}

${ammc.config.splitEnabled ? `

${t("timing.split_decoder")}: ${ammcStatus.split?.running ? t("settings.running") : t("settings.stopped")} • ${escapeHtml(String(ammc.config.splitDecoderHost || "-"))}:${escapeHtml(String(ammc.config.splitWsPort || 9001))}${ammcStatus.split?.lastError ? ` • ${escapeHtml(ammcStatus.split.lastError)}` : ""}

` : ""} ${ammc.config.pitEnabled ? `

${t("timing.pit_entry_decoder")}: ${ammcStatus.pitEntry?.running ? t("settings.running") : t("settings.stopped")} • ${escapeHtml(String(ammc.config.pitEntryDecoderHost || "-"))}:${escapeHtml(String(ammc.config.pitEntryWsPort || 9002))}${ammcStatus.pitEntry?.lastError ? ` • ${escapeHtml(ammcStatus.pitEntry.lastError)}` : ""}

${t("timing.pit_exit_decoder")}: ${ammcStatus.pitExit?.running ? t("settings.running") : t("settings.stopped")} • ${escapeHtml(String(ammc.config.pitExitDecoderHost || "-"))}:${escapeHtml(String(ammc.config.pitExitWsPort || 9003))}${ammcStatus.pitExit?.lastError ? ` • ${escapeHtml(ammcStatus.pitExit.lastError)}` : ""}

` : ""}

${t("settings.last_error")}: ${escapeHtml(ammc.lastError || ammcStatus.lastError || "-")}

${t("settings.backend_url")}: ${escapeHtml(getBackendUrl())}

WebSocket URL: ${escapeHtml(suggestedWsUrl)}

${escapeHtml(
          ammcOutput.length
            ? ammcOutput.map((entry) => `[${new Date(entry.ts).toLocaleTimeString()}] ${entry.role || "main"}/${entry.stream}: ${entry.line}`).join("\n")
            : "-"
        )}

${t("settings.storage")}

${t("settings.backend_url")}: ${escapeHtml(getBackendUrl())}

${t("settings.backend_status")}: ${backend.available ? t("settings.online") : t("settings.offline")}

${t("settings.last_sync")}: ${backend.lastSyncAt ? new Date(backend.lastSyncAt).toLocaleString() : "-"}

${escapeHtml(backend.lastError || "")}

${t("settings.server_backups")}

${t("settings.server_backups_hint")}

${!serverBackupsLoaded ? `

${t("settings.loading_backups")}

` : serverBackups.length ? serverBackups.slice(0, 10).map((backup) => `
${escapeHtml(new Date(backup.createdAt).toLocaleString())}${escapeHtml(backup.fileName)}
${escapeHtml(t(`settings.backup_reason_${backup.reason}`))} ${formatBackupSize(backup.sizeBytes)}
`).join("") : `

${t("settings.no_backups")}

`}

${t("settings.restore_backup_hint")}

${t("settings.storage_note_full")}

${t("settings.storage_note_directory")}

${t("settings.storage_note_csv")}

${t("settings.driver_csv_guide_title")}

${t("settings.driver_csv_required")}

${t("settings.driver_csv_columns")}

${t("settings.driver_csv_behavior")}

${t("settings.driver_csv_example")}
name;transponder;class;brand;club;country
Anna Andersson;123456;2WD;;JMK;SE
Erik Eriksson;654321;4WD;Associated;;
${settingsStorageNotice ? `

${escapeHtml(settingsStorageNotice)}

` : ""}

${t("settings.clear_data_title")}

${t("settings.clear_data_note")}

${t("settings.race_presets")}

${t("settings.race_presets_note")}

${(state.settings.racePresets || []).length} preset(s)

`; const applyRaceAudioFormValues = (form) => { const events = {}; raceAudioEventDefinitions.forEach((definition) => { events[definition.key] = { effect: form.get("raceAudioEffect_" + definition.key) === "on", voice: form.get("raceAudioVoice_" + definition.key) === "on", }; }); state.settings.raceAudio = normalizeRaceAudioSettings({ ...state.settings.raceAudio, enabled: form.get("raceAudioEnabled") === "on", muted: form.get("raceAudioMuted") === "on", language: String(form.get("raceAudioLanguage") || "sv"), voiceUri: String(form.get("raceAudioVoiceUri") || ""), voiceRate: Number(form.get("raceAudioVoiceRate")), voicePitch: Number(form.get("raceAudioVoicePitch")), masterVolume: Number(form.get("raceAudioMaster")), effectsVolume: Number(form.get("raceAudioEffects")), voiceVolume: Number(form.get("raceAudioVoice")), passingVolume: Number(form.get("raceAudioPassing")), startCountdownSeconds: Number(form.get("raceAudioCountdown")), startLights: form.get("raceAudioStartLights") === "on", randomStartDelay: form.get("raceAudioRandomDelay") === "on", minStartDelay: Number(form.get("raceAudioMinDelay")), maxStartDelay: Number(form.get("raceAudioMaxDelay")), perCompetitorPassingTone: form.get("raceAudioPerCompetitor") === "on", events, }); }; const persistRaceAudioFormValues = () => { const formNode = document.getElementById("settingsForm"); if (!(formNode instanceof HTMLFormElement)) return false; applyRaceAudioFormValues(new FormData(formNode)); state.settings.audioEnabled = state.settings.audioEnabled || state.settings.raceAudio.enabled; saveState(); return true; }; const applyAudioFormValues = () => { const formNode = document.getElementById("settingsForm"); if (!(formNode instanceof HTMLFormElement)) return; const form = new FormData(formNode); state.settings.audioEnabled = form.get("audioEnabled") === "on"; state.settings.passingSoundMode = String(form.get("passingSoundMode") || "beep"); state.settings.finishVoiceEnabled = form.get("finishVoiceEnabled") === "on"; state.settings.startSoundEnabled = form.get("startSoundEnabled") === "on"; state.settings.finishSoundEnabled = form.get("finishSoundEnabled") === "on"; state.settings.speechVoiceUri = String(form.get("speechVoiceUri") || ""); state.settings.speechRate = Math.max(0.65, Math.min(1.35, Number(form.get("speechRate") || 0.95) || 0.95)); state.settings.speechPitch = Math.max(0.7, Math.min(1.3, Number(form.get("speechPitch") || 1) || 1)); }; const splitDecoderToggle = document.getElementById("settingsSplitEnabled"); const pitDecoderToggle = document.getElementById("settingsPitEnabled"); const updateDecoderLayoutUi = () => { const splitEnabled = Boolean(splitDecoderToggle?.checked); const pitEnabled = Boolean(pitDecoderToggle?.checked); document.getElementById("settingsSplitCard")?.classList.toggle("is-disabled", !splitEnabled); document.getElementById("settingsPitCard")?.classList.toggle("is-disabled", !pitEnabled); document.querySelector('[data-decoder-state="split"]')?.replaceChildren( document.createTextNode(t(splitEnabled ? "settings.role_enabled" : "settings.role_disabled")) ); document.querySelector('[data-decoder-state="pit"]')?.replaceChildren( document.createTextNode(t(pitEnabled ? "settings.role_enabled" : "settings.role_disabled")) ); const activeLayout = splitEnabled && pitEnabled ? "full" : splitEnabled ? "split" : pitEnabled ? "pit" : "main"; document.querySelectorAll("[data-decoder-layout]").forEach((button) => { button.classList.toggle("is-active", button.dataset.decoderLayout === activeLayout); button.setAttribute("aria-pressed", String(button.dataset.decoderLayout === activeLayout)); }); }; splitDecoderToggle?.addEventListener("change", updateDecoderLayoutUi); pitDecoderToggle?.addEventListener("change", updateDecoderLayoutUi); document.querySelectorAll("[data-decoder-layout]").forEach((button) => { button.addEventListener("click", () => { const layout = button.dataset.decoderLayout; if (splitDecoderToggle) splitDecoderToggle.checked = layout === "split" || layout === "full"; if (pitDecoderToggle) pitDecoderToggle.checked = layout === "pit" || layout === "full"; updateDecoderLayoutUi(); }); }); updateDecoderLayoutUi(); document.getElementById("settingsForm")?.addEventListener("submit", (e) => { e.preventDefault(); const form = new FormData(e.currentTarget); state.settings.wsUrl = String(form.get("wsUrl") || "").trim(); state.settings.splitDecoderEnabled = form.get("splitDecoderEnabled") === "on"; state.settings.splitWsUrl = String(form.get("splitWsUrl") || "").trim(); state.settings.pitDecoderEnabled = form.get("pitDecoderEnabled") === "on"; state.settings.pitEntryWsUrl = String(form.get("pitEntryWsUrl") || "").trim(); state.settings.pitExitWsUrl = String(form.get("pitExitWsUrl") || "").trim(); state.settings.backendUrl = String(form.get("backendUrl") || "").trim(); state.settings.autoReconnect = form.get("autoReconnect") === "on"; applyAudioFormValues(); applyRaceAudioFormValues(form); state.settings.audioEnabled = state.settings.audioEnabled || state.settings.raceAudio.enabled; state.settings.speakerPassingCueEnabled = form.get("speakerPassingCueEnabled") === "on"; state.settings.speakerLeaderCueEnabled = form.get("speakerLeaderCueEnabled") === "on"; state.settings.speakerFinishCueEnabled = form.get("speakerFinishCueEnabled") === "on"; state.settings.speakerBestLapCueEnabled = form.get("speakerBestLapCueEnabled") === "on"; state.settings.speakerTop3CueEnabled = form.get("speakerTop3CueEnabled") === "on"; state.settings.speakerSessionStartCueEnabled = form.get("speakerSessionStartCueEnabled") === "on"; state.settings.clubName = String(form.get("clubName") || "").trim() || "JMK RB"; state.settings.clubTagline = String(form.get("clubTagline") || "").trim() || "Live Timing & Race Control"; state.settings.pdfFooter = String(form.get("pdfFooter") || "").trim() || "Generated by RC RaceCore"; state.settings.pdfTheme = ["classic", "minimal", "motorsport"].includes(String(form.get("pdfTheme") || "classic")) ? String(form.get("pdfTheme")) : "classic"; saveState(); renderView(); }); document.getElementById("settingsConnect")?.addEventListener("click", connectDecoder); document.querySelectorAll('[name^="raceAudio"]').forEach((control) => { control.addEventListener("change", persistRaceAudioFormValues); }); document.getElementById("settingsSaveRaceAudio")?.addEventListener("click", persistRaceAudioFormValues); const testRaceAudio = (eventKey) => { persistRaceAudioFormValues(); triggerRaceAudio(eventKey, { force: true, interrupt: true, variables: { driverName: state.drivers[0]?.name || "Driver", lapTime: "16.000", marshalPost: "1", transponder: "123", podium: "P1, P2, P3" } }); }; document.getElementById("settingsRaceAudioTest")?.addEventListener("click", () => testRaceAudio("audio_test")); document.querySelectorAll("[data-test-race-audio]").forEach((button) => button.addEventListener("click", () => testRaceAudio(button.dataset.testRaceAudio))); document.getElementById("settingsTestStart")?.addEventListener("click", () => { applyAudioFormValues(); if (state.settings.startSoundEnabled) playStartCue(); if (state.settings.finishVoiceEnabled) { setTimeout(() => speakText(t("timing.race_started_voice"), { interrupt: true }), state.settings.startSoundEnabled ? 650 : 0); } }); document.getElementById("settingsTestFinish")?.addEventListener("click", () => { applyAudioFormValues(); if (state.settings.finishSoundEnabled) playFinishSiren(); if (state.settings.finishVoiceEnabled) { setTimeout(() => speakText(t("timing.race_finished_voice"), { interrupt: true }), state.settings.finishSoundEnabled ? 1150 : 0); } }); document.getElementById("settingsTestDriverVoice")?.addEventListener("click", () => { applyAudioFormValues(); const driver = state.drivers[0]; speakText(driver?.speechName || driver?.name || t("settings.driver_voice_sample"), { interrupt: true }); }); document.getElementById("settingsLaunchObs")?.addEventListener("click", () => openOverlayWindow("obs", { public: true })); document.getElementById("settingsCopyObsUrl")?.addEventListener("click", async () => { const url = buildOverlayUrl("obs", { public: true }); if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(url).catch(() => {}); return; } window.prompt("Copy OBS URL", url); }); [ ["obsRows", "rows", (value) => Math.max(3, Math.min(12, Number(value) || DEFAULT_OBS_OVERLAY_SETTINGS.rows))], ["obsLayout", "layout", (value) => OBS_LAYOUTS.includes(String(value || "").toLowerCase()) ? String(value).toLowerCase() : DEFAULT_OBS_OVERLAY_SETTINGS.layout], ["obsTheme", "theme", (value) => OBS_THEMES.includes(String(value || "").toLowerCase()) ? String(value).toLowerCase() : DEFAULT_OBS_OVERLAY_SETTINGS.theme], ["obsPublicToken", "publicToken", (value) => String(value || "").trim()], ["obsShowClock", "showClock", (value, el) => Boolean(el?.checked)], ["obsShowFastest", "showFastest", (value, el) => Boolean(el?.checked)], ["obsShowGrid", "showGrid", (value, el) => Boolean(el?.checked)], ["obsShowLaps", "showLaps", (value, el) => Boolean(el?.checked)], ["obsShowResult", "showResult", (value, el) => Boolean(el?.checked)], ["obsShowBest", "showBest", (value, el) => Boolean(el?.checked)], ["obsShowGap", "showGap", (value, el) => Boolean(el?.checked)], ].forEach(([id, key, mapper]) => { const el = document.getElementById(id); if (!el) { return; } const handler = () => { const next = normalizeObsOverlaySettings({ ...state.settings.obsOverlay, [key]: mapper(el.value, el), }); state.settings.obsOverlay = next; saveState(); }; el.addEventListener("change", handler); if (el instanceof HTMLInputElement && (el.type === "number" || el.type === "text")) { el.addEventListener("input", handler); } }); document.getElementById("settingsTestBackend")?.addEventListener("click", async () => { await pingBackend(); renderView(); }); document.getElementById("settingsSyncNow")?.addEventListener("click", async () => { await syncStateToBackend(); renderView(); }); document.getElementById("createServerBackup")?.addEventListener("click", async (event) => { const button = event.currentTarget; if (button instanceof HTMLButtonElement) button.disabled = true; try { await createManualServerBackup(); await refreshServerBackups({ render: false }); alert(t("settings.backup_created")); } catch (error) { alert(t("settings.backup_failed", { msg: error instanceof Error ? error.message : String(error) })); } renderView(); }); document.getElementById("refreshServerBackups")?.addEventListener("click", () => refreshServerBackups()); document.getElementById("logoUpload")?.addEventListener("change", (event) => { const input = event.currentTarget; const file = input instanceof HTMLInputElement ? input.files?.[0] : null; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { state.settings.logoDataUrl = typeof reader.result === "string" ? reader.result : ""; saveState(); renderView(); }; reader.readAsDataURL(file); }); document.getElementById("clearLogo")?.addEventListener("click", () => { state.settings.logoDataUrl = ""; saveState(); renderView(); }); document.getElementById("exportData")?.addEventListener("click", () => { const payload = buildDataExportPayload(); downloadJsonFile("live_rc_full_export.json", payload); }); document.getElementById("importData")?.addEventListener("change", (event) => { const input = event.currentTarget; const file = input instanceof HTMLInputElement ? input.files?.[0] : null; if (!file) { return; } const reader = new FileReader(); reader.onload = async () => { try { const parsed = JSON.parse(String(reader.result || "{}")); const completed = await runWithSafetyBackup(() => importDataPayload(parsed, "full")); if (!completed && input instanceof HTMLInputElement) input.value = ""; } catch (error) { settingsStorageNotice = t("settings.import_failed", { msg: error instanceof Error ? error.message : String(error) }); settingsStorageNoticeIsError = true; renderView(); } }; reader.readAsText(file); }); document.getElementById("exportDirectoryData")?.addEventListener("click", () => { const payload = buildDirectoryExportPayload(); downloadJsonFile("live_rc_directory_export.json", payload); }); document.getElementById("exportDriversCsv")?.addEventListener("click", () => { downloadCsvFile("live_rc_drivers.csv", buildDriversCsvRows()); }); document.getElementById("downloadDriversCsvTemplate")?.addEventListener("click", () => { downloadCsvFile("racecore_driver_import_template.csv", [ ["name", "transponder", "class", "brand", "spoken_name", "club", "country"], ]); }); document.getElementById("exportCarsCsv")?.addEventListener("click", () => { downloadCsvFile("live_rc_cars.csv", buildCarsCsvRows()); }); const bindCsvImport = (selectButtonId, inputId, type) => { const input = document.getElementById(inputId); document.getElementById(selectButtonId)?.addEventListener("click", () => input?.click()); input?.addEventListener("change", (event) => { const fileInput = event.currentTarget; const file = fileInput instanceof HTMLInputElement ? fileInput.files?.[0] : null; if (!file) return; const reader = new FileReader(); reader.onload = () => importDirectoryCsv(String(reader.result || ""), type); reader.onerror = () => importDirectoryCsv("", type); reader.readAsText(file); }); }; bindCsvImport("selectDriversCsv", "importDriversCsv", "drivers"); bindCsvImport("selectCarsCsv", "importCarsCsv", "cars"); document.querySelectorAll("[data-clear-scope]").forEach((button) => { button.addEventListener("click", async () => { const scope = String(button.dataset.clearScope || ""); if (!scope || !window.confirm(t(`settings.clear_${scope}_confirm`))) return; button.disabled = true; const completed = await runWithSafetyBackup(() => clearDataScope(scope)); if (!completed) button.disabled = false; }); }); document.getElementById("importDirectoryData")?.addEventListener("change", (event) => { const input = event.currentTarget; const file = input instanceof HTMLInputElement ? input.files?.[0] : null; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { try { const parsed = JSON.parse(String(reader.result || "{}")); importDataPayload(parsed, "directory"); } catch (error) { settingsStorageNotice = t("settings.import_failed", { msg: error instanceof Error ? error.message : String(error) }); settingsStorageNoticeIsError = true; renderView(); } }; reader.readAsText(file); }); document.getElementById("exportRacePresets")?.addEventListener("click", () => { const payload = { ...buildExportMeta("race_presets"), racePresets: (state.settings.racePresets || []).map((preset) => normalizeStoredRacePreset(preset)), }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "live_rc_race_presets.json"; link.click(); URL.revokeObjectURL(url); }); document.getElementById("importRacePresets")?.addEventListener("change", (event) => { const input = event.currentTarget; const file = input instanceof HTMLInputElement ? input.files?.[0] : null; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { try { const parsed = JSON.parse(String(reader.result || "{}")); const incoming = Array.isArray(parsed?.racePresets) ? parsed.racePresets.map((preset) => normalizeStoredRacePreset(preset)).filter((preset) => preset.name) : []; const existingById = new Map((state.settings.racePresets || []).map((preset) => [preset.id, normalizeStoredRacePreset(preset)])); incoming.forEach((preset) => { existingById.set(preset.id, preset); }); state.settings.racePresets = [...existingById.values()]; saveState(); renderView(); } catch { // ignore invalid preset file } }; reader.readAsText(file); }); document.getElementById("ammcForm")?.addEventListener("submit", async (e) => { e.preventDefault(); const form = new FormData(e.currentTarget); const config = { managedEnabled: form.get("managedEnabled") === "on", autoStart: form.get("autoStart") === "on", decoderHost: String(form.get("decoderHost") || "").trim(), wsPort: Number(form.get("wsPort") || 9000), splitEnabled: form.get("splitEnabled") === "on", splitDecoderHost: String(form.get("splitDecoderHost") || "").trim(), splitWsPort: Number(form.get("splitWsPort") || 9001), pitEnabled: form.get("pitEnabled") === "on", pitEntryDecoderHost: String(form.get("pitEntryDecoderHost") || "").trim(), pitEntryWsPort: Number(form.get("pitEntryWsPort") || 9002), pitExitDecoderHost: String(form.get("pitExitDecoderHost") || "").trim(), pitExitWsPort: Number(form.get("pitExitWsPort") || 9003), executablePath: String(form.get("executablePath") || "").trim(), workingDirectory: String(form.get("workingDirectory") || "").trim(), extraArgs: String(form.get("extraArgs") || "").trim(), }; await saveAmmcConfigToBackend(config); renderView(); }); document.getElementById("ammcRefresh")?.addEventListener("click", async () => { await refreshAmmcStatus(); renderView(); }); document.getElementById("ammcStart")?.addEventListener("click", async () => { const formElement = document.getElementById("ammcForm"); if (formElement instanceof HTMLFormElement) { const form = new FormData(formElement); await saveAmmcConfigToBackend({ managedEnabled: form.get("managedEnabled") === "on", autoStart: form.get("autoStart") === "on", decoderHost: String(form.get("decoderHost") || "").trim(), wsPort: Number(form.get("wsPort") || 9000), splitEnabled: form.get("splitEnabled") === "on", splitDecoderHost: String(form.get("splitDecoderHost") || "").trim(), splitWsPort: Number(form.get("splitWsPort") || 9001), pitEnabled: form.get("pitEnabled") === "on", pitEntryDecoderHost: String(form.get("pitEntryDecoderHost") || "").trim(), pitEntryWsPort: Number(form.get("pitEntryWsPort") || 9002), pitExitDecoderHost: String(form.get("pitExitDecoderHost") || "").trim(), pitExitWsPort: Number(form.get("pitExitWsPort") || 9003), executablePath: String(form.get("executablePath") || "").trim(), workingDirectory: String(form.get("workingDirectory") || "").trim(), extraArgs: String(form.get("extraArgs") || "").trim(), }); } await startManagedAmmc(); renderView(); }); document.getElementById("ammcStop")?.addEventListener("click", async () => { await stopManagedAmmc(); renderView(); }); document.getElementById("ammcUseServerWs")?.addEventListener("click", () => { state.settings.wsUrl = suggestedWsUrl; state.settings.splitDecoderEnabled = Boolean(ammc.config.splitEnabled); state.settings.splitWsUrl = suggestedSplitWsUrl; state.settings.pitDecoderEnabled = Boolean(ammc.config.pitEnabled); state.settings.pitEntryWsUrl = suggestedPitEntryWsUrl; state.settings.pitExitWsUrl = suggestedPitExitWsUrl; saveState(); renderView(); }); }