Files
Live_RC/docs/javascript-architecture.md
larssand 3aeb77a042 add stat
2026-08-24 21:28:38 +02:00

2159 lines
56 KiB
Markdown

# Live_RC Architecture Reference
This is the technical architecture reference for the `Live_RC` codebase.
It is intended for debugging, maintenance, and future refactoring work.
This document covers:
- runtime components
- backend/frontend boundaries
- every JavaScript file in the repo
- direct import dependencies
- injected dependencies from `app.js`
- state ownership
- save/rerender rules
- end-to-end data flows
- where to debug specific failures
## 1. Runtime Architecture
## 1.1 Components
The system has four main runtime parts:
1. Browser UI
- `index.html`
- `src/styles.css`
- `src/app.js`
- the rest of `src/*.js`
2. Node backend
- `server.js`
- Express API
- static file serving
- AMMC process control
- PDF export endpoint
- app version endpoint
3. Local persistence
- SQLite via `better-sqlite3`
- main file: `data/rc_timing.sqlite`
- AMMC config JSON: `data/ammc_config.json`
4. Decoder / AMMC runtime
- AMMC process started by backend or manually elsewhere
- browser decoder socket handled in frontend runtime
## 1.2 High-level diagram
```mermaid
flowchart LR
Browser[Browser UI\nindex.html + src/*.js] -->|HTTP GET / static files| Server[server.js / Express]
Browser -->|GET/POST /api/state| Server
Browser -->|GET/POST /api/ammc/*| Server
Browser -->|POST /api/passings| Server
Browser -->|GET /api/app-version| Server
Server -->|persist app state| SQLite[(SQLite)]
Server -->|persist passings| SQLite
Server -->|read/write config| AMMCConfig[(ammc_config.json)]
Server -->|spawn/stop| AMMC[AMMC process]
Browser -->|WebSocket| DecoderWS[AMMC websocket / decoder feed]
```
## 1.3 Ownership split
Frontend owns:
- UI rendering
- in-memory app state while page is active
- decoder websocket client
- leaderboard and standings calculations
- judging/corrections in current page state
Backend owns:
- static asset serving
- persisted app state snapshot
- passings table persistence
- AMMC process management
- PDF generation endpoint
- file-watch based app version bumping
## 2. Repository JavaScript Inventory
## 2.1 Root/runtime files
### `package.json`
Role:
- package metadata
- runtime dependencies
- npm scripts
Dependencies:
- `express`
- `better-sqlite3`
Runtime contract:
- Node.js `>=22 <27`
- npm `>=10`
- dependency graph locked by `package-lock.json`
- `scripts/preinstall.js` rejects unsupported Node/Windows architecture before native install
- `scripts/verify-install.js` loads Express and executes an in-memory SQLite query after install
Scripts:
- `npm start` -> `node scripts/serverctl.js start`
- `npm run start:fg` -> `node server.js`
- `npm stop`
- `npm run status`
- `npm restart`
- `npm run verify`
- `npm test`
- `npm run setup:windows`
### `server.js`
Role:
- backend runtime
Uses:
- `fs`
- `path`
- `os`
- `child_process.spawn`
- `express`
- `better-sqlite3`
- `server/public_training_profile.js`
Owns:
- HTTP server
- API routes
- SQLite schema/init
- static serving
- file watching for app version
- AMMC config + process state
- public overlay route serving via same frontend shell
- tokenized public driver training profile API and standalone mobile shell
- PDF export endpoint
Delegates public training token validation and response shaping to `server/public_training_profile.js`; `server.js` does not duplicate those rules.
Main API groups:
- `/api/health`
- `/api/app-version`
- `/api/state`
- `/api/passings`
- `/api/ammc/config`
- `/api/ammc/status`
- `/api/ammc/start`
- `/api/ammc/stop`
- `/api/export/pdf`
- `/public-training/api/:shareToken`
Static routes of interest:
- `/`
- `/public-overlay`
- `/public-overlay/obs`
- `/public-training/:shareToken`
Runtime environment variables:
- `PORT`: HTTP port, default `8081`
- `HOST`: bind address, default `0.0.0.0`
- `RC_TIMING_DATA_DIR`: optional absolute/relative data directory override; defaults to `<repo>/data`
- `RC_TIMING_PID_FILE`: optional PID-file path used by process control
- `PUBLIC_OVERLAY_TOKEN`: optional shared token for public overlay routes
### `server/public_training_profile.js`
Role:
- pure CommonJS backend helper for tokenized driver profiles
Owns:
- enabled share-token lookup
- selected-driver lap filtering and aggregate statistics
- sanitized branding, class, goal, rank, and recommendation response
- clock-skew-safe personal timer start/window normalization
- bounded current-session lap, best-lap, and average calculations
Security boundary:
- returns no driver IDs, transponders, raw passings, decoder settings, or other driver names
- disabled, short, unknown, or driverless tokens return no profile
- logo data is restricted to bounded PNG/JPEG/WebP data URLs
### `scripts/serverctl.js`
Role:
- local process manager for `server.js`
Owns:
- background start/stop/restart/status
- pid file management
- log file paths
Uses:
- `data/server.pid`
- `logs/server.out.log`
- `logs/server.err.log`
### Windows bootstrap
Files:
- `windows/install.bat`
- `windows/setup.ps1`
- `windows/start_backend.bat`
- `windows/start_ammc.bat`
- `windows/start_all.bat`
- `windows/stop_all.bat`
Dependency flow:
```mermaid
flowchart LR
InstallBat[windows/install.bat] --> SetupPs[windows/setup.ps1]
SetupPs -->|missing/unsupported| Winget[winget OpenJS.NodeJS.LTS]
SetupPs --> VCRuntime[winget Microsoft.VCRedist.2015+.x64]
SetupPs --> NpmCi[npm ci]
NpmCi --> Preinstall[scripts/preinstall.js]
NpmCi --> AllowScripts[package.json allowScripts]
AllowScripts --> Native[better-sqlite3 v12 Windows prebuilt]
NpmCi --> Postinstall[scripts/verify-install.js]
Postinstall --> SQLite[SQLite in-memory query]
Postinstall --> AMMC[AMMC/windows64 runtime check]
SetupPs --> ServerCtl[scripts/serverctl.js]
```
Important constraints:
- the repository should be installed in a short ASCII path such as `C:\\Live_RC`
- `better-sqlite3` is pinned to v12.11.x because its prebuilt Windows binary works with the supported Node.js releases; npm 12 is permitted to run only this dependency installer through `package.json#allowScripts`
- `npm ci --ignore-scripts` is unsupported because it leaves the native SQLite binding uninstalled
- AMMC Windows binaries and DLLs must remain together in `AMMC\\windows64`
- `npm start` is already a background start; batch wrappers must not create a second detached npm process
- server process identity is stored in `data\\server.pid`
- Windows stdout/stderr use `logs\\server.out.log` and `logs\\server.err.log`
- the optional `-OpenFirewall` setup flag opens inbound TCP `8081` and `9000` only when PowerShell is elevated
## 2.2 Frontend files
### `src/app.js`
Type:
- frontend composition root / app shell
Owns:
- global `state`
- bootstrapping
- route/view selection
- local persistence/bootstrap
- top-level DOM rendering
- utility/formatting helpers
- all dependency injection into extracted modules
This is the only file that should know about almost every other frontend module.
### `src/runtime_services.js`
Type:
- frontend runtime helpers
Owns:
- backend hydrate/sync/version polling
- overlay polling/rotation/live refresh
- AMMC config/status/start/stop helpers
- audio/speech helpers
- session timer ticks
### `src/decoder_runtime.js`
Type:
- frontend decoder websocket runtime
Owns:
- connect/disconnect decoder
- process incoming decoder messages
- resolve incoming passings into app state
### `src/timing_logic.js`
Type:
- timing domain logic
Owns:
- active session and timing helpers
- lap validation labels and lap window logic
- leaderboard generation
- practice/qual/final standings
- team race standings and stint logs
- grid helpers
### `src/judging_logic.js`
Type:
- judging domain logic
Owns:
- manual corrections
- invalidate/restore lap
- undo handling
- judging filters
### `src/event_race_logic.js`
Type:
- event/race domain logic
Owns:
- event normalization
- branding normalization
- team normalization
- race presets
- race wizard defaults and session generation
- practice-only Open Practice event/session generation
- sponsor round generation
- event drivers / teams / team driver pool
- qualifying/finals generation and bumps
- race summary/manage status logic
### `src/race_render_helpers.js`
Type:
- race-specific render helpers
Owns:
- team standings block
- team stint log block
- final matrix block
- grid render block
- print/export HTML builders
### `src/race_setup_ui.js`
Type:
- race setup shared UI fragments
Owns:
- race format field cards
- context cards
- summary items/warnings
- wizard steps/content
- standings table UI
### `src/event_common.js`
Type:
- shared event/session utility module
Owns:
- sessions for event
- mode/start-mode labels
- class/event name lookup
- assignment list render
- sessions table render
### `src/event_views.js`
Type:
- outer event/race workspace markup
### `src/event_workspace_controller.js`
Type:
- outer event/race workspace controller
Owns:
- create/edit/delete event or race
- race wizard step flow
- list actions
- opening manager view
### `src/event_manager_view.js`
Type:
- `Hantera` markup
Owns:
- session forms
- race format panel
- branding panel
- team section
- sponsor tools
- assignments section
- summary/actions panels
- team/session modals
### `src/event_manager_controller.js`
Type:
- `Hantera` controller
Owns:
- 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
### `src/overlays.js`
Type:
- overlay renderers
Owns:
- normal overlay
- team overlay
- OBS/public overlay
- side panels
- overlay leaderboard rows
### `src/timing_views.js`
Type:
- timing/judging renderers
Owns:
- timing page UI
- judging page UI
- leaderboard modal
- recent passings UI
- quick-add UI
### `src/core_views.js`
Type:
- overview/admin renderers
Owns:
- Dashboard
- Classes
- Drivers
- Cars
### `src/misc_views.js`
Type:
- misc page renderers
Owns:
- Guide page
- Overlay page inside admin UI
### `src/settings.js`
Type:
- settings page renderer
Owns:
- backend/decoder settings UI
- OBS settings UI
- branding/audio settings UI
- import/export settings UI
### `src/training_logic.js`
Type:
- pure training/statistics logic
Owns:
- training-state normalization
- durable ledger synchronization from `resultsBySession`
- valid driver-lap aggregation across the training ledger
- lifetime/session/day/best-lap/average/consistency calculations
- active Open/Free/regular Practice statistics scoped to one driver and the current session start
- stint splitting after passing gaps greater than `120000 ms`
- best N consecutive laps and fixed-duration result calculation
- five-minute (`300000 ms`) results include the final lap when its start is before the cutoff and expose average pace for the counted laps
- daily summaries, controlled-lap streak, and recent personal-best gain
- rule-based practice recommendation selection
- automatic and custom milestone progress calculation
Rules:
- only passings with a matching `driverId` are included
- invalid laps and lap times at or below `500 ms` are excluded
- anonymous Open Practice transponders are excluded until registered as drivers
- consistency is the P10-to-P90 spread of the latest 30 valid laps divided by their mean
- every save replaces ledger rows only for session IDs that still exist in `resultsBySession`
- ledger rows for a deleted session are retained and become historical records
- `sourceKey` deduplicates repeated saves of the same session passing
- a session change always starts a new stint
- a passing gap strictly greater than 2 minutes starts a new stint; exactly 2 minutes remains in the current stint
- a boundary lap whose `lapMs` contains the full break is excluded from pace metrics
- duration-result ranking compares lap count first and elapsed result time second
- a duration result is valid only after crossing the target and includes a final lap whose start is before the target cutoff
### `src/training_views.js`
Type:
- training page renderer/controller
Owns:
- driver selector
- simple default view with four key statistics, recommendation, next level, and active rewards
- progressive disclosure for goal/share administration, detailed history, and club comparison
- milestone/reward create, claim, and delete actions
- mobile-responsive training markup
- compact live-practice card and primary personal-profile action for the selected driver
Mutations:
- replaces or appends entries in `state.training.goals`
- creates, rotates, enables, or disables entries in `state.training.shares`
- always calls injected `saveState()` before rerender
Route behavior:
- `/?view=training` opens the training view directly
- `driver=<driverId>` selects a driver and supports mobile bookmarks
- an explicitly enabled driver receives `/public-training/<shareToken>` as a separate read-only profile
- replacing or disabling the share invalidates its old route immediately after state sync
### `public-training.html`
Type:
- standalone public-profile entry document
Loads:
- `/src/public_training.css`
- `/src/public_training.js`
Served only after `server.js` validates the enabled share token in `/public-training/:shareToken`.
### `src/public_training.js`
Type:
- standalone public profile frontend
Owns:
- fetching only `/public-training/api/:shareToken`
- rendering one driver's sanitized statistics, goals, rewards, and daily history
- local 20-minute practice timer with 10-minute push/control phases
- token-scoped start-age and bounded-window polling for aggregate lap count, best lap, and average during the personal timer; using an age avoids mobile/server clock-skew errors and the bounded window freezes results after the timer finishes
- Swedish/English copy selected by `lang` query or browser locale
Important isolation:
- does not import `app.js`
- does not call `/api/state`
- does not mutate backend or race state
- timer state is stored in the mobile browser under a token-scoped localStorage key
- session-window responses contain aggregates only; individual lap rows and timestamps are not exposed
### `src/public_training.css`
Type:
- standalone responsive public-profile stylesheet
Owns:
- phone-first layout independent from the admin sidebar/theme system
- profile, level, timer, goals, and daily-history presentation
## 3. Direct Import Dependencies
## 3.1 Static import graph
### `src/app.js` imports
- `src/overlays.js`
- `src/settings.js`
- `src/race_setup_ui.js`
- `src/event_race_logic.js`
- `src/timing_logic.js`
- `src/core_views.js`
- `src/misc_views.js`
- `src/event_common.js`
- `src/event_workspace_controller.js`
- `src/event_manager_view.js`
- `src/event_manager_controller.js`
- `src/runtime_services.js`
- `src/decoder_runtime.js`
- `src/timing_views.js`
- `src/race_render_helpers.js`
- `src/judging_logic.js`
- `src/training_logic.js`
- `src/training_views.js`
### `src/event_workspace_controller.js` imports
- `src/event_views.js`
### Backend/root imports
- `server.js` imports Node core + `express` + `better-sqlite3` + `server/public_training_profile.js`
- `server/public_training_profile.js` imports no runtime modules
- `scripts/serverctl.js` imports Node core only
## 3.2 Injected dependency graph
This project uses dependency injection heavily. These relationships do not appear as `import` statements, but they are architecturally real.
```mermaid
flowchart TD
App[src/app.js]
TRL[src/timing_logic.js]
ERL[src/event_race_logic.js]
EMC[src/event_manager_controller.js]
TV[src/timing_views.js]
OV[src/overlays.js]
MISC[src/misc_views.js]
RT[src/runtime_services.js]
DEC[src/decoder_runtime.js]
EW[src/event_workspace_controller.js]
EView[src/event_views.js]
EMV[src/event_manager_view.js]
App --> TRL
App --> ERL
App --> EMC
App --> TV
App --> OV
App --> MISC
App --> RT
App --> DEC
App --> EW
EW --> EView
EMC --> EMV
```
## 4. `app.js` Wrapper Layer
`app.js` defines thin wrappers around extracted module exports so they receive the current state, current language, formatters, and other app-level helpers.
This wrapper layer is one of the most important parts of the architecture.
## 4.1 Event/race wrappers
- `normalizeRaceTeam`
- `normalizeStoredRacePreset`
- `getRaceFormatPresets`
- `applyRaceFormatPreset`
- `buildRaceFormatConfigFromForm`
- `normalizeBrandingConfig`
- `normalizeEvent`
- `resolveEventBranding`
- `buildDefaultRaceWizardDraft`
- `getRaceWizardPreset`
- `applyRaceWizardPresetDefaults`
- `ensureRaceParticipantsConfigured`
- `buildRaceSession`
- `buildTrackSession`
- `createSponsorRounds`
- `buildRaceSessionsFromWizard`
- `getRaceWizardSessionPlan`
- `getEventDrivers`
- `getEventTeams`
- `getTeamDriverPool`
- `findEventTeamForPassing`
- `generateQualifyingForRace`
- `reseedUpcomingQualifying`
- `generateFinalsForRace`
- `applyBumpsForRace`
## 4.2 Timing wrappers
- `getSessionTypeLabel`
- `getStatusLabel`
- `isUntimedSession`
- `getActiveSession`
- `getSessionTargetMs`
- `getSessionLapWindow`
- `isCountedPassing`
- `getVisiblePassings`
- `getPassingValidationLabel`
- `getSessionTiming`
- `ensureSessionResult`
- `formatLapDelta`
- `formatLeaderboardGap`
- `getCompetitorElapsedMs`
- `getCompetitorPassings`
- `getCompetitorSeedMetric`
- `getSessionEntrants`
- `buildPracticeStandings`
- `getQualifyingPointsValue`
- `isHighPointsTable`
- `compareNumberSet`
- `buildQualifyingTieBreakNote`
- `hasQualifyingPrimaryTie`
- `buildQualifyingStandings`
- `formatTeamActiveMemberLabel`
- `buildTeamRaceStandings`
- `buildTeamStintLog`
- `getSessionGridEntries`
- `getSessionGridOrder`
- `ensureSessionDriverOrder`
- `buildFinalStandings`
## 4.3 Judging wrappers
- `getManualCorrectionSummary`
- `applyCompetitorCorrection`
- `recalculateCompetitorFromPassings`
- `invalidateCompetitorLastLap`
- `restoreCompetitorLastInvalidLap`
- `findPassingByUndoMarker`
- `undoJudgingAdjustment`
- `getJudgeFilteredRows`
- `getJudgeFilteredLog`
## 4.4 Event/session shared wrappers
- `getSessionsForEvent`
- `getModeLabel`
- `normalizeStartMode`
- `getStartModeLabel`
- `getClassName`
- `getEventName`
- `renderAssignmentList`
- `renderSessionsTable`
## 4.5 Runtime wrappers
- `createDefaultAmmcConfig`
- `getManagedWsUrl`
- `loadAmmcConfigFromBackend`
- `saveAmmcConfigToBackend`
- `refreshAmmcStatus`
- `startManagedAmmc`
- `stopManagedAmmc`
- `applyPersistedState`
- `hydrateFromBackend`
- `scheduleBackendSync`
- `syncStateToBackend`
- `pingBackend`
- `checkAppVersion`
- `startAppVersionPolling`
- `startOverlaySync`
- `startOverlayRotation`
- `startOverlayLiveRefresh`
- `ensureAudioContext`
- `playPassingBeep`
- `playFinishSiren`
- `playLeaderCue`
- `playStartCue`
- `playBestLapCue`
- `pushOverlayEvent`
- `speakText`
- `announcePassing`
- `announceRaceFinished`
- `handleSessionTimerTick`
- `tickClock`
## 4.6 Decoder wrappers
- `connectDecoder`
- `disconnectDecoder`
- `processDecoderMessage`
## 5. State Model
Global state owner:
- `src/app.js`
Primary state buckets:
- `state.events`
- `state.sessions`
- `state.drivers`
- `state.cars`
- `state.classes`
- `state.resultsBySession`
- `state.training`
- `state.settings`
- `state.passings`
UI and runtime state in or near `app.js`:
- selected event/session/team/grid IDs
- current view
- overlay mode/view mode
- backend dirty/synced version flags
- version polling flags
- audio context
- decoder connection reference
## 5.1 Persistence split
Frontend-local / backend-mirrored state:
- most of `state`
- synced through `/api/state`
SQLite persisted backend data:
- app state snapshot
- passings history
Separate config file:
- `data/ammc_config.json`
## 6. Save / Rerender Rules
## 6.1 Rule
If a change must survive rerender, update the canonical state array.
Good:
```js
state.events = state.events.map((item) =>
item.id === eventId ? normalizeEvent({ ...item, raceConfig: nextConfig }) : item
);
saveState();
renderView();
```
Risky:
```js
event.raceConfig = nextConfig;
saveState();
rerenderEventManager(eventId);
```
## 6.2 Why this matters
After the code split, many bugs came from:
- changing a local `event` reference
- but not replacing the stored object in `state.events`
- then rerender reading from canonical state and showing the old value again
This pattern is especially important in:
- `src/event_manager_controller.js`
- `src/event_workspace_controller.js`
## 7. Screen Ownership
### Overview
Render chain:
- `app.js` -> `renderView()` -> `renderDashboardView()` in `src/core_views.js`
### Classes / Drivers / Cars
Render chain:
- `app.js` -> `renderView()` -> `renderClassesView()` / `renderDriversView()` / `renderCarsView()`
### Event / Race Setup outer page
Render chain:
- `app.js` -> `renderEventWorkspace(mode)`
- `event_workspace_controller.js` -> `renderEventWorkspaceMarkup()` from `event_views.js`
### Event / Race Setup manager (`Hantera`)
Render chain:
- `app.js` -> `renderEventManager(eventId)`
- `event_manager_controller.js` -> `renderEventManagerMarkup()` from `event_manager_view.js`
### Timing / Judging
Render chain:
- `app.js` -> `renderView()` -> `renderTimingView()` / `renderJudgingView()` from `timing_views.js`
### Overlay admin page
Render chain:
- `app.js` -> `renderOverlay()` -> `renderOverlayPageView()` from `misc_views.js`
### Public overlays
Render chain:
- `app.js` -> `renderOverlay()` -> helpers in `overlays.js`
### Settings
Render chain:
- `app.js` -> `renderView()` -> `renderSettings()`
## 8. End-to-End Flows
## 8.1 Page load
```mermaid
sequenceDiagram
participant B as Browser
participant A as app.js
participant R as runtime_services.js
participant S as server.js
participant DB as SQLite
B->>A: load app.js
A->>A: loadState()
A->>R: hydrateFromBackend()
R->>S: GET /api/state
S->>DB: read app_state
DB-->>S: stored JSON
S-->>R: state payload
R-->>A: applyPersistedState()
A->>A: renderNav()/renderView()
```
## 8.2 Event manager save flow
```mermaid
sequenceDiagram
participant UI as event_manager_view.js
participant C as event_manager_controller.js
participant A as app.js
participant R as runtime_services.js
participant S as server.js
UI->>C: submit form
C->>C: build normalized object/config
C->>A: update canonical state array
C->>A: saveState()
A->>R: schedule/sync backend state
R->>S: POST /api/state
C->>A: renderView()/rerenderEventManager()
```
## 8.3 Decoder passing flow
```mermaid
sequenceDiagram
participant D as Decoder/AMMC WS
participant DR as decoder_runtime.js
participant TL as timing_logic.js
participant A as app.js
participant S as server.js
participant DB as SQLite
D->>DR: websocket message
DR->>A: resolve session/driver/car/team
DR->>TL: lap validation + result mutation inputs
DR->>A: update resultsBySession
A->>S: POST /api/passings
S->>DB: insert passing
A->>A: rerender timing/overlay
```
## 8.4 Public overlay flow
```mermaid
sequenceDiagram
participant O as Public overlay page
participant A as app.js
participant R as runtime_services.js
participant S as server.js
participant DB as SQLite
O->>A: load public overlay route
A->>R: hydrateFromBackend()
R->>S: GET /api/state
S->>DB: read app_state
DB-->>S: stored JSON
S-->>R: state snapshot
A->>A: render overlay view
A->>R: overlay sync polling
```
## 9. Dependency/Ownership Matrix
| File | Type | Direct imports | Imported by | Owns DOM? | Owns state? | Mutates canonical state? |
|---|---|---|---|---|---|---|
| `src/app.js` | shell | many | browser entry | yes | yes | yes |
| `src/runtime_services.js` | runtime helpers | none | `app.js` | no | no | indirectly via injected setters |
| `src/decoder_runtime.js` | runtime helpers | none | `app.js` | no | no | yes, through injected state access |
| `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/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 |
| `src/event_views.js` | view | none | `event_workspace_controller.js` | yes | no | no |
| `src/event_workspace_controller.js` | controller | `event_views.js` | `app.js` | yes | no | yes |
| `src/event_manager_view.js` | view | none | `app.js` | yes | no | no |
| `src/event_manager_controller.js` | controller | none | `app.js` | yes | no | yes |
| `src/overlays.js` | view/render | none | `app.js` | yes | no | no |
| `src/timing_views.js` | view/render | none | `app.js` | yes | no | no |
| `src/core_views.js` | view/render | none | `app.js` | yes | no | no |
| `src/misc_views.js` | view/render | none | `app.js` | yes | no | no |
| `src/settings.js` | view/render | none | `app.js` | yes | no | no |
| `src/training_logic.js` | logic | none | `app.js` | no | no | no, returns normalized/derived data |
| `src/training_views.js` | view/controller | none | `app.js` | yes | no | yes, through injected state access |
| `public-training.html` | standalone entry | `public_training.css`, `public_training.js` | `server.js` route | root only | no | no |
| `src/public_training.js` | standalone public view | none | `public-training.html` | yes | local timer only | no backend mutation |
| `src/public_training.css` | standalone stylesheet | n/a | `public-training.html` | n/a | no | no |
| `server/public_training_profile.js` | backend pure logic | none | `server.js`, tests | no | no | no |
| `server.js` | backend runtime | node core + deps | npm/node | n/a | backend process state | yes |
| `scripts/serverctl.js` | process control | node core | npm scripts | n/a | no | pid/log files only |
## 10. Symptom-Based Entry Points
### Value reverts after save
Open first:
- `src/event_manager_controller.js`
- `src/event_race_logic.js`
- `src/app.js`
### Button click does nothing
Open first:
- matching `*_view.js`
- matching `*_controller.js`
### Overlay/timing crashes with missing helper
Open first:
- `src/app.js`
- target module signature / deps object
### Wrong drivers in race/team flow
Open first:
- `src/event_race_logic.js`
- `src/event_manager_controller.js`
- `src/event_manager_view.js`
### Decoder online but no laps
Open first:
- `src/decoder_runtime.js`
- `src/timing_logic.js`
- `src/runtime_services.js`
### Public overlay works differently from admin overlay
Open first:
- `src/overlays.js`
- `src/misc_views.js`
- `src/runtime_services.js`
- `server.js`
## 11. Frontend vs Backend Boundary
Start in frontend when the problem is:
- form save/revert
- button binding
- modal behavior
- wrong driver/team/session visibility
- leaderboard/standings math
- overlay layout or missing values
Start in backend when the problem is:
- persisted backend state endpoint
- AMMC process control
- public overlay route resolution
- SQLite schema/data corruption
- PDF export endpoint
- app version/file-watch reload
## 12. Maintenance Discipline
Whenever a new module is added or a major responsibility moves:
- update this file
- add direct imports
- note who calls the module
- note whether it mutates canonical state
- note whether the module owns DOM or only logic
This file should stay strict and technical. It is a debugging map, not user documentation.
## 13. Known Fragile Paths
These are the parts of the codebase that have historically been most likely to regress after refactors.
### 13.1 `src/event_manager_controller.js`
Why fragile:
- owns many independent save paths in one controller
- mixes DOM binding, normalization, state updates, and rerender logic
- uses both `state.events` and `state.sessions`
- depends on a large injected dependency set from `app.js`
High-risk operations:
- race format save
- preset apply/save/delete
- session create/edit/delete
- team create/edit/delete
- assignment actions
- grid actions
Typical failures:
- button click works but value reverts after rerender
- wrong scope for team drivers vs race participants
- session type saves wrong value
- modal save button stops submitting after markup change
Debug rule:
- first verify the handler fires
- then verify canonical array update
- then verify rerender reads the updated object
### 13.2 `src/event_race_logic.js`
Why fragile:
- contains defaults and normalization logic
- owns race presets and wizard defaults
- decides driver/team visibility in race flows
High-risk functions:
- `normalizeEvent(...)`
- `buildRaceFormatConfigFromForm(...)`
- `buildOpenPracticeBundle(...)`
- `getEventDrivers(...)`
- `getTeamDriverPool(...)`
- `generateQualifyingForRace(...)`
- `generateFinalsForRace(...)`
Typical failures:
- saved values snap back to defaults
- wrong driver lists in race/team setup
- preset application overwrites custom changes
Debug rule:
- check whether normalization is reintroducing fallback values
- check whether the controller passes the correct event object into these helpers
### 13.3 `src/app.js` dependency wiring
Why fragile:
- almost every extracted module still depends on `app.js` wiring
- one missing helper injection can break a whole screen
Typical failures:
- `... is not defined`
- `... is not a function`
- only one overlay mode breaks while others work
- a view loads but one action silently fails
Debug rule:
- inspect the wrapper in `app.js`
- inspect the called module signature
- verify the helper is passed through exactly once
### 13.4 `src/timing_logic.js`
Why fragile:
- central math for leaderboard, standings, lap validity, and team timing
- used by normal race, qualifying, finals, free practice, and team race
High-risk functions:
- `getSessionLapWindow(...)`
- `buildLeaderboard(...)`
- `buildQualifyingStandings(...)`
- `buildTeamRaceStandings(...)`
- `buildTeamStintLog(...)`
Typical failures:
- invalid laps counted or hidden incorrectly
- gap/delta metrics wrong
- team standings or stints split wrong
Debug rule:
- confirm incoming session config first
- then inspect row/metric calculation
- only after that inspect rendering
### 13.5 `src/runtime_services.js`
Why fragile:
- multiple background loops interact with UI and backend state
- can produce symptoms that look unrelated to runtime logic
Typical failures:
- decoder online/offline after refresh
- page reloads at the wrong time
- overlay timer/preview stops moving
- stale backend state overwrites local changes
Debug rule:
- inspect timers and polling before touching screen renderers
## 14. Function-Level Map
This section lists the most important functions to know in each critical file.
### 14.1 `src/app.js`
#### Bootstrap and persistence
- `init()`
- application startup
- runs initial render/bootstrap flow
- `seedDefaultData()`
- seeds initial local data when needed
- `loadState()`
- reads persisted frontend state
- `saveState(options)`
- persists frontend state and schedules backend sync
- `buildPersistableState()`
- selects what is written to backend/local persistence
#### Routing and rendering
- `renderNav()`
- main side navigation render
- `renderView()`
- main route switch for current page
- `renderEventWorkspace(mode)`
- thin wrapper into event workspace controller
- `renderEventManager(eventId)`
- thin wrapper into event manager controller
- `renderOverlay()`
- overlay/admin overlay dispatch point
#### Normalization and formatting
- `normalizeDriver(...)`
- `normalizeCar(...)`
- `normalizeSession(...)`
- `formatLap(...)`
- `formatPredictedLapDelta(...)`
- `formatCountdown(...)`
- `formatElapsedClock(...)`
- `formatRaceClock(...)`
- `formatSeedMetric(...)`
#### Export/print helpers
- `buildRacePackagePayload(...)`
- `importRacePackagePayload(...)`
- `openPrintWindow(...)`
- `requestPdfExport(...)`
- `exportRaceStartListsPdf(...)`
- `exportRaceResultsPdf(...)`
- `exportTeamRaceResultsPdf(...)`
- `exportSessionHeatSheetPdf(...)`
#### Decoder/session helpers still local
- `persistPassingToBackend(...)`
- `validateTrackSessionForStart(...)`
- `findDuplicateSessionTransponders(...)`
- `autoAssignTrackSession(...)`
### 14.2 `src/event_manager_controller.js`
#### Manager setup
- `renderEventManagerView(context)`
- main controller entry
- resolves event/session/team/current manager state
- binds all forms and buttons in `Hantera`
#### Important internal controller patterns
- `refreshManager()`
- full rerender helper used after saves
- `updateEvent(updater)`
- canonical `state.events` update path
#### High-risk save sections inside this controller
- branding save form submit
- race format form submit
- preset apply/save/delete handlers
- session create form submit
- session edit form submit
- team create form submit
- team edit form submit
- team delete action
If one of these breaks, inspect this controller first.
### 14.3 `src/event_race_logic.js`
#### Normalization
- `normalizeEvent(...)`
- central race/event normalization
- `normalizeBrandingConfig(...)`
- `normalizeRaceTeam(...)`
- `normalizeStoredRacePreset(...)`
#### Race format / preset logic
- `getRaceFormatPresets(...)`
- `applyRaceFormatPreset(...)`
- `buildRaceFormatConfigFromForm(...)`
#### Race wizard logic
- `buildDefaultRaceWizardDraft(...)`
- `getRaceWizardPreset(...)`
- `applyRaceWizardPresetDefaults(...)`
- `buildRaceSessionsFromWizard(...)`
- `getRaceWizardSessionPlan(...)`
#### Pool/scoping logic
- `getEventDrivers(...)`
- `getEventTeams(...)`
- `getTeamDriverPool(...)`
- `findEventTeamForPassing(...)`
#### Race generation logic
- `generateQualifyingForRace(...)`
- `reseedUpcomingQualifying(...)`
- `generateFinalsForRace(...)`
- `applyBumpsForRace(...)`
### 14.4 `src/timing_logic.js`
#### Session state
- `getActiveSession(...)`
- `getSessionTargetMs(...)`
- `getSessionLapWindow(...)`
- `getSessionTiming(...)`
#### Leaderboard core
- `ensureSessionResult(...)`
- `buildLeaderboard(...)`
- `getCompetitorPassings(...)`
- `getCompetitorSeedMetric(...)`
- `formatLeaderboardGap(...)`
- `formatLapDelta(...)`
#### Standings
- `buildPracticeStandings(...)`
- `buildQualifyingStandings(...)`
- `buildFinalStandings(...)`
- `buildTeamRaceStandings(...)`
- `buildTeamStintLog(...)`
#### Grid
- `getSessionGridEntries(...)`
- `getSessionGridOrder(...)`
- `ensureSessionDriverOrder(...)`
### 14.5 `src/runtime_services.js`
#### Backend sync/runtime
- `hydrateFromBackendHelper(...)`
- `syncStateToBackendHelper(...)`
- `scheduleBackendSyncHelper(...)`
- `pingBackendHelper(...)`
- `checkAppVersionHelper(...)`
- `startAppVersionPollingHelper(...)`
#### Overlay/runtime refresh
- `startOverlaySyncHelper(...)`
- `startOverlayRotationHelper(...)`
- `startOverlayLiveRefreshHelper(...)`
- `tickClockHelper(...)`
- `handleSessionTimerTickHelper(...)`
#### AMMC/backend control
- `loadAmmcConfigFromBackendHelper(...)`
- `saveAmmcConfigToBackendHelper(...)`
- `refreshAmmcStatusHelper(...)`
- `startManagedAmmcHelper(...)`
- `stopManagedAmmcHelper(...)`
#### Audio/event feed
- `pushOverlayEventHelper(...)`
- `announcePassingHelper(...)`
- `announceRaceStartedHelper(...)`
- `announceRaceFinishedHelper(...)`
- `speakTextHelper(...)`
- `getSpeechVoicesHelper(...)`
- `selectSpeechVoice(...)`
- `getSessionAnnouncementText(...)`
Start and finish lifecycle announcements are initiated from `timing_views.js`, while timer-based finish uses `handleSessionTimerTickHelper(...)`. Passing announcements use `competitor.speechName` when available. Speech does not interrupt another driver name; a busy speech queue falls back to the passing beep. Start and finish announcements deliberately interrupt pending passing speech.
### 14.6 `server.js`
#### Backend API and persistence
- `initSchema()`
- initializes SQLite schema
- `watchAppFiles()`
- bumps app version when `index.html` or any top-level `src/*.js` / `src/*.css` file changes
- `loadAmmcConfig()` / `saveAmmcConfig()`
- AMMC config file persistence
- `normalizeAmmcConfig(...)`
- validates/stabilizes AMMC config
- `buildAmmcStatus()`
- current AMMC process status object
- `startAmmcProcess()` / `stopAmmcProcess()`
- process lifecycle
- PDF export helpers below `/api/export/pdf`
#### Critical backend routes
- `GET /api/state`
- `POST /api/state`
- `POST /api/passings`
- `GET/POST /api/ammc/*`
- `GET /api/app-version`
These are the first backend touchpoints to inspect if the frontend bug crosses the API boundary.
## 15. Frontend State Schema
This section describes the practical shape of the frontend state as persisted by `buildPersistableState()` in `src/app.js`.
## 15.1 Persisted top-level shape
```js
{
classes: Class[],
drivers: Driver[],
cars: Car[],
events: Event[],
sessions: Session[],
resultsBySession: Record<string, SessionResult>,
training: TrainingState,
activeSessionId: string,
settings: Settings
}
```
## 15.2 `Class`
```js
{
id: string,
name: string
}
```
Produced/normalized by:
- `normalizeImportedClass()` in `src/app.js`
## 15.3 `Driver`
```js
{
id: string,
name: string,
classId: string,
transponder: string,
brand: string,
speechName: string // optional phonetic form used only by browser speech
}
```
Normalized by:
- `normalizeDriver()` in `src/app.js`
Used by:
- `state.drivers`
- race participant selection
- team selection
- decoder passing resolution
- exports/imports
## 15.4 `Car`
```js
{
id: string,
name: string,
transponder: string,
brand: string
}
```
Normalized by:
- `normalizeCar()` in `src/app.js`
Used by:
- `state.cars`
- sponsor event shared cars
- team race car selection
- decoder passing resolution
## 15.5 `Event`
Practical shape:
```js
{
id: string,
name: string,
date: string,
classId: string,
mode: "track" | "race",
branding: BrandingConfig,
raceConfig: RaceConfig
}
```
Normalized by:
- `normalizeEvent()` in `src/event_race_logic.js`
### `BrandingConfig`
```js
{
brandName: string,
brandTagline: string,
pdfFooter: string,
pdfTheme: "" | "classic" | "minimal" | "motorsport",
logoDataUrl: string
}
```
Normalized by:
- `normalizeBrandingConfig()` in `src/event_race_logic.js`
### `RaceConfig`
Practical normalized shape:
```js
{
presetId: string,
qualifyingScoring: "points" | "best",
qualifyingRounds: number,
carsPerHeat: number,
qualDurationMin: number,
qualStartMode: string,
qualSeedLapCount: number,
qualSeedMethod: "best_sum" | "average" | "consecutive",
countedQualRounds: number,
qualifyingPointsTable: "rank_low" | "field_desc" | "ifmar",
qualifyingTieBreak: "rounds" | "best_lap" | "best_round",
carsPerFinal: number,
finalLegs: number,
countedFinalLegs: number,
finalDurationMin: number,
finalStartMode: string,
followUpSec: number,
minLapMs: number,
maxLapMs: number,
bumpCount: number,
reserveBumpSlots: boolean,
driverIds: string[],
participantsConfigured: boolean,
finalsSource: "practice" | "qualifying",
teams: RaceTeam[]
practiceOnly: boolean
}
```
`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`
```js
{
id: string,
name: string,
driverIds: string[],
carIds: string[]
}
```
Normalized by:
- `normalizeRaceTeam()` in `src/event_race_logic.js`
## 15.6 `Session`
Practical shape:
```js
{
id: string,
eventId: string,
name: string,
type: string,
durationMin: number,
followUpSec: number,
startMode: string,
status: string,
startedAt: number | null,
endedAt: number | null,
staggerGapSec: number,
maxCars: number,
seedLapCount: number,
seedMethod: string,
generated: boolean,
driverIds: string[],
manualGridIds: string[],
gridLocked: boolean,
...additional race/session fields
}
```
Normalized by:
- `normalizeSession()` in `src/app.js`
Used by:
- race setup
- timing view
- team race
- overlay
- exports/printouts
## 15.7 `resultsBySession`
Practical shape:
```js
{
[sessionId: string]: {
competitors: {
[rowKey: string]: {
key: string,
driverId?: string,
driverName?: string,
carId?: string,
carName?: string,
teamId?: string,
teamName?: string,
transponder: string,
laps: number,
bestLapMs: number,
lastLapMs: number,
totalMs: number,
manualLapAdjustment?: number,
manualTimeAdjustmentMs?: number,
invalidPending?: boolean,
invalidReason?: string,
...runtime-derived leaderboard fields
}
},
passings: Passing[],
adjustments?: Adjustment[]
}
}
```
This bucket is the most dynamic part of state and is mutated heavily during live timing and judging.
## 15.8 `TrainingState`
```js
{
goals: TrainingGoal[],
ledger: TrainingLap[],
shares: TrainingShare[]
}
```
`TrainingGoal`:
```js
{
id: string,
driverId: string,
title: string,
reward: string,
targetLaps: number,
createdAt: string, // ISO timestamp
claimedAt: string // empty until reward is delivered
}
```
`TrainingShare`:
```js
{
driverId: string,
token: string, // 24 random bytes encoded as base64url (192 bits)
enabled: boolean,
createdAt: string
}
```
Share rules:
- no share exists until an admin explicitly enables the selected driver profile
- only one normalized share entry is retained per driver
- disabled shares fail public route lookup
- rotating a share replaces its token; the previous URL no longer resolves after persistence
- tokens are generated in the browser with `crypto.getRandomValues()`, never from the shorter UI `uid()` helper
`TrainingLap`:
```js
{
id: string,
sourceKey: string, // sessionId + passing timestamp + competitor + passing index
sessionId: string,
sessionName: string,
eventId: string,
eventName: string,
driverId: string,
timestamp: number,
lapMs: number,
recordedAt: string
}
```
Normalized by:
- `normalizeTrainingState()` wrapper in `src/app.js`
- `normalizeTrainingState()` implementation in `src/training_logic.js`
- `syncTrainingLedger()` wrapper in `src/app.js`
- `syncTrainingLedger()` implementation in `src/training_logic.js`
Persisted through:
- `buildPersistableState()` in `src/app.js`
- `applyPersistedStateHelper()` in `src/runtime_services.js`
- existing full-state `POST /api/state` backend path
Derived statistics are intentionally not stored. They are recalculated from `state.training.ledger`. Before persistence, `saveState()` synchronizes every session currently present in `resultsBySession`: existing rows for those session IDs are replaced from current valid passings, so judging corrections are reflected. Session IDs absent from `resultsBySession` are not removed from the ledger, preserving lifetime totals after archival or deletion.
Migration behavior:
- older state without `training.ledger` normalizes to an empty ledger
- the next `saveState()` imports valid registered-driver laps from all current session results
- invalid laps, anonymous transponders, and laps at or below `500 ms` are excluded
## 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
This shape is broad and partly feature-driven. It is normalized through `applyPersistedState()` and related helpers rather than one single strict schema function.
## 16. Backend API Contract
This section describes the practical contract exposed by `server.js`.
## 16.1 `GET /api/health`
Response:
```json
{
"ok": true,
"dbPath": "/abs/path/to/data/rc_timing.sqlite"
}
```
Use for:
- health check
- confirming backend is alive
## 16.2 `GET /api/app-version`
Response:
```json
{
"revision": 1,
"updatedAt": "2026-03-30T...Z"
}
```
Use for:
- frontend file-change reload detection
- app version polling in runtime services
## 16.3 `GET /api/state`
Success response:
```json
{
"state": { /* persisted frontend state */ },
"updatedAt": "2026-03-30T...Z"
}
```
Empty response when no state exists:
```json
{
"state": null,
"updatedAt": null
}
```
Error response:
```json
{
"error": "Stored app state is invalid JSON"
}
```
Use for:
- frontend hydration
- public overlay hydration
## 16.4 `POST /api/state`
Request body:
- full persistable frontend state object
- effectively the output of `buildPersistableState()`
Success response:
```json
{
"ok": true,
"updatedAt": "2026-03-30T...Z"
}
```
Error response:
```json
{
"error": "Expected JSON body"
}
```
Important note:
- this endpoint stores the full frontend state snapshot in `app_state`
- it is not a patch endpoint
## 16.5 `POST /api/passings`
Request body:
```json
{
"sessionId": "session_...",
"passing": {
"timestamp": 1711111111111,
"transponder": "1234567",
"driverId": "driver_...",
"driverName": "Name",
"carId": "car_...",
"carName": "Car",
"strength": -53.2,
"loopId": "1",
"resend": false,
"...": "raw decoder-derived fields may exist"
},
"sessionResult": {
"...": "optional current resultsBySession[sessionId] snapshot"
}
}
```
Success response:
```json
{
"ok": true
}
```
Error response:
```json
{
"error": "Expected { sessionId, passing }"
}
```
Important note:
- if `sessionResult` is provided, backend also hot-updates `app_state.resultsBySession[sessionId]`
- this is important for overlays hydrating from backend instead of direct decoder websocket
## 16.6 `GET /api/passings`
Query parameters:
- `sessionId` optional
- `limit` optional, max `1000`
Response:
```json
{
"rows": [
{
"id": 1,
"session_id": "session_...",
"timestamp_ms": 1711111111111,
"transponder": "1234567",
"driver_id": "driver_...",
"driver_name": "Name",
"car_id": "car_...",
"car_name": "Car",
"strength": -53.2,
"loop_id": "1",
"resend": 0,
"raw_json": "{...}",
"created_at": "2026-03-30T...Z"
}
]
}
```
Use for:
- reading persisted passings history
- debugging session passings
## 16.7 `GET /api/ammc/config`
Response:
```json
{
"config": { /* normalized AMMC config */ },
"status": { /* buildAmmcStatus() output */ }
}
```
## 16.8 `POST /api/ammc/config`
Request body:
- config object to normalize and save
Success response:
```json
{
"ok": true,
"config": { /* normalized config */ },
"status": { /* buildAmmcStatus() output */ }
}
```
Error response:
```json
{
"error": "...",
"status": { /* buildAmmcStatus() output */ }
}
```
## 16.9 `GET /api/ammc/status`
Response:
- current output of `buildAmmcStatus()`
Practical fields include:
- whether process is running
- pid
- timestamps
- last exit code/signal
- last error
- recent log lines
- resolved executable path
- current normalized config
## 16.10 `POST /api/ammc/start`
Success response:
```json
{
"ok": true,
"status": { /* buildAmmcStatus() */ }
}
```
Error response:
```json
{
"error": "...",
"status": { /* buildAmmcStatus() */ }
}
```
## 16.11 `POST /api/ammc/stop`
Success response:
```json
{
"ok": true,
"status": { /* buildAmmcStatus() */ }
}
```
Error response:
```json
{
"error": "...",
"status": { /* buildAmmcStatus() */ }
}
```
## 16.12 `POST /api/export/pdf`
Request body:
```json
{
"title": "...",
"subtitle": "...",
"brandName": "...",
"brandTagline": "...",
"footer": "...",
"theme": "classic|minimal|motorsport",
"logoDataUrl": "data:image/jpeg;base64,...",
"filename": "results.pdf",
"sections": [
{
"title": "...",
"headers": ["..."],
"rows": [["...", "..."]]
}
]
}
```
Success response:
- binary PDF with `Content-Type: application/pdf`
Error response:
```json
{
"error": "..."
}
```
## 16.13 Public overlay routes
Routes:
- `/public-overlay`
- `/public-overlay/:mode`
Behavior:
- if `PUBLIC_OVERLAY_TOKEN` is set, query param `token` must match or the route returns `403 Forbidden`
- if no token environment variable is set, the overlay route is available without a query token
- on success serves `index.html`
Use for:
- public OBS/browser-source overlays without exposing admin path directly
## 16.14 Public training profile routes
Routes:
- page: `GET /public-training/:shareToken`
- data: `GET /public-training/api/:shareToken`
Authorization:
- `shareToken` must match an enabled entry in `state.training.shares`
- token length is at least 24 characters after normalization
- missing, disabled, replaced, or unknown tokens return `404` rather than revealing whether a driver exists
Sanitized response fields:
- selected driver display name, class, and brand
- numeric club rank without other driver names
- aggregate totals, best/average time, consistency, controlled streak, and daily summaries
- optional `currentSession` aggregates when a valid start age (or backward-compatible recent `since` timestamp) is supplied
- next automatic milestone
- recommendation level and target lap
- selected driver's goal titles, reward text, progress, and claimed state
- public club branding and safe PNG/JPEG/WebP logo data
Explicitly excluded:
- transponder IDs
- driver IDs and names of other drivers
- cars, events, sessions, assignments, and raw passings
- race settings and decoder/AMMC configuration
- complete `/api/state` payload
Static serving boundary:
- Express serves static files only under `/src` and `/docs`
- `data/`, `AMMC/`, `server.js`, scripts, manifests, and package files are not static downloads
- `public-training.html` and `index.html` are served only by explicit routes/fallback handlers
## 17. Regression History / Known Gotchas
This section captures bugs already seen in this codebase after the modular split.
## 17.1 Missing dependency injection from `app.js`
Observed failures:
- `DEFAULT_OBS_OVERLAY_SETTINGS` before initialization
- `OBS_LAYOUTS` before initialization
- `formatLap is not defined`
- `formatPredictedLapDelta is not a function`
- `getPassingValidationLabel` dependency missing
Pattern:
- helper/module was extracted or wrapper changed
- consumer module signature expected injected helper
- `app.js` no longer passed it through correctly
Mitigation:
- whenever a module crashes after split, compare:
- imported function in `app.js`
- wrapper function in `app.js`
- destructured params in target module
## 17.2 Local mutation vs canonical state update
Observed failures:
- race format values reverted after save
- team create/edit/delete looked successful but UI snapped back
- branding changes could be lost after rerender
Pattern:
- local `event` object mutated
- canonical `state.events` was not updated via array replacement
Mitigation:
- in controllers, prefer `updateEvent(...)` / array replacement pattern
- do not rely on mutating local references inside a controller
## 17.3 Event manager save fragility
Observed failures:
- session type saved as `open practice` instead of selected type
- team save buttons did nothing
- team save worked for create but not edit
- race format save reverted min/max values
Pattern:
- `Hantera` mixes many forms, rerenders, and local references in one controller
Mitigation:
- treat `src/event_manager_controller.js` as a high-risk file
- verify submit source, selected DOM node, canonical update, then rerender in that order
## 17.4 Team race pool confusion
Observed failures:
- team race showed class-only drivers when user wanted all drivers
- race participant list and team pool affected each other
- selecting team members unexpectedly affected participant selection or vice versa
Pattern:
- using the same pool/list for:
- race participants
- team selection
Mitigation:
- keep race participant scope and team pool scope separate
- document clearly whether team race uses:
- all drivers
- or only race participants
## 17.5 Overlay/public route issues
Observed failures:
- public overlay loaded plain page without JS/CSS behavior
- overlay preview timer in main app did not tick
- OBS config dropdowns closed immediately
- overlay/logo/theme inconsistencies
Pattern:
- nested routes and overlay runtime had different bootstrap needs
Mitigation:
- prefer absolute asset paths in `index.html`
- keep overlay refresh timers separate from admin rerenders
- test:
- admin overlay page
- popup overlay
- public overlay
- OBS overlay
## 17.6 Backend hydration overwriting local edits
Observed failures:
- edits in admin pages could appear to save and then revert
Pattern:
- local dirty state vs backend hydration/sync race
Mitigation:
- treat backend hydration and local dirty flags as runtime concerns
- inspect `src/runtime_services.js` together with the affected save path
## 18. Automated Regression Tests
Run:
```bash
npm test
```
`tests/training_logic.test.js` covers:
- training-state normalization and duplicate rejection
- invalid, anonymous, and sub-500 ms lap exclusion
- replacement of current-session ledger rows after corrections
- retention of archived/deleted-session history
- per-driver isolation and day/session aggregation
- recommendation branch selection
- custom goal and automatic milestone progress
- selected-driver active-practice isolation, current-start filtering, and non-practice rejection
- two-minute stint boundaries, cross-session isolation, and boundary-lap exclusion
- best-three consecutive calculation and five-minute final-lap/average-pace behavior
`tests/public_training_profile.test.js` covers:
- enabled/disabled/unknown share-token handling
- selected-driver isolation and response sanitization
- personal timer aggregate and frozen-window calculations
- clock-skew-safe query normalization and bounds
`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
- wizard generation of practice, qualifying, and team-race sessions
- qualifying heat generation and manual-grid-safe reseeding
- final generation, reserved bump slots, and bump-up promotion
- timed, follow-up, and untimed session clocks
- race lap windows and best/consecutive seed metrics
- qualifying points, tie comparisons, and leaderboard gaps
- multi-leg final standings and counted-leg scoring
`tests/judging_logic.test.js` covers:
- cumulative lap/time corrections, reset, and correction-state undo
- manual lap invalidation and competitor-result recalculation
- restoration and undo of passing validity
- safe handling when no counted lap exists
- competitor and judging-log filters
`tests/runtime_persistence.test.js` covers:
- persisted entity/settings normalization during backend hydration
- invalid-theme fallback and retention of local setting defaults
- hydration success/error behavior and no-write-back handling
- full-state POST payloads and synced-version advancement
- dirty-state protection when newer local edits occur during a request
- failed-sync behavior without false version advancement
`tests/audio_runtime.test.js` covers:
- explicit and language-based speech voice selection
- practice-specific versus race-specific start/finish phrases
- per-driver phonetic spoken-name selection and busy-speech beep fallback
- race-start overlay and audio cue dispatch
`tests/server_persistence.integration.test.js` starts an isolated backend with a temporary SQLite data directory and covers:
- health/database path reporting
- empty-state response and full app-state round-trip
- passing persistence and session-scoped passing queries
- atomic hot-update of `resultsBySession[sessionId]` for overlay hydration
- preservation of the rest of the stored state during a passing hot-update
The suite uses Node's built-in `node:test` runner and does not add a test-framework dependency. Windows PowerShell/CMD can run the same suite with `npm.cmd test`.
## 19. Recommended Future Additions
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
- 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