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:
@@ -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
|
||||
|
||||
13
ROADMAP.md
13
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.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user