diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11e5a1a..7b6b83c 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.20.2 - 2026-09-12
+
+### Changed
+
+- Brought `docs/javascript-architecture.md` back up to date with this session's changes: documented `src/track_layouts_logic.js`, `buildEventSchedule`, the Settings/Event Manager tab layouts, and the new `Event.raceConfig.scheduleStartAt`/`scheduleGapMin` and `Session.trackLayoutId` fields; added a real SQLite table contract section and a full field-by-field `Settings` shape (both previously just TODO notes); documented several previously-undocumented backend routes (`/api/update-status`, `/api/update/run`, `/api/network/public-address`, `/api/passings/purge`, the public-training self-service actions); and listed every file `tests/browser_smoke.js` actually invokes.
+
## 1.20.1 - 2026-09-12
### Changed
diff --git a/ROADMAP.md b/ROADMAP.md
index 75aabd0..a9b8ad9 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -496,9 +496,24 @@ Verified 2026-09-11: confirmed all 7 files are served with `200` and the correct
## Documentation maintenance
-- [ ] Update the architecture's module/dependency inventory as modules change.
-- [ ] Complete state/settings/SQLite contracts and critical form-to-controller mappings.
+- [x] Update the architecture's module/dependency inventory as modules change.
+- [x] Complete state/settings/SQLite contracts and critical form-to-controller mappings.
- [x] Consolidate older future-work notes so this file remains the primary roadmap.
- [x] Update the in-app Guide page for this session's shipped features. Added three new "Träningsstatistik" steps (guided-exercise config, mobile self-service, the end-of-practice summary) and a new "Resultatarkiv" section (overview card + 5-step walkthrough); corrected `guide.sqlite_2`/`_6`, which claimed raw SQLite passings are always retained - no longer true now that deletion/clear-data purges them. Verified 2026-09-09 with a headless-browser check confirming the new content renders as real translated text (sv), not raw `guide.*` keys.
-Consolidated 2026-09-09: the raw "Träningsstat" brainstorm notes (Pyramiden, Konsistens-jakten) were folded into section 3's "Guided practice exercises" as a proper specification and removed from here.
\ No newline at end of file
+Consolidated 2026-09-09: the raw "Träningsstat" brainstorm notes (Pyramiden, Konsistens-jakten) were folded into section 3's "Guided practice exercises" as a proper specification and removed from here.
+
+Closed out 2026-09-12, requested by the user alongside the screenshot refresh above: `docs/javascript-architecture.md` (~2850 lines) hadn't kept pace with this whole session's work. Used a research-only Explore pass first to get a concrete diff (doc location vs. actual code) rather than guessing what was stale, then applied it:
+
+- [x] Section 2.2 (file inventory): added `src/track_layouts_logic.js` (previously undocumented anywhere - zero mentions), expanded `src/settings.js`'s and `src/event_manager_view.js`/`event_manager_controller.js`'s entries to describe their tab layouts (`activeSettingsTab`/`activeManageTab`, `MANAGE_TAB_TARGETS`) instead of the pre-tab flat panel lists. Section 2.1: added an entry for `scripts/record-video-guides.mjs` and `scripts/capture-screenshots.mjs`.
+- [x] Section 4.1 (`app.js` wrapper functions): added `buildEventSchedule`, previously absent from every part of the doc despite being wired into both Event Manager and the printed start lists.
+- [x] Section 9 (Dependency/Ownership Matrix): added rows for `driver_profile_logic.js` and `track_layouts_logic.js`.
+- [x] Section 15 (Frontend State Schema): added the missing `trackLayouts`/`audioEventLog` top-level keys and a new `TrackLayout` type (15.1a); added `Event.raceConfig.scheduleStartAt`/`scheduleGapMin` and `Session.trackLayoutId`, both previously undocumented despite driving real features (the race day schedule, track-layout-scoped lap comparisons). Rewrote 15.9 `Settings` from a loose "contains at least" prose list into an actual field-by-field shape grouped by which Settings tab owns each field.
+- [x] New section 15.10, SQLite Table Contract: the two actual tables (`app_state`, `passings`) straight from `initSchema()` in `server.js` - this literally didn't exist before, despite being one of the two explicit roadmap TODOs being closed here.
+- [x] Section 16 (Backend API Contract): added the update-checker routes (`GET /api/update-status`, `POST /api/update/run`), `GET /api/network/public-address`, `POST /api/passings/purge`, and the three public-training self-service action routes (even-laps, step-training, exercise-sessions) plus the QR route - all previously live in `server.js` with zero documentation.
+- [x] Section 18 (Automated Regression Tests): added `tests/driver_profile_logic.test.js` and `tests/track_layouts_logic.test.js` to the unit-test coverage list; rewrote the browser-suite appendix to list all 10 scenario files `tests/browser_smoke.js` actually invokes (it previously named only 2) and fixed a stale line claiming "session/team flows... remain tracked in ROADMAP.md" when both have had real coverage for a while.
+- [x] Section 19 (Recommended Future Additions): removed the 3 items now addressed (resultsBySession schema, settings schema, SQLite contract) and the "browser-level smoke tests" item (long since done), keeping only the genuinely still-open items - neural TTS (deferred pending a provider) and permissions/auth (deferred pending its own design conversation), both already tracked in this file.
+
+Deliberately left as pre-existing, lower-priority gaps rather than closing everything in one pass: several older modules (`race_audio.js`, `sync_status.js`, `html_escape.js`, `list_sorting.js`, `results_archive_view.js`, `decoder_diagnostics.js`/`decoder_diagnostics_view.js`) are documented only in later unnumbered appendix sections, not in section 2.2's main per-file inventory or section 3's import graph - real inconsistencies, but not something this session's changes touched, so fixing them was out of scope for "update as modules change."
+
+Verified 2026-09-12: re-read every edited section back against the actual source (`grep` for each newly-documented symbol/field/route) to confirm nothing was invented; new subsections inserted between existing numbered ones (`15.1a`, `16.2a`, `16.6a`) use a letter suffix rather than renumbering everything after them.
\ No newline at end of file
diff --git a/docs/javascript-architecture.md b/docs/javascript-architecture.md
index c5fa4a6..7f2ddd6 100644
--- a/docs/javascript-architecture.md
+++ b/docs/javascript-architecture.md
@@ -219,6 +219,15 @@ Uses:
- `logs/server.out.log`
- `logs/server.err.log`
+### `scripts/record-video-guides.mjs` and `scripts/capture-screenshots.mjs`
+Role:
+- documentation tooling - not run in production, not imported by `server.js` or any `src/*.js` module
+
+Owns:
+- both spin up an isolated `server.js` (temp `RC_TIMING_DATA_DIR`) plus a headless Chrome instance driven over the DevTools Protocol via a raw `WebSocket`, the same harness pattern `tests/browser_smoke.js` uses - own temp SQLite dir, own browser profile, never touches a real installation
+- `record-video-guides.mjs`: narrated MKV screen-capture clips for new-user video guides (`docs/videos/*.mkv`, linked from `README.md`/`README.sv.md` and the in-app Guide page). Each clip is a list of `{ text, action }` steps; narration audio is synthesized up front via Piper (`pipx install piper-tts` plus a downloaded voice model - not espeak-ng, which was tried first and found barely intelligible), so each line's real spoken duration is known before recording, and the final audio track is built from the actual measured wall-clock offset each line started at (`ffmpeg`'s `adelay`/`amix`), not an idealized schedule - avoids drifting out of sync with page-load/network latency over a 30+ second clip. A `prepare()` hook lets a clip spin up a live dependency (e.g. a fake decoder socket via `tests/browser_fake_decoder.js`) before the app is seeded.
+- `capture-screenshots.mjs`: single-frame JPEG captures (`docs/screenshots/*.jpg`, 1600x900 CSS at `deviceScaleFactor: 2`) against one shared seeded demo dataset ("JMK RB" branding, a finished "Klubbmästare Race 4" event) so screenshots taken at different times stay visually consistent with each other.
+
### Windows bootstrap
Files:
@@ -366,6 +375,7 @@ Owns:
- event drivers / teams / team driver pool
- qualifying/finals generation and bumps
- race summary/manage status logic
+- `buildEventSchedule(event, sessions, { getSessionSortWeight })`: cascades each race-mode session's planned clock time from `event.raceConfig.scheduleStartAt`, in running order (reusing `getSessionSortWeight`'s practice→qualifying→finals ordering from `race_render_helpers.js`), each session's own duration/follow-up plus `scheduleGapMin` between sessions. Once a session has actually started/finished, everything after it re-anchors to that real time - a late start or an overrun (a pause's wall-clock time is already baked into a session's real `endedAt`) pushes the rest of the day forward automatically, with no separate "reschedule" step. Returns `[]` when no `scheduleStartAt` is set. Consumed by Event Manager's Schedule tab and by `buildRaceStartListsHtml` (prints the planned time next to each session).
### `src/race_render_helpers.js`
Type:
@@ -419,27 +429,32 @@ Type:
- `Hantera` markup
Owns:
+- for a race-mode event: a 4-tab layout (`.manage-tabs`: Setup/Participants & teams, Format, Live & results, Schedule & print) wrapping the panels below in `
` groups - session-plan (create/list sessions) and branding stay always visible above the tabs regardless of the active one, as does the permanent 4-card step-status summary (Setup/Format/Generate/Live)
- session forms
-- race format panel
-- branding panel
-- team section
+- race format panel (Format tab)
+- branding panel (always visible)
+- team section (Setup tab)
- sponsor tools
- assignments section
+- Schedule panel (Schedule tab): start-time + gap-between-sessions form, and (once set) a table of every session's planned/actual time and a "Delayed" flag, built from `buildEventSchedule`
- summary/actions panels
- team/session modals
+- track/sponsor (non-race-mode) events skip the tabs entirely and keep the simple, untabbed layout - they never had enough content to need them
### `src/event_manager_controller.js`
Type:
- `Hantera` controller
Owns:
+- `activeManageTab` (module-scoped, same pattern as `settings.js`'s `activeSettingsTab`) and `MANAGE_TAB_TARGETS`, a lookup from a quick-jump target id (the step-status cards, the race-summary's warning links) to the tab it now lives inside, so jumping to it switches tabs first - `scrollIntoView` on an element behind an inactive tab does nothing on its own
- session create/edit/delete
- race format save/apply/delete preset
- branding save/logo save
- team create/edit/delete
- generation actions
- assignment actions
-- print/export/grid actions
+- print/export/grid actions (opening a session's grid editor also switches to the Live tab, since that's where the editor renders)
+- schedule form save/clear (`scheduleForm`/`scheduleClear`)
### `src/overlays.js`
Type:
@@ -502,6 +517,20 @@ Imported by:
- `src/training_logic.js` for class-scoped lap aggregation
- `src/training_views.js` for person and class selectors
+### `src/track_layouts_logic.js`
+Type:
+- pure track-layout identity/lookup helpers
+
+Owns:
+- normalizing a single layout (`{ id, label, startedAt }`) and the installation's whole layout list, sorted by `startedAt`
+- resolving which layout was active at a given timestamp (`getTrackLayoutIdAt`) - layouts are contiguous, so a layout's implicit end is the next one's `startedAt`, with no separate stop field
+- default/next label generation (`getTrackLayoutLabel`, `nextTrackLayoutLabel` - "Layout N")
+
+Imported by:
+- `src/app.js` to stamp `session.trackLayoutId` at the moment a session starts (never recomputed later, so editing the layout list afterwards can't retroactively reclassify old results) and to scope `getTrackRecordMs` (`src/timing_logic.js`) and training pace/consistency stats (`src/training_logic.js`, `server/public_training_profile.js`) to the current layout
+- `src/settings.js` for the "Banlayouter" panel (add/list/delete, delete blocked while any session still references the layout)
+- `src/results_archive_view.js` to show which layout a completed session ran on
+
### `src/misc_views.js`
Type:
- misc page renderers
@@ -515,7 +544,8 @@ Type:
- settings page renderer
Owns:
-- backend/decoder settings UI
+- the page's 4-tab layout (`.settings-tabs`: Connection, Audio, Appearance & overlay, System & backup) - `activeSettingsTab` is module-scoped state, not part of `app.js`'s state object, so the selected tab survives a re-render (e.g. after a save) within the same page load but resets on navigation
+- backend/decoder settings UI (Connection tab) and the "Banlayouter" track-layout panel (Appearance & overlay tab)
- OBS settings UI
- branding/audio settings UI
- import/export settings UI
@@ -766,6 +796,7 @@ This wrapper layer is one of the most important parts of the architecture.
- `reseedUpcomingQualifying`
- `generateFinalsForRace`
- `applyBumpsForRace`
+- `buildEventSchedule`
## 4.2 Timing wrappers
- `getSessionTypeLabel`
@@ -1201,6 +1232,8 @@ sequenceDiagram
| `src/timing_logic.js` | logic | none | `app.js` | no | no | yes, on session results through injected access |
| `src/judging_logic.js` | logic | none | `app.js` | no | no | yes, on session results through injected access |
| `src/event_race_logic.js` | logic | none | `app.js` | no | no | no, returns normalized/generated data |
+| `src/driver_profile_logic.js` | pure logic | none | `app.js`, `core_views.js`, `training_logic.js`, `training_views.js` | no | no | no |
+| `src/track_layouts_logic.js` | pure logic | none | `app.js` | no | no | no |
| `src/race_render_helpers.js` | render helpers | none | `app.js` | yes | no | no |
| `src/race_setup_ui.js` | render helpers | none | `app.js` | yes | no | no |
| `src/event_common.js` | shared helpers | none | `app.js` | mixed | no | no |
@@ -1627,10 +1660,28 @@ This section describes the practical shape of the frontend state as persisted by
resultsBySession: Record,
training: TrainingState,
activeSessionId: string,
- settings: Settings
+ settings: Settings,
+ trackLayouts: TrackLayout[],
+ audioEventLog: AudioEventLogEntry[]
}
```
+`trackLayouts` and `audioEventLog` are both append/edit-only lists with no other top-level state referencing them by shape (sessions reference a layout only by `trackLayoutId`, a plain string - see 15.6). `audioEventLog` records every triggered race-audio cue (`{ event, sessionId, ts, ... }`, see section 20) for diagnostics and the "recent audio events" checks in the browser test suite; it is not surfaced in any admin UI.
+
+## 15.1a `TrackLayout`
+
+```js
+{
+ id: string,
+ label: string, // defaults to "Layout N", user-renamable
+ startedAt: number // epoch ms; layouts are contiguous, so a layout's implicit
+ // end is simply the next layout's startedAt - there is no
+ // separate end/stop field to keep in sync
+}
+```
+
+Normalized by `normalizeTrackLayout`/`normalizeTrackLayouts` in `src/track_layouts_logic.js`. Managed from Settings' "Banlayouter" panel; deleting a layout is blocked while any session still references it via `trackLayoutId`. See that module's doc entry in section 2.2 for how lookups/scoping work.
+
## 15.2 `Class`
```js
@@ -1762,10 +1813,14 @@ Practical normalized shape:
participantsConfigured: boolean,
finalsSource: "practice" | "qualifying",
teams: RaceTeam[]
- practiceOnly: boolean
+ practiceOnly: boolean,
+ scheduleStartAt: number | null, // epoch ms; null when no time-of-day schedule is set
+ scheduleGapMin: number // minutes between sessions, defaults to 3
}
```
+`scheduleStartAt`/`scheduleGapMin` drive `buildEventSchedule()` (section 4.1, and its doc entry under `src/event_race_logic.js` in section 2.2) - the race day Schedule tab's cascading planned-time table. Set/cleared from Event Manager's Schedule tab (`scheduleForm`/`scheduleClear` in `event_manager_controller.js`).
+
`practiceOnly: true` identifies the simplified Open Practice flow. It deliberately has no class scope or configured participants, uses an `open_practice` session, ignores `durationMin` in timing logic, and must not require qualifying/finals in race summary warnings.
### `RaceTeam`
@@ -1806,6 +1861,10 @@ Practical shape:
driverIds: string[],
manualGridIds: string[],
gridLocked: boolean,
+ trackLayoutId: string, // "" if no track layout is configured; stamped once at
+ // commitStart() to whichever layout is active at that
+ // moment (see src/track_layouts_logic.js) and never
+ // recomputed afterwards
...additional race/session fields
}
```
@@ -1976,21 +2035,88 @@ Migration behavior:
## 15.9 `Settings`
-Practical shape contains at least:
-- theme
-- language
-- backendUrl
-- decoderUrl / websocket URL
-- speaker/audio flags
-- `audioEnabled`, `passingSoundMode`, `startSoundEnabled`, `finishSoundEnabled`
-- `finishVoiceEnabled` (legacy property name; controls start/finish voice announcements)
-- `speechVoiceUri`, `speechRate`, `speechPitch`
-- OBS overlay settings
-- branding defaults
-- race preset library
-- AMMC/managed runtime preferences
+Practical shape (field names as actually read/written in `src/settings.js` and `src/app.js` - there is no single strict schema function; it's normalized piecemeal through `applyPersistedState()` and each form's own submit handler):
-This shape is broad and partly feature-driven. It is normalized through `applyPersistedState()` and related helpers rather than one single strict schema function.
+```js
+{
+ // Connection tab
+ theme: "dark" | "nord" | "light",
+ language: "sv" | "en",
+ backendUrl: string,
+ wsUrl: string, // main decoder WebSocket URL
+ splitDecoderEnabled: boolean,
+ splitWsUrl: string,
+ pitDecoderEnabled: boolean,
+ pitEntryWsUrl: string,
+ pitExitWsUrl: string,
+ autoReconnect: boolean,
+ publicBaseUrl: string,
+
+ // Audio tab
+ audioEnabled: boolean,
+ passingSoundMode: string,
+ startSoundEnabled: boolean,
+ finishSoundEnabled: boolean,
+ finishVoiceEnabled: boolean, // legacy name; controls start/finish voice announcements
+ speechVoiceUri: string,
+ speechRate: number,
+ speechPitch: number,
+ speakerPassingCueEnabled: boolean,
+ speakerLeaderCueEnabled: boolean,
+ speakerFinishCueEnabled: boolean,
+ speakerBestLapCueEnabled: boolean,
+ speakerTop3CueEnabled: boolean,
+ speakerSessionStartCueEnabled: boolean,
+ raceAudio: RaceAudioSettings, // see section 20.3 - the newer, more complete race-audio config (countdown, lights, per-event effect/voice toggles, staggered-release timing)
+
+ // Appearance & overlay tab
+ clubName: string,
+ clubTagline: string,
+ pdfFooter: string,
+ pdfTheme: "" | "classic" | "minimal" | "motorsport",
+ logoDataUrl: string,
+ obsOverlay: object, // OBS overlay layout/theme/column config
+
+ // System & backup tab
+ racePresets: StoredRacePreset[]
+}
+```
+
+`trackLayouts` (section 15.1a) lives at the top level, not under `Settings`, even though it's managed from the Appearance & overlay tab alongside branding.
+
+Settings' own 4-tab layout (`activeSettingsTab`, module-scoped - see the `src/settings.js` doc entry in 2.2) groups these fields as marked above; the underlying `state.settings` object itself has no such grouping, it's a flat bag of fields.
+
+## 15.10 SQLite Table Contract
+
+Defined in `initSchema()` in `server.js`. There are exactly two tables - almost everything else in the app (classes, drivers, cars, events, sessions, results, settings, training, track layouts) lives in one JSON blob, not relational rows; see "Persistence split" (section 5.1) for why.
+
+```sql
+CREATE TABLE app_state (
+ id INTEGER PRIMARY KEY CHECK (id = 1), -- always exactly one row
+ state_json TEXT NOT NULL, -- the entire frontend state tree (section 15.1), as JSON
+ updated_at TEXT NOT NULL
+);
+
+CREATE TABLE passings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ session_id TEXT NOT NULL,
+ timestamp_ms INTEGER NOT NULL,
+ transponder TEXT NOT NULL,
+ driver_id TEXT,
+ driver_name TEXT,
+ car_id TEXT,
+ car_name TEXT,
+ strength REAL,
+ loop_id TEXT,
+ resend INTEGER NOT NULL DEFAULT 0,
+ raw_json TEXT NOT NULL,
+ created_at TEXT NOT NULL
+);
+CREATE INDEX idx_passings_session ON passings(session_id);
+CREATE INDEX idx_passings_timestamp ON passings(timestamp_ms);
+```
+
+`app_state` is a single-row key/value store, not a normalized table - `POST /api/state` (16.4) always replaces the whole `state_json` blob (see the optimistic-locking discussion in ROADMAP.md, section 2). `passings` is the one durable, append-only record of every raw decoder message that isn't reachable through `/api/state` at all - only deletable via `POST /api/passings/purge` (16.6a), never overwritten by a state save. Its columns match `GET /api/passings`'s response shape (16.6) exactly, field for field.
## 16. Backend API Contract
@@ -2026,6 +2152,16 @@ Use for:
- frontend file-change reload detection
- app version polling in runtime services
+## 16.2a `GET /api/update-status`, `POST /api/update/run`
+
+`GET /api/update-status` (optional `?force=1` to bypass its own cache) runs `checkForUpdate()` and returns whether a newer commit is available on the configured git remote, cached for `UPDATE_CHECK_CACHE_MS`. Backs the in-app update checker (Settings, System & backup tab) and the sidebar "New version - update" link (ROADMAP.md "In-app update checker and one-click updater").
+
+`POST /api/update/run` responds immediately (`{ ok: true, message: "..." }`) and spawns a **detached** `scripts/serverctl.js update` child process that outlives the request - it stops the currently running server, `git pull`s, runs `npm install`, then starts a fresh server, reusing the same stop/start machinery as `npm run restart`. The HTTP response returns before any of that happens, so the frontend cannot observe success/failure directly; it just expects the server to become briefly unreachable and then come back on the new version.
+
+## 16.2b `GET /api/network/public-address`
+
+Calls `https://api.ipify.org` server-side (5s timeout) and returns `{ address, suggestedBaseUrl }`. Used by Settings to suggest a public Backend URL when configuring remote access; the outbound call happens from the backend, not the browser, so it works even when the admin page itself is only reachable over LAN/VPN.
+
## 16.3 `GET /api/state`
Success response:
@@ -2164,6 +2300,10 @@ Use for:
- reading persisted passings history
- debugging session passings
+## 16.6a `POST /api/passings/purge`
+
+Body: `{ "sessionIds": string[] }`. Deletes every raw `passings` row for the given session ids and responds `{ ok: true, deleted: }`. The raw lap-crossing log is the one piece of session data not reachable at all through `/api/state` (it lives only in the append-only SQLite `passings` table, deliberately never included in the state blob a `POST /api/state` could overwrite - see section 17's "Backend hydration overwriting local edits" gotcha), so deleting an event/session or running the "Rensa data" clear-data flow calls this separately to avoid leaving orphaned rows behind with no UI ever pointing at them again.
+
## 16.7 `GET /api/ammc/config`
Response:
@@ -2307,8 +2447,9 @@ Use for:
Routes:
- page: `GET /public-training/:shareToken`
+- QR code for the page's own URL: `GET /public-training/qr/:shareToken`
- data: `GET /public-training/api/:shareToken`
-- restricted marker action: `POST /public-training/api/:shareToken/markers`
+- restricted actions (same token authorization as the data route): `POST /public-training/api/:shareToken/markers`, `.../even-laps`, `.../step-training`, `.../exercise-sessions` - the driver-self-service actions (marking a lap, "Jämna varv" lap-evening, step-training logging, and guided exercise session logging) each read-modify-write only their own slice of `state.training` server-side rather than accepting a client's full state blob, so an idle mobile session can't revert unrelated admin edits the way a raw `POST /api/state` could (see section 17.6).
Authorization:
- `shareToken` must match an enabled entry in `state.training.shares`
@@ -2489,6 +2630,11 @@ npm test
- personal timer aggregate and frozen-window calculations
- clock-skew-safe query normalization and bounds
+`tests/driver_profile_logic.test.js` covers `findTransponderConflict`:
+- flags a transponder shared across two different personal profiles
+- never flags one person's own multi-class entries sharing a transponder
+- names the conflicting driver and class in the returned conflict
+
`tests/race_timing_logic.test.js` covers:
- race-format parsing, defaults, lap limits, participants, and teams
- practice-only Open Practice bundle creation and persisted mode markers
@@ -2499,6 +2645,15 @@ npm test
- race lap windows and best/consecutive seed metrics
- qualifying points, tie comparisons, and leaderboard gaps
- multi-leg final standings and counted-leg scoring
+- `getEventDrivers` preserving participant-selection order (not directory creation order) and dropping stale/out-of-class ids
+- `getSessionSortWeight`'s practice-first ordering (`??` not `||`, since `open_practice`'s own weight is `0`)
+- `buildEventSchedule`'s cascading planned-time math, running-order sort, track-mode exclusion, and re-anchoring to a session's real start/end (including overtime from a pause)
+- `personalTimeUp` (staggered-mode personal window elapsed, excluding paused time) and the staggered-mode shared-clock extension
+
+`tests/track_layouts_logic.test.js` covers:
+- layout normalization/sorting and label defaulting/renaming
+- `getTrackLayoutIdAt`'s contiguous "layout active at time X" lookup at its boundaries
+- `getTrackRecordMs` scoping a live lap's comparison to only same-layout sessions, never conflating a layout-tagged session with an untagged legacy one
`tests/judging_logic.test.js` covers:
- cumulative lap/time corrections, reset, and correction-state undo
@@ -2539,18 +2694,17 @@ The suite uses Node's built-in `node:test` runner and does not add a test-framew
## 19. Recommended Future Additions
+Closed out 2026-09-12: `resultsBySession`'s competitor row schema (15.7), a full `settings` schema snapshot (15.9), and the SQLite table contract (15.10) are now documented; the browser regression suite (section 18) already covers every critical create/edit/save workflow (classes, drivers, cars, sponsor events, the race wizard, team management, Race Day start/pause/resume/finish, session-type-specific start sequences, Settings tabs, Event Manager tabs, the schedule panel, OBS popup, decoder reconnect).
+
If the project grows further, the next useful additions to this document are:
-- optional neural TTS provider integration for natural start, finish, and driver-name announcements
+- optional neural TTS provider integration for natural start, finish, and driver-name announcements (deferred at the user's request pending a chosen provider - see ROADMAP.md, "More natural race speech")
- backend-only provider credentials with no API key material in frontend state, exports, or public overlays
- server-side speech generation and content-addressed cache under `data/tts/` so race playback never waits for a cloud request
- cached audio invalidation when language, voice, phrase, or a driver's `speechName` changes
- offline fallback order: cached neural audio -> uploaded WAV/MP3 -> browser system voice -> passing beep
- provider health/status and a pre-generate-all-names action in Settings before race day
-- exact `resultsBySession` competitor row schema
-- exact `settings` schema snapshot
-- SQLite table contract section
-- controller-to-view ID map for critical forms/buttons
-- browser-level smoke tests for critical create/edit workflows
+- a full controller-to-view ID map for every form/button (large - the doc currently documents this per-feature as each area is touched, e.g. the manage-tab targets in 2.2's `event_manager_controller.js` entry, rather than as one exhaustive cross-reference)
+- permissions/authentication design once that work actually starts (ROADMAP.md, "Permissions and mobile Race Control" - deferred, no identity system exists yet)
## 20. Race Audio Architecture
@@ -2660,9 +2814,23 @@ The application currently has no authentication or role model. Race-control soun
- Existing app-version polling reports server contact through an optional `onContact` callback. `src/app.js` updates `lastContactAt`/`connectionChecked` and renders status text once per second without rebuilding forms. Contact older than 15 seconds is stale. Decoder connectivity remains separate.
- `index.html` owns `syncStatus`, `syncStatusLabel`, `syncStatusDetail`, `syncStatusRetry`, and `syncWarning`. Retry uses the existing full-state POST when local changes or write errors exist. Successful full-state synchronization at the current local version clears the passing error too. Keep the page open when unsaved changes exist; this is not a new durable offline queue or a multi-controller conflict-resolution mechanism.
- `tests/sync_status.test.js` verifies status precedence and state-write failure/retry. `tests/browser_smoke.js` drives Chrome/Chromium/Edge over its local debugging protocol with Node's WebSocket API. It launches an isolated server using `RC_TIMING_DATA_DIR` and a temporary browser profile. `CHROME_PATH` overrides discovery. `RC_BROWSER_NO_SANDBOX=1` is an explicit opt-in for restricted test environments; normal runs keep the browser sandbox enabled.
-- The browser suite covers register CRUD creation/editing (not deletion), drafts through two version polls, reload persistence, failed offline saves and retry. Session/team flows and CI platform coverage remain tracked in ROADMAP.md.
+- The browser suite covers register CRUD creation/editing (not deletion), drafts through two version polls, reload persistence, failed offline saves and retry, session/team flows, and Race Day's full session lifecycle. CI platform coverage (Windows/Linux runs) remains tracked in ROADMAP.md as the one still-open item under "Reliability and browser regression tests" - everything else in that roadmap section is done.
-Browser scenarios are split into `tests/browser_race_setup.js` (sponsor-event/race/team/session persistence) and `tests/browser_overlays.js` (shared overlay column alignment and sorting isolation). Both are invoked by `tests/browser_smoke.js` using its isolated browser/server harness. Overlay headings and rows use the same `--overlay-columns` grid tracks in `src/styles.css`; narrow interactive lists scroll horizontally as one unit. OBS grid rows retain their compact-screen wrapping because they have no sorting toolbar.
+`tests/browser_smoke.js` is the entry point (`npm run test:browser`); it wires together one isolated server/browser instance and invokes every scenario file below in sequence:
+- `tests/browser_race_setup.js` - sponsor-event/race/team/session creation, editing, and reload persistence
+- `tests/browser_team_membership.js` - moving a driver between teams (the duplicate-membership bug fix), team-scoped driver pool, and team roster cleanup on driver deletion
+- `tests/browser_event_manager_tabs.js` - Event Manager's 4-tab layout: default tab, per-tab panel visibility, the quick-jump step cards/warning links switching tabs before scrolling, and the Open Grid button jumping to the Live tab
+- `tests/browser_event_schedule.js` - the Schedule tab: saving/clearing a time-of-day schedule, the cascaded running-order table, and the "Delayed" badge
+- `tests/browser_settings_tabs.js` - Settings' 4-tab layout: default tab, per-tab panel visibility, and the active tab surviving a save-triggered re-render
+- `tests/browser_race_day.js` - Race Day start/pause/resume/finish, switching to the next queued heat, against a simulated decoder socket
+- `tests/browser_race_day_start_sequences.js` - the real countdown/light/audio start sequence for heat/qualification/final/team_race/practice sessions (as opposed to open_practice/free_practice's instant start)
+- `tests/browser_obs_popup.js` - the OBS overlay popup opening via a real click, compact layout, and independent live-updates
+- `tests/browser_decoder_reconnect.js` - auto-reconnect after a dropped socket, settling on exactly one connection, and auto-reconnect staying off when disabled
+- `tests/browser_overlays.js` - shared overlay column alignment and sorting isolation across viewport sizes
+
+`tests/browser_fake_decoder.js` is a shared helper (not a scenario file itself): it completes just the WebSocket upgrade handshake so any scenario needing a "connected decoder" doesn't need real AMMC hardware, with `dropConnections()` to simulate a network interruption.
+
+Overlay headings and rows use the same `--overlay-columns` grid tracks in `src/styles.css`; narrow interactive lists scroll horizontally as one unit. OBS grid rows retain their compact-screen wrapping because they have no sorting toolbar.
## Decoder Diagnostics (Browser-Local)
diff --git a/package-lock.json b/package-lock.json
index c3d42cc..d154e93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rc-racecore",
- "version": "1.20.1",
+ "version": "1.20.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rc-racecore",
- "version": "1.20.1",
+ "version": "1.20.2",
"hasInstallScript": true,
"dependencies": {
"better-sqlite3": "^12.11.1",
diff --git a/package.json b/package.json
index 7a63234..9a100af 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rc-racecore",
- "version": "1.20.1",
+ "version": "1.20.2",
"private": true,
"description": "RC RaceCore live timing and race control with AMMC ingest and SQLite persistence",
"engines": {