diff --git a/CHANGELOG.md b/CHANGELOG.md index 84a4452..a75ad75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ The application version is maintained in `package.json` and displayed in the adm No changes recorded yet. +## 1.31.1 - 2026-10-07 + +### Fixed + +- The "update available" link at the bottom of the sidebar opened Settings on whichever tab was last used, Connection by default. It now opens **System & backup** and scrolls to Updates. +- Race Day's "fix" buttons for the decoder or backend now open the Connection tab, and the audio fix opens the Audio tab. + ## 1.31.0 - 2026-10-07 ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 9ae0ead..1b4154e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -729,6 +729,19 @@ Verified 2026-10-07: - 57 browser scenarios pass. - Not covered by an automated test: the Race Day form saving to the layout during practice. +## Settings links land on the right tab + +Reported by the user 2026-10-07: clicking the sidebar "update available" notice didn't land in the right Settings section, which is System & backup. + +Since Settings was split into tabs, the link only switched the view, so the last-used tab (Connection by default) showed. Race Day's preflight "fix" buttons had the same issue. + +- [x] `openSettingsTab(tab, focusId)` in `settings.js`. The update link opens `system` and scrolls to `#settingsUpdateSection`. `navigateToView(view, { settingsTab })` is used by Race Day: decoder and backend fixes open `connection`, the audio fix opens `audio`. + +Verified 2026-10-07: +- A unit test covers the Race Day fix tabs. +- The browser test clicks the sidebar link from the Dashboard and checks that System & backup is active and the Updates section is visible. +- 253 unit and 58 browser scenarios pass. + ## Documentation maintenance - [x] Update the architecture's module/dependency inventory as modules change. diff --git a/package-lock.json b/package-lock.json index b4f922e..1602499 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rc-racecore", - "version": "1.31.0", + "version": "1.31.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rc-racecore", - "version": "1.31.0", + "version": "1.31.1", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.11.1", diff --git a/package.json b/package.json index 4ce83d1..f928147 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-racecore", - "version": "1.31.0", + "version": "1.31.1", "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 12ea834..7b630dd 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,7 @@ import { installListSorting } from "./list_sorting.js"; import { escapeHtml } from "./html_escape.js"; import { getOverlayModeLabel, buildOverlayPanels, renderOverlaySidePanel, renderOverlayLeaderboard, renderTeamOverlay, renderObsOverlay } from "./overlays.js"; -import { renderSettings } from "./settings.js"; +import { renderSettings, openSettingsTab } from "./settings.js"; import { buildRaceDayQueue, buildRaceDayPreflight, getRaceDriverIds } from "./race_day_logic.js"; import { recordTransponderSighting, buildWarmupCheck, createPrologue, getPrologueRemainingMs, holdPrologue, resumePrologue, setPrologueRemaining, takeDuePrologueCall, formatPrologueTime, getPrologueHandoverMs, normalizePrologueSec } from "./prologue_logic.js"; import { findTransponderConflict, getDriverProfileId } from "./driver_profile_logic.js"; @@ -5162,6 +5162,7 @@ function refreshSidebarUpdateLink() { } function navigateToSettings() { + openSettingsTab("system", "settingsUpdateSection"); currentView = "settings"; updateViewUrl(currentView); renderNav(); @@ -5209,7 +5210,9 @@ async function purgeSessionPassings(sessionIds) { } } -function navigateToView(view) { +// `settingsTab` opens Settings on that tab (Race Day's "fix" buttons, for instance). +function navigateToView(view, { settingsTab = "" } = {}) { + if (view === "settings" && settingsTab) openSettingsTab(settingsTab); currentView = view; updateViewUrl(view); renderNav(); diff --git a/src/race_day_view.js b/src/race_day_view.js index 2384471..967e9ca 100644 --- a/src/race_day_view.js +++ b/src/race_day_view.js @@ -4,7 +4,7 @@ import { PROLOGUE_OPTIONS_SEC, formatPrologueOption } from "./prologue_logic.js" export function getRaceDayCheckAction(item, session) { if (item.level === "ok") return null; if (["backend", "decoder", "split_decoder", "pit_decoders", "audio"].includes(item.id)) { - return { view: "settings", label: "nav.settings" }; + return { view: "settings", settingsTab: item.id === "audio" ? "audio" : "connection", label: "nav.settings" }; } if (item.id === "lap_window" && session?.mode === "race") { return { field: "minLapSec", label: "race_day.lap_window_edit" }; @@ -257,7 +257,7 @@ export function renderRaceDayView(deps) {
${t("settings.update_current_version")}: v${escapeHtml(String(updateStatus?.currentVersion || ""))}
@@ -579,6 +587,10 @@ Erik Eriksson;654321;4WD;Associated;; button.addEventListener("click", () => applySettingsTab(button.dataset.settingsTab)); }); applySettingsTab(settingsTabButtons.some((button) => button.dataset.settingsTab === activeSettingsTab) ? activeSettingsTab : "connection"); + if (pendingSettingsFocusId) { + document.getElementById(pendingSettingsFocusId)?.scrollIntoView({ block: "start" }); + pendingSettingsFocusId = ""; + } mountAuthSettings(document.getElementById("authSettings"), { t, escapeHtml }); mountTtsSettings(document.getElementById("ttsSettings"), { state, t, escapeHtml, saveState, speakText }); diff --git a/tests/browser_settings_tabs.js b/tests/browser_settings_tabs.js index 95ffc99..5bcefd9 100644 --- a/tests/browser_settings_tabs.js +++ b/tests/browser_settings_tabs.js @@ -6,7 +6,7 @@ import assert from "node:assert/strict"; // load, and - the actual bug class this guards against - keep the selection when the page // re-renders in place (e.g. after saving the form), not silently reset back to Connection. // activeSettingsTab is a module-level variable specifically so it survives that re-render. -export async function runSettingsTabsSmoke({ evaluate, navigate }) { +export async function runSettingsTabsSmoke({ evaluate, navigate, waitFor }) { await navigate("settings", "settingsForm"); const activeTab = () => evaluate(`document.querySelector(".settings-tab.active")?.dataset.settingsTab`); @@ -56,4 +56,13 @@ export async function runSettingsTabsSmoke({ evaluate, navigate }) { await evaluate(`(() => { document.querySelector('[name="raceAudioRaceCallouts"]').value = "passing"; document.getElementById("settingsForm").requestSubmit(); })()`); assert.equal(await callouts(), "passing"); console.log("PASS the legacy audio panel is gone and race calls default to passing tone only"); + + // The sidebar "update available" link opens Settings on System & backup at Updates, even + // when another tab was the last one used. + await evaluate(`document.querySelector('[data-settings-tab="connection"]').click()`); + await navigate("dashboard", "view"); + await evaluate(`(() => { const link = document.getElementById("appUpdateAvailable"); link.hidden = false; link.click(); })()`); + await waitFor(async () => (await activeTab()) === "system", "update link lands on System & backup"); + assert.equal(await evaluate(`Boolean(document.getElementById("settingsUpdateSection")) && !document.getElementById("settingsUpdateSection").closest("[data-settings-panel]").hidden`), true, "the update section is visible"); + console.log("PASS the sidebar update link opens Settings on System & backup -> Updates"); } diff --git a/tests/browser_smoke.js b/tests/browser_smoke.js index 8db7cab..6cc390a 100644 --- a/tests/browser_smoke.js +++ b/tests/browser_smoke.js @@ -148,7 +148,7 @@ try { await runTeamMembershipSmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base }); await runEventManagerTabsSmoke({ evaluate, navigate, savedState, waitFor, base }); await runEventScheduleSmoke({ evaluate, navigate, fill, savedState, waitFor, base }); - await runSettingsTabsSmoke({ evaluate, navigate }); + await runSettingsTabsSmoke({ evaluate, navigate, waitFor }); await runGuideTabsSmoke({ evaluate, navigate }); await runAuthSmoke({ evaluate, navigate, fill, savedState, waitFor }); await runTtsSettingsSmoke({ evaluate, navigate, savedState, waitFor }); diff --git a/tests/race_day_logic.test.js b/tests/race_day_logic.test.js index 6fbd686..6ed79c3 100644 --- a/tests/race_day_logic.test.js +++ b/tests/race_day_logic.test.js @@ -107,6 +107,8 @@ test("preflight shortcuts respect driver, shared-car and team setup", async () = assert.equal(action(missing, { mode: "race", type: "team_race" }).view, "race_setup"); assert.equal(action({ id: "participants", level: "error" }, { mode: "track" }).view, "events"); assert.equal(action({ id: "decoder", level: "error" }).view, "settings"); + assert.equal(action({ id: "decoder", level: "error" }).settingsTab, "connection", "the decoder fix opens the Connection tab"); + assert.equal(action({ id: "audio", level: "warning" }).settingsTab, "audio", "the audio fix opens the Audio tab"); assert.equal(action({ id: "session", level: "error" }).view, "timing"); assert.equal(action({ id: "lap_window", level: "warning" }, { mode: "race" }).field, "minLapSec"); assert.equal(action({ id: "decoder", level: "ok" }), null);