- ${renderRaceFormatContextCard(isEndurancePreset ? "events.context_endurance_title" : "events.context_standard_title", isEndurancePreset ? "events.context_endurance_hint" : "events.context_standard_hint")}
- ${showBasicFinalFields ? renderRaceFormatField(
+ ${renderRaceFormatContextCardView(isEndurancePreset ? "events.context_endurance_title" : "events.context_standard_title", isEndurancePreset ? "events.context_endurance_hint" : "events.context_standard_hint")}
+ ${showBasicFinalFields ? renderRaceFormatFieldView(
"events.cars_per_final",
"events.cars_per_final_hint",
`
`
) : ""}
- ${showBasicFinalFields ? renderRaceFormatField(
+ ${showBasicFinalFields ? renderRaceFormatFieldView(
"events.final_legs",
"events.final_legs_hint",
`
`
) : ""}
${raceFormatAdvanced
- ? renderRaceFormatField(
+ ? renderRaceFormatFieldView(
"events.counted_final_legs",
"events.counted_final_legs_hint",
`
`
)
: ""}
- ${showBasicFinalFields ? renderRaceFormatField(
+ ${showBasicFinalFields ? renderRaceFormatFieldView(
"events.final_duration",
"events.final_duration_hint",
`
`
) : ""}
- ${showBasicFinalFields ? renderRaceFormatField(
+ ${showBasicFinalFields ? renderRaceFormatFieldView(
"events.final_start_mode",
"events.final_start_mode_hint",
`
`
) : ""}
- ${showBasicFinalFields ? renderRaceFormatField(
+ ${showBasicFinalFields ? renderRaceFormatFieldView(
"events.bump_count",
"events.bump_count_hint",
`
`
) : ""}
- ${showBasicFinalFields ? renderRaceFormatField(
+ ${showBasicFinalFields ? renderRaceFormatFieldView(
"events.source_for_finals",
"events.finals_source_hint",
`
`
) : ""}
${raceFormatAdvanced
- ? renderRaceFormatField(
+ ? renderRaceFormatFieldView(
"events.reserve_bump_slots",
"events.reserve_bump_slots_hint",
`
`,
@@ -4885,18 +4935,18 @@ function renderEventManager(eventId) {
- ${renderRaceFormatContextCard("events.context_rules_title", "events.context_rules_hint")}
- ${renderRaceFormatField(
+ ${renderRaceFormatContextCardView("events.context_rules_title", "events.context_rules_hint")}
+ ${renderRaceFormatFieldView(
"events.follow_up_sec",
"events.follow_up_sec_hint",
`
`
)}
- ${renderRaceFormatField(
+ ${renderRaceFormatFieldView(
"events.min_lap_time",
"events.min_lap_time_hint",
`
`
)}
- ${renderRaceFormatField(
+ ${renderRaceFormatFieldView(
"events.max_lap_time",
"events.max_lap_time_hint",
`
`
@@ -4969,21 +5019,21 @@ function renderEventManager(eventId) {
- ${renderRaceStandingsTable(buildPracticeStandings(event), t("events.no_practice_results"))}
+ ${renderRaceStandingsTableView(buildPracticeStandings(event), t("events.no_practice_results"))}
- ${renderRaceStandingsTable(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
+ ${renderRaceStandingsTableView(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
- ${renderRaceStandingsTable(buildFinalStandings(event), t("events.no_final_results"))}
+ ${renderRaceStandingsTableView(buildFinalStandings(event), t("events.no_final_results"))}
@@ -6711,15 +6761,15 @@ function renderOverlay() {
${t("events.practice_standings")}
- ${renderRaceStandingsTable(practiceRows, t("events.no_practice_results"))}
+ ${renderRaceStandingsTableView(practiceRows, t("events.no_practice_results"))}
${t("events.qualifying_standings")}
- ${renderRaceStandingsTable(qualifyingRows, t("events.no_qualifying_results"))}
+ ${renderRaceStandingsTableView(qualifyingRows, t("events.no_qualifying_results"))}
${t("events.final_standings")}
- ${renderRaceStandingsTable(finalRows, t("events.no_final_results"))}
+ ${renderRaceStandingsTableView(finalRows, t("events.no_final_results"))}
`
@@ -7010,498 +7060,6 @@ function renderRecentPassings(session) {
);
}
-function renderSettings() {
- const ammcStatus = ammc.status || {};
- const ammcOutput = Array.isArray(ammcStatus.lastOutput) ? ammcStatus.lastOutput : [];
- const suggestedWsUrl = getManagedWsUrl();
- const running = Boolean(ammcStatus.running);
-
- dom.view.innerHTML = `
-
-
-
-
-
${t("settings.expected_json")}: {"msg":"PASSING", "transponder":232323, "rtc_time":"..."}
-
${t("dashboard.live_note")}
-
-
-
-
-
-
-
-
-
-
-
${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))}
-
${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.stream}: ${entry.line}`).join("\n")
- : "-"
- )}
-
-
-
-
-
-
-
${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.storage_note_full")}
-
-
-
-
-
-
-
${t("settings.storage_note_directory")}
-
-
-
-
-
-
-
${t("settings.storage_note_csv")}
-
-
-
-
-
- ${settingsStorageNotice ? `
${escapeHtml(settingsStorageNotice)}
` : ""}
-
-
-
-
-
-
-
${t("settings.race_presets_note")}
-
${(state.settings.racePresets || []).length} preset(s)
-
-
-
-
-
-
- `;
-
- document.getElementById("settingsForm")?.addEventListener("submit", (e) => {
- e.preventDefault();
- const form = new FormData(e.currentTarget);
- state.settings.wsUrl = String(form.get("wsUrl") || "").trim();
- state.settings.backendUrl = String(form.get("backendUrl") || "").trim();
- state.settings.autoReconnect = form.get("autoReconnect") === "on";
- state.settings.audioEnabled = form.get("audioEnabled") === "on";
- state.settings.passingSoundMode = String(form.get("passingSoundMode") || "beep");
- state.settings.finishVoiceEnabled = form.get("finishVoiceEnabled") === "on";
- 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 RaceController";
- state.settings.clubTagline = String(form.get("clubTagline") || "").trim() || "RC Timing System";
- state.settings.pdfFooter = String(form.get("pdfFooter") || "").trim() || "Generated by JMK RB RaceController";
- 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.getElementById("settingsTestAudio")?.addEventListener("click", () => {
- playPassingBeep();
- setTimeout(() => {
- if (state.settings.finishVoiceEnabled) {
- playFinishSiren();
- }
- }, 180);
- });
- 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("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 = () => {
- try {
- const parsed = JSON.parse(String(reader.result || "{}"));
- importDataPayload(parsed, "full");
- } 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("exportCarsCsv")?.addEventListener("click", () => {
- downloadCsvFile("live_rc_cars.csv", buildCarsCsvRows());
- });
-
- 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),
- 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),
- 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 = getManagedWsUrl();
- saveState();
- renderView();
- });
-}
function getSessionsForEvent(eventId) {
return state.sessions.filter((s) => s.eventId === eventId);
@@ -8437,90 +7995,6 @@ function renderTable(headers, rowHtml) {
`;
}
-function renderRaceFormatField(labelKey, hintKey, controlHtml, options = {}) {
- const extraClass = options.checkbox ? " field-card-checkbox" : "";
- return `
-
- `;
-}
-
-function renderRaceFormatContextCard(titleKey, hintKey) {
- return `
-
- ${t(titleKey)}
- ${t(hintKey)}
-
- `;
-}
-
-function getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset) {
- const cfg = event.raceConfig || {};
- const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
- const practiceCount = sessions.filter((session) => ["practice", "free_practice", "open_practice"].includes(session.type)).length;
- const qualCount = sessions.filter((session) => session.type === "qualification").length;
- const finalCount = sessions.filter((session) => session.type === "final").length;
- const teamCount = sessions.filter((session) => session.type === "team_race").length;
- const scoringLabel = cfg.qualifyingScoring === "best" ? t("events.qualifying_scoring_best") : t("events.qualifying_scoring_points");
- const lapWindow = `${((cfg.minLapMs || 0) / 1000).toFixed(1)}s / ${((cfg.maxLapMs || 0) / 1000).toFixed(1)}s`;
- const isEndurancePreset = selectedPreset?.id === "endurance";
- if (isEndurancePreset) {
- return [
- { label: t("events.race_summary_preset"), value: selectedPreset?.label || t("events.preset_custom") },
- { label: t("events.race_summary_focus"), value: `${t("events.team_race")} · ${getStartModeLabel("mass")}` },
- { label: t("events.race_summary_participants"), value: String(participantCount || 0) },
- { label: t("events.race_summary_created_sessions"), value: `P ${practiceCount} · T ${teamCount}` },
- { label: t("events.race_summary_generation"), value: `${teamCount ? t("events.team_race") : t("events.wizard_use_team_race")} · ${cfg.finalDurationMin || 0} min` },
- { label: t("events.race_summary_validation"), value: lapWindow },
- { label: t("events.race_summary_follow_up"), value: `${cfg.followUpSec || 0}s` },
- ];
- }
- return [
- { label: t("events.race_summary_preset"), value: selectedPreset?.label || t("events.preset_custom") },
- { label: t("events.race_summary_participants"), value: String(participantCount || 0) },
- { label: t("events.race_summary_created_sessions"), value: `P ${practiceCount} · Q ${qualCount} · F ${finalCount} · T ${teamCount}` },
- { label: t("events.race_summary_qualifying"), value: `${cfg.qualifyingRounds || 0} x ${cfg.qualDurationMin || 0} min · ${cfg.carsPerHeat || 0}/heat · ${scoringLabel}` },
- { label: t("events.race_summary_finals"), value: `${cfg.carsPerFinal || 0}/main · ${cfg.finalLegs || 0} leg · ${getStartModeLabel(cfg.finalStartMode || "position")}` },
- { label: t("events.race_summary_generation"), value: `${cfg.finalsSource === "practice" ? t("events.finals_from_practice") : t("events.finals_from_qualifying")} · bump ${cfg.bumpCount || 0}` },
- { label: t("events.race_summary_validation"), value: lapWindow },
- { label: t("events.race_summary_follow_up"), value: `${cfg.followUpSec || 0}s` },
- ];
-}
-
-function getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selectedPreset) {
- const cfg = event.raceConfig || {};
- const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
- const warnings = [];
- const isEndurancePreset = selectedPreset?.id === "endurance";
- const hasQualifying = sessions.some((session) => session.type === "qualification");
- const hasFinals = sessions.some((session) => session.type === "final");
- const hasAnySession = sessions.length > 0;
- if (participantCount <= 0) {
- warnings.push({ message: t("events.summary_warning_no_participants"), target: "manage-setup-participants" });
- }
- if (!hasAnySession) {
- warnings.push({ message: t("events.summary_warning_no_sessions"), target: "manage-session-plan" });
- }
- if (!isEndurancePreset && !hasQualifying) {
- warnings.push({ message: t("events.summary_warning_no_qualifying"), target: "manage-generation" });
- }
- if (!isEndurancePreset && !hasFinals) {
- warnings.push({ message: t("events.summary_warning_no_finals"), target: "manage-generation" });
- }
- if (isEndurancePreset && raceTeams.length <= 0) {
- warnings.push({ message: t("events.summary_warning_no_teams"), target: "manage-setup-teams" });
- }
- const minLapMs = Math.max(0, Number(cfg.minLapMs || 0) || 0);
- const maxLapMs = Math.max(0, Number(cfg.maxLapMs || 0) || 0);
- if (maxLapMs > 0 && maxLapMs <= minLapMs) {
- warnings.push({ message: t("events.summary_warning_invalid_lap_window"), target: "manage-format" });
- }
- return warnings;
-}
-
function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selectedPreset) {
const cfg = event.raceConfig || {};
const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
@@ -8574,10 +8048,6 @@ function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selected
};
}
-function renderManageStatusBadge(status) {
- return `
${t(`events.status_${status}`)}`;
-}
-
function getDriversForClass(classId) {
return state.drivers.filter((driver) => !classId || driver.classId === classId);
}
@@ -8667,125 +8137,6 @@ function getRaceWizardSessionPlan(draft) {
return plan;
}
-function renderRaceWizardSteps() {
- const steps = [t("events.wizard_step_1"), t("events.wizard_step_2"), t("events.wizard_step_3"), t("events.wizard_step_4")];
- return steps
- .map(
- (label, index) => `
-
- ${index + 1}
- ${escapeHtml(label)}
-
- `
- )
- .join("");
-}
-
-function renderRaceWizardContent(draft, classOptions, wizardDrivers, preset) {
- if (raceWizardStep === 1) {
- return `
-
- `;
- }
- if (raceWizardStep === 2) {
- return `
-
-
-
-
- ${wizardDrivers.length ? `
-
- ${wizardDrivers
- .map(
- (driver) => `
-
- `
- )
- .join("")}
-
- ` : `
${t("events.wizard_no_class_drivers")}
`}
- `;
- }
- if (raceWizardStep === 3) {
- const isEndurance = preset.id === "endurance";
- return `
-
-
${t("events.wizard_finals_note")}
- `;
- }
- const selectedClassName = getClassName(draft.classId);
- const selectedDrivers = draft.driverIds.length ? draft.driverIds.length : wizardDrivers.length;
- return `
-
-
- ${t("events.wizard_summary_title")}
- ${escapeHtml(draft.name || "-")}
-
-
- ${t("table.class")}
- ${escapeHtml(selectedClassName || "-")}
-
-
- ${t("events.race_summary_preset")}
- ${escapeHtml(preset?.label || t("events.preset_custom"))}
-
-
- ${t("events.race_summary_participants")}
- ${selectedDrivers}
-
-
- ${t("events.wizard_summary_sessions")}
- ${escapeHtml(getRaceWizardSessionPlan(draft).join(" • ") || "-")}
-
-
- `;
-}
-
function formatLap(ms) {
if (!ms && ms !== 0) {
return "-";
@@ -9194,28 +8545,6 @@ function buildQualifyingStandings(event) {
}));
}
-function renderRaceStandingsTable(rows, emptyLabel) {
- if (!rows.length) {
- return `
${emptyLabel}
`;
- }
-
- return renderTable(
- [t("table.pos"), t("table.driver"), t("table.score")],
- rows.map(
- (row) => `
-
- | ${row.rank} |
- ${escapeHtml(row.driverName || t("common.unknown_driver"))} |
-
- ${escapeHtml(row.score || "-")}
- ${row.scoreNote ? `${escapeHtml(row.scoreNote)} ` : ""}
- |
-
- `
- )
- );
-}
-
function formatTeamActiveMemberLabel(rowOrPassing) {
if (!rowOrPassing) {
return "-";
@@ -9988,15 +9317,15 @@ function buildRaceResultsHtml(event) {
${t("events.practice_standings")}
- ${renderRaceStandingsTable(buildPracticeStandings(event), t("events.no_practice_results"))}
+ ${renderRaceStandingsTableView(buildPracticeStandings(event), t("events.no_practice_results"))}
${t("events.qualifying_standings")}
- ${renderRaceStandingsTable(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
+ ${renderRaceStandingsTableView(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
${t("events.final_standings")}
- ${renderRaceStandingsTable(buildFinalStandings(event), t("events.no_final_results"))}
+ ${renderRaceStandingsTableView(buildFinalStandings(event), t("events.no_final_results"))}
${t("events.team_standings")}
diff --git a/src/race_setup_ui.js b/src/race_setup_ui.js
new file mode 100644
index 0000000..98c7412
--- /dev/null
+++ b/src/race_setup_ui.js
@@ -0,0 +1,216 @@
+export function renderRaceFormatField(labelKey, hintKey, controlHtml, options = {}, { t }) {
+ const extraClass = options.checkbox ? " field-card-checkbox" : "";
+ return `
+
+ `;
+}
+
+export function renderRaceFormatContextCard(titleKey, hintKey, { t }) {
+ return `
+
+ ${t(titleKey)}
+ ${t(hintKey)}
+
+ `;
+}
+
+export function getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset, { t, getStartModeLabel }) {
+ const cfg = event.raceConfig || {};
+ const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
+ const practiceCount = sessions.filter((session) => ["practice", "free_practice", "open_practice"].includes(session.type)).length;
+ const qualCount = sessions.filter((session) => session.type === "qualification").length;
+ const finalCount = sessions.filter((session) => session.type === "final").length;
+ const teamCount = sessions.filter((session) => session.type === "team_race").length;
+ const scoringLabel = cfg.qualifyingScoring === "best" ? t("events.qualifying_scoring_best") : t("events.qualifying_scoring_points");
+ const lapWindow = `${((cfg.minLapMs || 0) / 1000).toFixed(1)}s / ${((cfg.maxLapMs || 0) / 1000).toFixed(1)}s`;
+ const isEndurancePreset = selectedPreset?.id === "endurance";
+ if (isEndurancePreset) {
+ return [
+ { label: t("events.race_summary_preset"), value: selectedPreset?.label || t("events.preset_custom") },
+ { label: t("events.race_summary_focus"), value: `${t("events.team_race")} · ${getStartModeLabel("mass")}` },
+ { label: t("events.race_summary_participants"), value: String(participantCount || 0) },
+ { label: t("events.race_summary_created_sessions"), value: `P ${practiceCount} · T ${teamCount}` },
+ { label: t("events.race_summary_generation"), value: `${teamCount ? t("events.team_race") : t("events.wizard_use_team_race")} · ${cfg.finalDurationMin || 0} min` },
+ { label: t("events.race_summary_validation"), value: lapWindow },
+ { label: t("events.race_summary_follow_up"), value: `${cfg.followUpSec || 0}s` },
+ ];
+ }
+ return [
+ { label: t("events.race_summary_preset"), value: selectedPreset?.label || t("events.preset_custom") },
+ { label: t("events.race_summary_participants"), value: String(participantCount || 0) },
+ { label: t("events.race_summary_created_sessions"), value: `P ${practiceCount} · Q ${qualCount} · F ${finalCount} · T ${teamCount}` },
+ { label: t("events.race_summary_qualifying"), value: `${cfg.qualifyingRounds || 0} x ${cfg.qualDurationMin || 0} min · ${cfg.carsPerHeat || 0}/heat · ${scoringLabel}` },
+ { label: t("events.race_summary_finals"), value: `${cfg.carsPerFinal || 0}/main · ${cfg.finalLegs || 0} leg · ${getStartModeLabel(cfg.finalStartMode || "position")}` },
+ { label: t("events.race_summary_generation"), value: `${cfg.finalsSource === "practice" ? t("events.finals_from_practice") : t("events.finals_from_qualifying")} · bump ${cfg.bumpCount || 0}` },
+ { label: t("events.race_summary_validation"), value: lapWindow },
+ { label: t("events.race_summary_follow_up"), value: `${cfg.followUpSec || 0}s` },
+ ];
+}
+
+export function getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selectedPreset, { t }) {
+ const cfg = event.raceConfig || {};
+ const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
+ const warnings = [];
+ const isEndurancePreset = selectedPreset?.id === "endurance";
+ const hasQualifying = sessions.some((session) => session.type === "qualification");
+ const hasFinals = sessions.some((session) => session.type === "final");
+ const hasAnySession = sessions.length > 0;
+ if (participantCount <= 0) warnings.push({ message: t("events.summary_warning_no_participants"), target: "manage-setup-participants" });
+ if (!hasAnySession) warnings.push({ message: t("events.summary_warning_no_sessions"), target: "manage-session-plan" });
+ if (!isEndurancePreset && !hasQualifying) warnings.push({ message: t("events.summary_warning_no_qualifying"), target: "manage-generation" });
+ if (!isEndurancePreset && !hasFinals) warnings.push({ message: t("events.summary_warning_no_finals"), target: "manage-generation" });
+ if (isEndurancePreset && raceTeams.length <= 0) warnings.push({ message: t("events.summary_warning_no_teams"), target: "manage-setup-teams" });
+ const minLapMs = Math.max(0, Number(cfg.minLapMs || 0) || 0);
+ const maxLapMs = Math.max(0, Number(cfg.maxLapMs || 0) || 0);
+ if (maxLapMs > 0 && maxLapMs <= minLapMs) warnings.push({ message: t("events.summary_warning_invalid_lap_window"), target: "manage-format" });
+ return warnings;
+}
+
+export function renderManageStatusBadge(status, { t }) {
+ return `${t(`events.status_${status}`)}`;
+}
+
+export function renderRaceWizardSteps({ t, raceWizardStep, escapeHtml }) {
+ const steps = [t("events.wizard_step_1"), t("events.wizard_step_2"), t("events.wizard_step_3"), t("events.wizard_step_4")];
+ return steps
+ .map(
+ (label, index) => `
+
+ ${index + 1}
+ ${escapeHtml(label)}
+
+ `
+ )
+ .join("");
+}
+
+export function renderRaceWizardContent(draft, classOptions, wizardDrivers, preset, { t, escapeHtml, raceWizardStep, getRaceFormatPresets, getClassName, getRaceWizardSessionPlan }) {
+ if (raceWizardStep === 1) {
+ return `
+
+ `;
+ }
+ if (raceWizardStep === 2) {
+ return `
+
+
+
+
+ ${wizardDrivers.length ? `
+
+ ${wizardDrivers
+ .map(
+ (driver) => `
+
+ `
+ )
+ .join("")}
+
+ ` : `${t("events.wizard_no_class_drivers")}
`}
+ `;
+ }
+ if (raceWizardStep === 3) {
+ const isEndurance = preset.id === "endurance";
+ return `
+
+ ${t("events.wizard_finals_note")}
+ `;
+ }
+ const selectedClassName = getClassName(draft.classId);
+ const selectedDrivers = draft.driverIds.length ? draft.driverIds.length : wizardDrivers.length;
+ return `
+
+
+ ${t("events.wizard_summary_title")}
+ ${escapeHtml(draft.name || "-")}
+
+
+ ${t("table.class")}
+ ${escapeHtml(selectedClassName || "-")}
+
+
+ ${t("events.race_summary_preset")}
+ ${escapeHtml(preset?.label || t("events.preset_custom"))}
+
+
+ ${t("events.race_summary_participants")}
+ ${selectedDrivers}
+
+
+ ${t("events.wizard_summary_sessions")}
+ ${escapeHtml(getRaceWizardSessionPlan(draft).join(" • ") || "-")}
+
+
+ `;
+}
+
+export function renderRaceStandingsTable(rows, emptyLabel, { t, renderTable, escapeHtml }) {
+ if (!rows.length) {
+ return `${emptyLabel}
`;
+ }
+
+ return renderTable(
+ [t("table.pos"), t("table.driver"), t("table.score")],
+ rows.map(
+ (row) => `
+
+ | ${row.rank} |
+ ${escapeHtml(row.driverName || t("common.unknown_driver"))} |
+
+ ${escapeHtml(row.score || "-")}
+ ${row.scoreNote ? `${escapeHtml(row.scoreNote)} ` : ""}
+ |
+
+ `
+ )
+ );
+}
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..f710c7d
--- /dev/null
+++ b/src/settings.js
@@ -0,0 +1,531 @@
+export function renderSettings(deps) {
+ const {
+ dom,
+ ammc,
+ state,
+ t,
+ backend,
+ settingsStorageNotice,
+ settingsStorageNoticeIsError,
+ getManagedWsUrl,
+ getBackendUrl,
+ getDefaultBackendUrl,
+ escapeHtml,
+ saveState,
+ renderView,
+ connectDecoder,
+ playPassingBeep,
+ playFinishSiren,
+ openOverlayWindow,
+ buildOverlayUrl,
+ normalizeObsOverlaySettings,
+ DEFAULT_OBS_OVERLAY_SETTINGS,
+ OBS_LAYOUTS,
+ OBS_THEMES,
+ pingBackend,
+ syncStateToBackend,
+ buildDataExportPayload,
+ downloadJsonFile,
+ importDataPayload,
+ buildDirectoryExportPayload,
+ downloadCsvFile,
+ buildDriversCsvRows,
+ buildCarsCsvRows,
+ buildExportMeta,
+ normalizeStoredRacePreset,
+ saveAmmcConfigToBackend,
+ refreshAmmcStatus,
+ startManagedAmmc,
+ stopManagedAmmc,
+ } = deps;
+ const ammcStatus = ammc.status || {};
+ const ammcOutput = Array.isArray(ammcStatus.lastOutput) ? ammcStatus.lastOutput : [];
+ const suggestedWsUrl = getManagedWsUrl();
+ const running = Boolean(ammcStatus.running);
+
+ dom.view.innerHTML = `
+
+
+
+
+
${t("settings.expected_json")}: {"msg":"PASSING", "transponder":232323, "rtc_time":"..."}
+
${t("dashboard.live_note")}
+
+
+
+
+
+
+
+
+
+
+
${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))}
+
${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.stream}: ${entry.line}`).join("\n")
+ : "-"
+ )}
+
+
+
+
+
+
+
${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.storage_note_full")}
+
+
+
+
+
+
+
${t("settings.storage_note_directory")}
+
+
+
+
+
+
+
${t("settings.storage_note_csv")}
+
+
+
+
+
+ ${settingsStorageNotice ? `
${escapeHtml(settingsStorageNotice)}
` : ""}
+
+
+
+
+
+
+
${t("settings.race_presets_note")}
+
${(state.settings.racePresets || []).length} preset(s)
+
+
+
+
+
+
+ `;
+
+ document.getElementById("settingsForm")?.addEventListener("submit", (e) => {
+ e.preventDefault();
+ const form = new FormData(e.currentTarget);
+ state.settings.wsUrl = String(form.get("wsUrl") || "").trim();
+ state.settings.backendUrl = String(form.get("backendUrl") || "").trim();
+ state.settings.autoReconnect = form.get("autoReconnect") === "on";
+ state.settings.audioEnabled = form.get("audioEnabled") === "on";
+ state.settings.passingSoundMode = String(form.get("passingSoundMode") || "beep");
+ state.settings.finishVoiceEnabled = form.get("finishVoiceEnabled") === "on";
+ 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 RaceController";
+ state.settings.clubTagline = String(form.get("clubTagline") || "").trim() || "RC Timing System";
+ state.settings.pdfFooter = String(form.get("pdfFooter") || "").trim() || "Generated by JMK RB RaceController";
+ 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.getElementById("settingsTestAudio")?.addEventListener("click", () => {
+ playPassingBeep();
+ setTimeout(() => {
+ if (state.settings.finishVoiceEnabled) {
+ playFinishSiren();
+ }
+ }, 180);
+ });
+ 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("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 = () => {
+ try {
+ const parsed = JSON.parse(String(reader.result || "{}"));
+ importDataPayload(parsed, "full");
+ } 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("exportCarsCsv")?.addEventListener("click", () => {
+ downloadCsvFile("live_rc_cars.csv", buildCarsCsvRows());
+ });
+
+ 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),
+ 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),
+ 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 = getManagedWsUrl();
+ saveState();
+ renderView();
+ });
+}