# 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 `/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 decoder channels `9000-9003` 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=` selects a personal profile and `class=` selects its performance class for bookmarks - an explicitly enabled personal profile receives `/public-training/` 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 Operator-facing installation and IP configuration is documented in `docs/decoder-setup.md` and `docs/decoder-setup.sv.md`. This architecture section defines code ownership and invariants; the setup guides define deployment values and verification order. 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 ` 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, 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