import { decoderDiagnosticsMarkup, mountDecoderDiagnostics } from "./decoder_diagnostics_view.js"; import { mountAuthSettings } from "./auth_view.js"; import { mountTtsSettings } from "./tts_settings_view.js"; let activeSettingsTab = "connection"; let pendingSettingsFocusId = ""; // Lets other views open Settings on the right tab (and scroll to a section) - e.g. the // sidebar "update available" link lands on System & backup -> Updates. export function openSettingsTab(tabId, focusId = "") { activeSettingsTab = tabId; pendingSettingsFocusId = focusId; } 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, trackLayouts, nextTrackLayoutLabel, addTrackLayout, deleteTrackLayout, updateTrackLayoutLapWindow = () => ({ ok: false }), updateStatus, updateStatusLoaded, updateCheckPending, updateNotice, updateNoticeIsError, refreshUpdateStatus, runUpdateNow, setUpdateNotice, } = 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 pageOrigin = /^https?:$/.test(window.location.protocol) ? window.location.origin : ""; const configuredBackendUrl = String(state.settings.backendUrl || getDefaultBackendUrl()).replace(/\/+$/, ""); const backendUrlMismatch = Boolean(pageOrigin && configuredBackendUrl && pageOrigin !== configuredBackendUrl); 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 raceAudioVoiceOptions = speechVoices .map((voice) => ``) .join(""); const raceAudioEventRows = raceAudioEventDefinitions.map((definition) => `
${t("settings.expected_json")}: {"msg":"PASSING", "transponder":232323, "rtc_time":"..."}
${t("dashboard.live_note")}
${t("audio.note")}
| Event | ${t("audio.effect")} | ${t("audio.voice")} |
|---|
${t("settings.branding_note")}
${t("settings.logo_note")}
${t("settings.track_layouts_hint")}
${t("layouts.lap_window_hint")}
${t("settings.track_layout_none")}
`}${t("overlay.obs_public_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.update_current_version")}: v${escapeHtml(String(updateStatus?.currentVersion || ""))}
${!updateStatusLoaded ? `${t("settings.update_checking")}
` : updateStatus?.error ? `${escapeHtml(t("settings.update_error", { msg: updateStatus.error }))}
` : updateStatus?.available ? `${escapeHtml(t("settings.update_available", { version: updateStatus.remoteVersion || "?" }))}
` : `${t("settings.update_up_to_date")}
`}${escapeHtml(updateNotice)}
` : ""}${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_hint")}
${t("settings.loading_backups")}
` : serverBackups.length ? serverBackups.slice(0, 10).map((backup) => `${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_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;;
${escapeHtml(settingsStorageNotice)}
` : ""}${t("settings.clear_data_note")}
${t("settings.race_presets_note")}
${(state.settings.racePresets || []).length} preset(s)