diff --git a/README.md b/README.md index f65163f..1147b92 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ English README. Swedish version: [`README.sv.md`](README.sv.md) JMK RB RaceController is an RC timing and race-control system with support for sponsor events with shared cars/transponders, AMMC WebSocket input, and local persistence through SQLite. +## Architecture + +- JavaScript module overview: `docs/javascript-architecture.md` +- Swedish operational guide: `README.sv.md` + ## Screenshots ### Overview diff --git a/README.sv.md b/README.sv.md index 6b32fe1..8a566b7 100644 --- a/README.sv.md +++ b/README.sv.md @@ -2,6 +2,11 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika heat/finaler), AMMC WebSocket och lokal SQLite-lagring på Windows. +## Arkitektur + +- JavaScript-modulöversikt: `docs/javascript-architecture.md` +- Primär engelsk README: `README.md` + ## Skärmbilder ### Översikt diff --git a/docs/javascript-architecture.md b/docs/javascript-architecture.md new file mode 100644 index 0000000..5df315d --- /dev/null +++ b/docs/javascript-architecture.md @@ -0,0 +1,1783 @@ +# 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, + 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