Files
Live_RC/docs/javascript-architecture.md
2026-03-30 11:52:13 +02:00

1784 lines
39 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`
Scripts:
- `npm start` -> `node scripts/serverctl.js start`
- `npm run start:fg` -> `node server.js`
- `npm stop`
- `npm run status`
- `npm restart`
### `server.js`
Role:
- backend runtime
Uses:
- `fs`
- `path`
- `os`
- `child_process.spawn`
- `express`
- `better-sqlite3`
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
- PDF export endpoint
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`
Static routes of interest:
- `/`
- `/public-overlay`
- `/public-overlay/obs`
### `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`
## 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
- 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
## 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/event_workspace_controller.js` imports
- `src/event_views.js`
### Backend/root imports
- `server.js` imports Node core + `express` + `better-sqlite3`
- `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.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 |
| `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(...)`
- `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(...)`
- `announceRaceFinishedHelper(...)`
- `speakTextHelper(...)`
### 14.6 `server.js`
#### Backend API and persistence
- `initSchema()`
- initializes SQLite schema
- `watchAppFiles()`
- bumps app version on 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>,
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
}
```
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[]
}
```
### `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 `Settings`
Practical shape contains at least:
- theme
- language
- backendUrl
- decoderUrl / websocket URL
- speaker/audio flags
- 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
- otherwise returns `403 Forbidden`
- on success serves `index.html`
Use for:
- public OBS/browser-source overlays without exposing admin path directly
## 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. Recommended Future Additions
If the project grows further, the next useful additions to this document are:
- exact `resultsBySession` competitor row schema
- exact `settings` schema snapshot
- SQLite table contract section
- controller-to-view ID map for critical forms/buttons
- test checklist per module