2343 lines
67 KiB
Markdown
2343 lines
67 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 personal training profiles
|
|
|
|
Owns:
|
|
- enabled share-token lookup
|
|
- profile resolution across several race-specific driver records
|
|
- strict per-class lap filtering and aggregate statistics
|
|
- sanitized branding, class-specific 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 profileless 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/driver_profile_logic.js`
|
|
Type:
|
|
- pure personal-profile identity helpers
|
|
|
|
Owns:
|
|
- resolving an explicit `profileId` or a legacy normalized-name profile key
|
|
- grouping race-specific driver records that belong to one person
|
|
- listing all driver IDs and class IDs attached to one personal profile
|
|
|
|
Imported by:
|
|
- `src/core_views.js` for profile assignment in the Drivers UI
|
|
- `src/training_logic.js` for class-scoped lap aggregation
|
|
- `src/training_views.js` for person and class selectors
|
|
|
|
### `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, scoped by personal profile and class
|
|
- lifetime/session/day/best-lap/average/consistency calculations
|
|
- active Open/Free/regular Practice statistics scoped to one personal profile, one class, 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
|
|
- `profileId` links multiple race-specific driver records to one person without changing decoder or race identity
|
|
- statistics combine linked driver records only when their `classId` matches the selected class
|
|
- 2WD and 4WD lap times, stints, best-three, five-minute results, and recommendations are never combined
|
|
- legacy driver records without `profileId` are grouped by normalized driver name until explicitly linked or separated in the Drivers UI
|
|
- 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=<profileId>` selects a personal profile and `class=<classId>` selects its performance class for bookmarks
|
|
- an explicitly enabled personal profile receives `/public-training/<shareToken>` as a separate read-only profile with a class selector
|
|
- 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 person's sanitized statistics, goals, rewards, and daily history with a class selector
|
|
- switching between class-specific statistics without combining lap performance across classes
|
|
- 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-and-class-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`
|
|
|
|
### Training/profile imports
|
|
- `src/core_views.js` imports `src/driver_profile_logic.js`
|
|
- `src/training_logic.js` imports `src/driver_profile_logic.js`
|
|
- `src/training_views.js` imports `src/driver_profile_logic.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.3.1 Multi-decoder topology and ownership
|
|
|
|
The decoder runtime supports four explicit roles. Roles are semantic, not inferred from `loop_id`; the WebSocket connection used to receive a message determines how it is processed.
|
|
|
|
| Runtime role | Frontend URL setting | Managed AMMC config | Default port | Canonical mutation |
|
|
|---|---|---|---:|---|
|
|
| `main` | `settings.wsUrl` | `decoderHost` / `wsPort` | `9000` | validates and appends `result.passings`; increments laps |
|
|
| `split` | `settings.splitWsUrl` | `splitDecoderHost` / `splitWsPort` | `9001` | appends `result.splits`; updates split fields only |
|
|
| `pit_entry` | `settings.pitEntryWsUrl` | `pitEntryDecoderHost` / `pitEntryWsPort` | `9002` | opens pit state and appends a pit entry event |
|
|
| `pit_exit` | `settings.pitExitWsUrl` | `pitExitDecoderHost` / `pitExitWsPort` | `9003` | closes pit state and appends the measured stop |
|
|
|
|
Physical and network chain:
|
|
|
|
```text
|
|
physical loop -> physical decoder IP -> one AMMC child process on backend host
|
|
-> unique AMMC WebSocket port -> browser WebSocket client with fixed role
|
|
-> processDecoderMessageHelper(message, deps, role)
|
|
```
|
|
|
|
Example complete installation:
|
|
|
|
```text
|
|
Main decoder 192.168.1.10 -> backend AMMC :9000 -> role main
|
|
Split decoder 192.168.1.11 -> backend AMMC :9001 -> role split
|
|
Pit entry decoder 192.168.1.12 -> backend AMMC :9002 -> role pit_entry
|
|
Pit exit decoder 192.168.1.13 -> backend AMMC :9003 -> role pit_exit
|
|
Browser host 192.168.1.100 uses ws://192.168.1.100:9000 through :9003
|
|
```
|
|
|
|
Invariants:
|
|
|
|
- only `main` may change `entry.laps`, `entry.lastTimestamp`, or append to `result.passings`
|
|
- split timing is measured from the latest main timestamp, or mass-start timestamp before the first completed lap
|
|
- a duplicate pit-entry signal while `entry.pitActive` is true is ignored
|
|
- pit exit without a prior pit entry is ignored
|
|
- an optional channel can disconnect without stopping main lap counting
|
|
- main, split, pit entry, and pit exit must use distinct AMMC WebSocket ports
|
|
- every physical decoder normally has a distinct IP; reusing one feed for different roles would process the same crossing with conflicting semantics
|
|
|
|
### 8.3.2 Multi-decoder frontend lifecycle
|
|
|
|
`src/app.js` owns role-indexed `wsClients` and `reconnectTimers` maps. `connectDecoderHelper()` first disconnects all clients, always connects `main`, then connects enabled optional roles. `connectDecoderRole()` binds the role into every message callback. Auto-reconnect checks the current enable flags before reconnecting optional channels.
|
|
|
|
Decoder runtime state is persisted with separate status fields:
|
|
|
|
```js
|
|
decoder: {
|
|
connected, lastMessageAt, lastError,
|
|
splitConnected, splitLastMessageAt, splitLastError,
|
|
pitEntryConnected, pitEntryLastMessageAt, pitEntryLastError,
|
|
pitExitConnected, pitExitLastMessageAt, pitExitLastError
|
|
}
|
|
```
|
|
|
|
Race data is stored under `resultsBySession[sessionId]`:
|
|
|
|
```js
|
|
{
|
|
passings: MainPassing[],
|
|
splits: SplitPassing[],
|
|
pitEvents: PitEvent[],
|
|
pitStops: PitStop[],
|
|
competitors: {
|
|
[competitorKey]: {
|
|
lastSplitMs, previousSplitMs, lastSplitDeltaMs, bestSplitMs,
|
|
lastSplitTimestamp, lastSplitLap,
|
|
pitActive, pitEnteredAt, pitStops,
|
|
lastPitStopMs, totalPitMs, bestPitStopMs
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The full result object is included in `/api/state` persistence. Only main passings are posted to `/api/passings` and the SQLite passings table. This preserves the existing lap ledger and prevents split/pit records from being interpreted as laps. If split or pit history later needs SQL reporting, add dedicated tables and endpoints rather than inserting those records into `passings`.
|
|
|
|
### 8.3.3 Split/pit attribution, analysis, and presentation
|
|
|
|
`src/decoder_runtime.js` snapshots the active member on `pit_entry` into the competitor record:
|
|
|
|
```js
|
|
{
|
|
pitEntryDriverId, pitEntryDriverName,
|
|
pitEntryCarId, pitEntryCarName,
|
|
pitEntryTransponder
|
|
}
|
|
```
|
|
|
|
On `pit_exit`, the resulting `PitStop` uses that entry snapshot rather than the currently active team member. This is essential for endurance races: a team may switch driver, car, or transponder while stopped. `exitTransponder` is stored separately and must not overwrite the entry attribution. After the stop is appended, the snapshot fields are cleared.
|
|
|
|
Analysis ownership is deliberately split into two pure functions:
|
|
|
|
| Function | Module | Scope | Consumers |
|
|
|---|---|---|---|
|
|
| `buildSessionChannelStats(session, deps)` | `src/timing_logic.js` | one heat/session, grouped by competitor key | Timing, Race Setup, team standings, HTML reports, PDF reports |
|
|
| `buildDriverTimingChannelStats(state, driverId, classId)` | `src/training_logic.js` | one personal driver profile and selected class across sessions | Training detailed statistics |
|
|
| `buildPublicTimingChannelStats(state, classDriverIds)` | `server/public_training_profile.js` | sanitized aggregate for one shared profile class | public training profile API and mobile view |
|
|
|
|
`buildSessionChannelStats()` aggregates split count/best/average and pit count/total/best/average, and returns a chronological enriched pit-stop log. `renderSessionChannelAnalysis()` in `src/race_render_helpers.js` is the canonical HTML presentation. Do not independently recalculate these values in Timing or Race Setup.
|
|
|
|
`buildDriverTimingChannelStats()` resolves every driver record belonging to the same personal profile but applies the selected class filter before reading `resultsBySession`. Consequently multiple transponders in 2WD can be aggregated while 2WD and 4WD data remain separate.
|
|
|
|
The public builder repeats the class isolation on the server because the client must never receive the full state. Its output is limited to split count/best/average and pit count/total/best/average. It intentionally excludes transponders, competitor keys, driver IDs, event names, timestamps, and individual pit stops. Keep the serialization security assertion in `tests/public_training_profile.test.js` whenever this payload changes.
|
|
|
|
Presentation/data path:
|
|
|
|
```text
|
|
decoder_runtime -> resultsBySession[sessionId].splits/pitStops
|
|
-> buildSessionChannelStats
|
|
-> renderSessionChannelAnalysis
|
|
-> Timing / Race Setup / HTML print
|
|
-> buildSessionChannelPdfSections -> PDF export
|
|
|
|
resultsBySession -> buildDriverTimingChannelStats
|
|
-> Training / More statistics
|
|
|
|
resultsBySession -> buildPublicTimingChannelStats (server-side sanitized)
|
|
-> /public-training/api/:shareToken
|
|
-> personal mobile profile
|
|
```
|
|
|
|
Important invariants:
|
|
|
|
- all displayed pit-stop counts come from completed `pitStops`, never unmatched `pitEvents`
|
|
- the pit log is available for every session type, not only `team_race`
|
|
- an optional split/pit channel never changes race laps, lap ranking, or finish timing
|
|
- old stored pit stops without entry-attribution fields remain readable via competitor fallback labels
|
|
- reports must use the same builders as live views to avoid differing totals
|
|
|
|
### 8.3.4 Managed AMMC process lifecycle
|
|
|
|
`server.js` owns four process states in `ammcState.channels`: `main`, `split`, `pitEntry`, and `pitExit`. `startAmmcProcesses()` normalizes and validates the complete configuration before spawning one child process per enabled role. All roles use the same resolved AMMC executable and working directory but receive different `-w <port> <decoderHost>` arguments.
|
|
|
|
The AMMC status API remains backward compatible: top-level status fields represent `main`; nested `split`, `pitEntry`, and `pitExit` objects expose optional process status. Combined `lastOutput` records include a `role` field. `stopAmmcProcesses()` stops all roles during an API stop request or server shutdown.
|
|
|
|
Configuration validation rejects:
|
|
|
|
- ports outside `1..65535`
|
|
- duplicate ports among enabled channels
|
|
- an enabled role without its decoder host when AMMC is started
|
|
- missing AMMC executable
|
|
|
|
Changing backend AMMC process code requires `npm restart` or `npm.cmd restart`. Editing browser modules alone is handled by app-version polling, but a server process cannot hot-reload its own implementation.
|
|
|
|
## 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()`
|
|
- backward-compatible main status plus nested split/pit channel statuses
|
|
- `startAmmcProcesses()` / `stopAmmcProcesses()`
|
|
- lifecycle for every enabled decoder role
|
|
- `startAmmcProcess(role)` / `stopAmmcProcess(role)`
|
|
- lifecycle for one AMMC child process
|
|
- 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
|
|
profileId: string, // shared personal practice-profile identity; race identity remains `id`
|
|
club: string, // personal profile metadata, not class performance data
|
|
country: string // personal profile metadata, not class performance data
|
|
}
|
|
```
|
|
|
|
Normalized by:
|
|
- `normalizeDriver()` in `src/app.js`
|
|
|
|
Used by:
|
|
- `state.drivers`
|
|
- race participant selection
|
|
- team selection
|
|
- decoder passing resolution
|
|
- exports/imports
|
|
|
|
Profile rules:
|
|
- `src/driver_profile_logic.js` resolves `profileId`, groups linked driver records, and returns all references belonging to one person
|
|
- a driver without an explicit `profileId` receives a legacy grouping key based on its normalized name
|
|
- creating another same-name driver links to that profile by default; the Drivers editor can link to another profile or create a separate one
|
|
- `driverId`, `classId`, and transponder remain independent per race entry
|
|
- `club` and `country` are inherited and synchronized across explicitly linked entries by the Drivers UI
|
|
- practice goals and public share tokens belong to the personal profile, while measured performance belongs to a selected class
|
|
|
|
## 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,
|
|
classId: string, // class whose lap total advances the goal
|
|
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 personal profile
|
|
- only one normalized share entry is retained per personal profile
|
|
- 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.
|
|
|
|
`TrainingLap.driverId` always records the exact race/decoder driver entry that produced the lap. At read time, `src/driver_profile_logic.js` resolves all driver IDs belonging to the selected personal profile, then `src/training_logic.js` narrows them to the selected `classId`. This permits several transponders in one class while preventing incomparable classes from being mixed.
|
|
|
|
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 person's display name plus linked class names and brands
|
|
- one `classProfiles` entry per linked class; no cross-class performance aggregate is emitted
|
|
- class-specific numeric club rank without other driver names
|
|
- class-specific totals, best/average time, consistency, controlled streak, stints, best-three, five-minute result, and daily summaries
|
|
- optional class-specific `currentSession` aggregates when a valid start age (or backward-compatible recent `since` timestamp) is supplied
|
|
- class-specific next automatic milestone, recommendation level, target lap, goals, and reward progress
|
|
- public club branding and safe PNG/JPEG/WebP logo data
|
|
|
|
Explicitly excluded:
|
|
- transponder IDs
|
|
- club and country profile metadata
|
|
- 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
|
|
- personal-profile isolation, same-class multi-transponder aggregation, and 2WD/4WD separation
|
|
- recommendation branch selection
|
|
- custom goal and automatic milestone progress
|
|
- selected-profile/class 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
|
|
- personal-profile isolation, class-specific statistics, 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
|