fix(settings): open the right Settings tab from the update link and Race Day

The sidebar update notice now lands on System & backup at Updates, and
Race Day's decoder/backend/audio fix buttons open the matching tab,
instead of whichever tab was used last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-10-07 20:44:28 +02:00
parent 9d79bcb4d3
commit 7f267e17fe
10 changed files with 57 additions and 11 deletions

View File

@@ -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

View File

@@ -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.

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

View File

@@ -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();

View File

@@ -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) {
<div class="panel-body race-day-checks">
${checks.map((item) => {
const action = getRaceDayCheckAction(item, session);
return `<article class="race-day-check is-${item.level}"><span aria-hidden="true"></span><div><strong>${t(`race_day.check_${item.id}`)}</strong><small>${renderPreflightDetail(item, { t, formatLap, escapeHtml })}</small>${item.level !== "ok" ? `<small>${t(`race_day.check_${item.level}`)}</small>` : ""}${action ? `<button class="btn btn-mini race-day-check-action" type="button" ${action.view ? `data-race-day-fix-view="${action.view}"` : `data-race-day-fix-field="${action.field}"`}>${t(action.label)}</button>` : ""}</div></article>`;
return `<article class="race-day-check is-${item.level}"><span aria-hidden="true"></span><div><strong>${t(`race_day.check_${item.id}`)}</strong><small>${renderPreflightDetail(item, { t, formatLap, escapeHtml })}</small>${item.level !== "ok" ? `<small>${t(`race_day.check_${item.level}`)}</small>` : ""}${action ? `<button class="btn btn-mini race-day-check-action" type="button" ${action.view ? `data-race-day-fix-view="${action.view}"${action.settingsTab ? ` data-race-day-fix-tab="${action.settingsTab}"` : ""}` : `data-race-day-fix-field="${action.field}"`}>${t(action.label)}</button>` : ""}</div></article>`;
}).join("")}
</div>
${session?.mode === "race" && lapWindowCheck ? `
@@ -286,7 +286,7 @@ export function renderRaceDayView(deps) {
</section>`;
dom.view.querySelectorAll("[data-race-day-fix-view]").forEach((button) => {
button.addEventListener("click", () => navigateTo(button.dataset.raceDayFixView));
button.addEventListener("click", () => navigateTo(button.dataset.raceDayFixView, { settingsTab: button.dataset.raceDayFixTab || "" }));
});
dom.view.querySelectorAll("[data-race-day-fix-field]").forEach((button) => {
button.addEventListener("click", () => {

View File

@@ -3,6 +3,14 @@ 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));
@@ -441,7 +449,7 @@ export function renderSettings(deps) {
<div class="panel-body" id="authSettings"></div>
</section>
<section class="panel mt-16">
<section class="panel mt-16" id="settingsUpdateSection">
<div class="panel-header"><h3>${t("settings.update_title")}</h3></div>
<div class="panel-body">
<p>${t("settings.update_current_version")}: <strong>v${escapeHtml(String(updateStatus?.currentVersion || ""))}</strong></p>
@@ -579,6 +587,10 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
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 });

View File

@@ -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");
}

View File

@@ -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 });

View File

@@ -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);