diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a18b82..f5ff1df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The application version is maintained in `package.json` and displayed in the adm No changes recorded yet. +## 1.12.0 - 2026-09-10 + +### Changed + +- The Settings page no longer stacks every section into one long scroll. It's now split into four tabs - Connection (decoder + managed AMMC), Audio, Appearance & overlay (branding, track layouts, OBS), and System & backup (updates, storage/backup, race presets, clear data) - so a specific setting is a click away instead of a scroll. + ## 1.11.0 - 2026-09-10 ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 09bb4c1..722d02f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -265,6 +265,20 @@ Requested 2026-09-10 by the user, after confirming heats seed by speed correctly Verified 2026-09-10: new unit tests (`tests/race_timing_logic.test.js`) cover `parseQualifyingHeatName` and `moveDriversIntoQualifyingHeat` (moves a lone driver out of their old heat, leaves a different round's same-numbered heat untouched, and is a no-op for a session with no parseable heat name). A full headless-browser run against a real, isolated server reproduced the exact reported scenario end to end: generated real qualifying heats for 7 drivers at 3 per heat through the actual "Generate qualifying" button, confirmed heat 3 had exactly one lone driver, opened heat 2's edit form and confirmed it named "Q1 H3" as a warning before saving, ticked the driver in and saved, and confirmed heat 2 gained them while heat 3 became empty - no duplicate entry, nobody left alone. +## Settings page: tabbed layout instead of one long scroll + +Requested 2026-09-10 by the user: the Settings view stacked all eleven panel sections (decoder, race sounds, legacy audio, branding, track layouts, OBS overlay, managed AMMC, updates, storage/backup, clear data, race presets) in one column, making anything past the first couple of sections a long scroll to reach. + +- [x] Added a small tab bar (`src/settings.js`) grouping the existing sections into four tabs, with no change to any section's own markup, ids, or event-binding logic - only wrapper `
` elements were added around existing content, and one out-of-order section (Managed AMMC) was assigned to the Connection tab via a second wrapper further down the page rather than physically moved, so no markup had to be reordered: + - **Anslutning / Connection** - decoder setup + role cards + backend connection + decoder diagnostics + Managed AMMC. + - **Ljud / Audio** - race sounds + legacy audio. + - **Utseende & overlay / Appearance & overlay** - branding, track layouts, OBS overlay config. + - **System & backup** - updates, storage/backup/export-import, clear data (danger zone), race presets. +- [x] The active tab is held in module-scoped state (`activeSettingsTab` in `src/settings.js`), not the URL, so it survives the periodic background re-renders the settings view already gets (update-status polling, decoder diagnostics) without snapping back to the first tab, and persists across switching to another view and back within the same session. +- [x] Tab styling reuses the existing sidebar nav's visual language (`.nav-item`/`.nav-item.active` in `src/styles.css`) via new `.settings-tab`/`.settings-tab.active` classes, kept as a sticky bar at the top of the view so it stays reachable while scrolling a long tab's content. + +Verified 2026-09-10: a full headless-browser check against a real, isolated server confirmed exactly 4 tabs render, only the Connection tab's panels (including the out-of-order Managed AMMC section) are visible by default, clicking each tab shows that tab's controls and hides the others (decoder form, race-sounds table, track layouts, OBS config, storage/export, clear-data buttons, race presets all checked by id), every previously-existing control id is still present in the DOM after the restructure (nothing dropped), and the chosen tab survives switching to another view via the app's real sidebar navigation and back. The full existing `tests:browser` suite (all pre-existing browser regression coverage, including the decoder-settings test that already exercises the `wsUrl` field inside the now-tabbed Connection panel) was also re-run and passed unchanged. + ## Documentation maintenance - [ ] Update the architecture's module/dependency inventory as modules change. diff --git a/package-lock.json b/package-lock.json index 40dcae0..04d9e3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rc-racecore", - "version": "1.11.0", + "version": "1.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rc-racecore", - "version": "1.11.0", + "version": "1.12.0", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.11.1", diff --git a/package.json b/package.json index 46d75c2..3d67d70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-racecore", - "version": "1.11.0", + "version": "1.12.0", "private": true, "description": "RC RaceCore live timing and race control with AMMC ingest and SQLite persistence", "engines": { diff --git a/src/app.js b/src/app.js index 40fa5e2..f5d6ffc 100644 --- a/src/app.js +++ b/src/app.js @@ -1350,6 +1350,10 @@ const TRANSLATIONS = { "timing.first_crossing_start": "Första crossing satte personlig start", "timing.seeding_mode": "Seedning", "timing.position_grid_hint": "Griden visar startordningen för positionsstart i aktiv session.", + "settings.tab_connection": "Anslutning", + "settings.tab_audio": "Ljud", + "settings.tab_appearance": "Utseende & overlay", + "settings.tab_system": "System & backup", "settings.decoder": "Decoder", "settings.main_ws_url": "Main WebSocket-url", "settings.enable_split_decoder": "Aktivera mellantidsdecoder", @@ -2861,6 +2865,10 @@ const TRANSLATIONS = { "timing.first_crossing_start": "First crossing set personal start", "timing.seeding_mode": "Seeding", "timing.position_grid_hint": "The grid shows the start order for position start in the active session.", + "settings.tab_connection": "Connection", + "settings.tab_audio": "Audio", + "settings.tab_appearance": "Appearance & overlay", + "settings.tab_system": "System & backup", "settings.decoder": "Decoder", "settings.main_ws_url": "Main WebSocket URL", "settings.enable_split_decoder": "Enable split decoder", diff --git a/src/settings.js b/src/settings.js index 64249e7..e760b0a 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,5 +1,7 @@ import { decoderDiagnosticsMarkup, mountDecoderDiagnostics } from "./decoder_diagnostics_view.js"; +let activeSettingsTab = "connection"; + function formatBackupSize(bytes) { const value = Math.max(0, Number(bytes || 0)); if (value < 1024) return `${value} B`; @@ -110,6 +112,14 @@ export function renderSettings(deps) { `).join(""); dom.view.innerHTML = ` +
+ + + + +
+ +

${t("settings.decoder")}

@@ -219,6 +229,9 @@ export function renderSettings(deps) {
${decoderDiagnosticsMarkup(state.settings.language)} +
+ +

${t("audio.race_sounds")}

@@ -315,7 +328,9 @@ export function renderSettings(deps) {

${t("settings.voice_note")}

+
+

${t("settings.branding")}

@@ -416,7 +431,9 @@ export function renderSettings(deps) {
+
+

${t("settings.managed_ammc")}

@@ -476,7 +493,9 @@ export function renderSettings(deps) { )}
+ +

${t("settings.update_title")}

@@ -600,9 +619,22 @@ Erik Eriksson;654321;4WD;Associated;;
+ `; mountDecoderDiagnostics(document.getElementById("decoderDiagnostics"), () => state.settings); + const settingsTabButtons = Array.from(document.querySelectorAll(".settings-tab")); + const settingsTabPanels = Array.from(document.querySelectorAll("[data-settings-panel]")); + const applySettingsTab = (tabId) => { + activeSettingsTab = tabId; + settingsTabButtons.forEach((button) => button.classList.toggle("active", button.dataset.settingsTab === tabId)); + settingsTabPanels.forEach((panel) => { panel.hidden = panel.dataset.settingsPanel !== tabId; }); + }; + settingsTabButtons.forEach((button) => { + button.addEventListener("click", () => applySettingsTab(button.dataset.settingsTab)); + }); + applySettingsTab(settingsTabButtons.some((button) => button.dataset.settingsTab === activeSettingsTab) ? activeSettingsTab : "connection"); + const applyRaceAudioFormValues = (form) => { const events = {}; raceAudioEventDefinitions.forEach((definition) => { diff --git a/src/styles.css b/src/styles.css index 254deb8..4c1d016 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1547,6 +1547,47 @@ body { gap: 10px; } +.settings-tabs { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 14px; + position: sticky; + top: 0; + z-index: 2; + padding: 8px 0; + background: var(--bg); +} + +.settings-tab { + border: 1px solid var(--line); + background: var(--surface-soft); + color: var(--text); + border-radius: 10px; + padding: 8px 14px; + cursor: pointer; + font-weight: 600; + font-size: 0.86rem; + transition: all 0.2s ease; +} + +.settings-tab:hover { + border-color: color-mix(in srgb, var(--accent-2) 42%, var(--line)); +} + +.settings-tab.active { + border-color: color-mix(in srgb, var(--accent) 55%, transparent); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--accent) 30%, transparent), + color-mix(in srgb, var(--accent) 10%, transparent) + ); +} + +[data-settings-panel][hidden] { + display: none; +} + .decoder-settings-form { display: grid; gap: 16px;