diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df394a..1e2361b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ The application version is maintained in `package.json` and displayed in the adm No changes recorded yet. +## 1.33.0 - 2026-10-08 + +### Added + +- **đ Test pronunciation** on the Drivers page, in the add and edit forms and per driver in the list. It plays the driver's passing sound and then two real announcements with the spoken name (personal best and winner) through race audio, so you hear the voice you actually use (browser or local Piper). In the forms it uses what is typed right now, so a nickname or a spelling can be tried before saving. + +### Fixed + +- The search field on the Drivers and Cars pages lost focus after the first letter, so a name like "Anders" couldn't be typed straight through. Every keystroke re-rendered the page; focus and the cursor are now put back, as the archive search already did. + ## 1.32.0 - 2026-10-07 ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index 556e034..16731de 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -760,6 +760,20 @@ Verified 2026-10-07: - `tests/browser_corrections_scope.js` checks a running heat: Timing has no penalty or lap-removal buttons and the lap list has no actions, "Open in Judging" lands on Judging with the penalty buttons and the driver. It then checks a running Open Practice: Timing has Remove per lap and no corrections block, and Judging has invalidate but no penalty or reset buttons. - 60 browser scenarios pass. +## Drivers page: search focus, pronunciation test + +Reported by the user 2026-10-08: +- Searching on Drivers jumped out of the field after the first letter. +- It should be possible to hear how a driver's spoken name or nickname sounds in announcements. + +- [x] The Drivers and Cars search inputs re-rendered the whole page on every `input` event, which replaced the field. Focus and the caret are now restored, the same pattern `results_archive_view.js` already used. +- [x] `testDriverVoice` in `app.js` plays the passing sound (`passSound`), then `personal_best` and `race_winner` with `driverName` set to the spoken name, through race audio with `force`. It goes through the same voice path as live announcements. Buttons are in the add and edit forms (reading the unsaved form values) and per row in the list. + +Verified 2026-10-08: +- `tests/browser_drivers_page.js` types "DP And" letter by letter and checks focus after every keystroke. +- It captures `speechSynthesis.speak` in the page and checks that the row button reads the saved spoken name, and that the add-form button reads an unsaved nickname without saving the driver. +- 253 unit and 62 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 13eca9c..ad66cfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rc-racecore", - "version": "1.32.0", + "version": "1.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rc-racecore", - "version": "1.32.0", + "version": "1.33.0", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.11.1", diff --git a/package.json b/package.json index ef6d013..dd3716f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-racecore", - "version": "1.32.0", + "version": "1.33.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 9f71b25..2239d65 100644 --- a/src/app.js +++ b/src/app.js @@ -400,7 +400,17 @@ const processDecoderMessage = (msg, decoderRole = "main") => processDecoderMessa }, decoderRole); const renderDashboard = () => renderDashboardView({ addTrackLayout, state, dom, t, backend, getActiveSession, getStatusLabel, getSessionTypeLabel, getEventName, getModeLabel, getBackendUrl, formatLap, renderSessionsTable, setCurrentView: (view) => { currentView = view; }, updateViewUrl, renderNav, renderView, connectDecoder, disconnectDecoder, openOverlayWindow, ensureAudioContext, playPassingBeep, playFinishSiren, escapeHtml }); const renderClasses = () => renderClassesView({ state, dom, t, selectedClassEditId: () => selectedClassEditId, setSelectedClassEditId: (value) => { selectedClassEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell }); -const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter: () => driverBrandFilter, setDriverBrandFilter: (value) => { driverBrandFilter = value; }, selectedDriverEditId: () => selectedDriverEditId, setSelectedDriverEditId: (value) => { selectedDriverEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName }); +// Driver page "Test pronunciation": the passing sound, then two real announcements with the +// spoken name, through race audio (so the chosen voice / Piper is what you hear). +const testDriverVoice = ({ spokenName, passSound }) => { + if (!spokenName) return; + ensureAudioContext(); + triggerRaceAudio("transponder_pass", { force: true, effectPath: passSound || "" }); + const variables = { driverName: spokenName, lapTime: formatLap(16420), podium: spokenName }; + triggerRaceAudio("personal_best", { force: true, interrupt: true, variables }); + triggerRaceAudio("race_winner", { force: true, variables }); +}; +const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter: () => driverBrandFilter, setDriverBrandFilter: (value) => { driverBrandFilter = value; }, selectedDriverEditId: () => selectedDriverEditId, setSelectedDriverEditId: (value) => { selectedDriverEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName , testDriverVoice }); const renderCars = () => renderCarsView({ state, dom, t, carBrandFilter: () => carBrandFilter, setCarBrandFilter: (value) => { carBrandFilter = value; }, selectedCarEditId: () => selectedCarEditId, setSelectedCarEditId: (value) => { selectedCarEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar }); const renderGuide = () => renderGuideView({ dom, t }); const renderRaceDay = () => renderRaceDayView({ @@ -1030,8 +1040,9 @@ const TRANSLATIONS = { "drivers.transponder_placeholder": "Personlig transponder (valfritt)", "drivers.transponder_conflict_confirm": "Transpondern {tp} anvĂ€nds redan av {name} ({class}). Spara Ă€ndĂ„?", "drivers.speech_name": "Uttalsnamn", + "drivers.voice_test": "đ Testa uttal", "drivers.speech_name_placeholder": "Uttal för upplĂ€sning (valfritt)", - "drivers.speech_name_hint": "Skriv namnet som webblĂ€sarrösten ska uttala det. LĂ€mna tomt för vanligt förarnamn.", + "drivers.speech_name_hint": "Skriv namnet som rösten ska uttala det, eller ett smeknamn. LĂ€mna tomt för vanligt förarnamn. Testa med đ.", "drivers.training_profile": "TrĂ€ningsprofil", "drivers.training_profile_hint": "LĂ€nka flera klass- och transponderposter för samma person till en gemensam trĂ€ningsprofil.", "drivers.training_profile_auto": "Automatiskt: samma namn eller ny profil", @@ -2694,8 +2705,9 @@ const TRANSLATIONS = { "drivers.transponder_placeholder": "Personal transponder (optional)", "drivers.transponder_conflict_confirm": "Transponder {tp} is already used by {name} ({class}). Save anyway?", "drivers.speech_name": "Spoken name", + "drivers.voice_test": "đ Test pronunciation", "drivers.speech_name_placeholder": "Pronunciation for announcements (optional)", - "drivers.speech_name_hint": "Enter the name as the browser voice should pronounce it. Leave blank to use the regular driver name.", + "drivers.speech_name_hint": "Enter the name as the voice should pronounce it, or a nickname. Leave blank to use the regular driver name. Try it with đ.", "drivers.training_profile": "Practice profile", "drivers.training_profile_hint": "Link multiple class and transponder entries for the same person to one shared practice profile.", "drivers.training_profile_auto": "Automatic: same name or new profile", diff --git a/src/core_views.js b/src/core_views.js index d4349ae..0f3ef9e 100644 --- a/src/core_views.js +++ b/src/core_views.js @@ -375,6 +375,7 @@ export function renderDriversView(deps) { bindModalShell, normalizeDriver, getClassName, + testDriverVoice = () => {}, } = deps; const classOptions = state.classes.map((c) => ``).join(""); const driverSearch = driverBrandFilter().trim().toLowerCase(); @@ -408,6 +409,7 @@ export function renderDriversView(deps) { + @@ -444,6 +446,7 @@ export function renderDriversView(deps) {