# RC RaceCore 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 - bounded SQLite backup API and local-only restore CLI 3. Local persistence - SQLite via `better-sqlite3` - main file: `data/rc_timing.sqlite` - AMMC config JSON: `data/ammc_config.json` - rotating SQLite snapshots: `data/backups/*.sqlite` plus metadata sidecars 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 Browser -->|GET/POST /api/backups| Server Server -->|persist app state| SQLite[(SQLite)] Server -->|persist passings| SQLite Server -->|online backup API| Backups[(data/backups)] 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 - SQLite online backup creation and bounded retention through `server/backup_service.js` ## 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 backup:restore -- --confirm` - `npm run setup:windows` ### `server.js` Role: - backend runtime Uses: - `fs` - `path` - `os` - `child_process.spawn` - `express` - `better-sqlite3` - `server/public_training_profile.js` - `server/backup_service.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, locally generated QR SVG, configurable public base URL, 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/network/public-address` (explicit UI action only; never polled) - `/api/backups` (`GET` list, `POST` create only) - `/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 - `RC_ALLOWED_ORIGINS`: optional comma-separated cross-origin UI origins; same-origin is always allowed ### `server/backup_service.js` Role: - server-side SQLite snapshot service Owns: - `data/backups` creation - online `better-sqlite3.backup(...)` snapshots that remain consistent while WAL mode is active - sanitized backup names and JSON metadata sidecars - newest-first backup listing - bounded retention of the latest 30 snapshots Security boundary: - the HTTP API can create and list metadata but cannot download or restore a database - destructive restore is available only through `scripts/restore-backup.js` on the backend host - the restore CLI requires the managed server to be stopped and an explicit `--confirm` flag - before replacement, restore validates SQLite/application state and creates a `pre_restore` safety snapshot ### `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 plus class-specific goals and recommendation response - server-side visibility filtering for progress, achievements, performance/stints, goals, and history - bounded class-specific pace history, active 30-minute pace buckets, and recent stint summaries for the mobile profile - token-, profile-, and class-validated creation of public car/setup markers - 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` ### `server/auth.js` Role: - optional admin PIN protection (CommonJS, required by `server.js`) Owns: - PIN hashing/verification (scrypt) in `data/auth.json` and the per-installation HMAC secret in `data/auth_secret` - both outside `app_state` on purpose - signed 30-day login cookie (`rc_auth`), invalidated when the PIN changes - login throttling per client address - `middleware`: once a PIN exists, rejects unauthenticated non-GET `/api/*` requests and `GET /api/ammc/config` with `401 { error: "auth_required" }`; applies `RC_ADMIN_ALLOWED_CIDRS` (403 `ip_not_allowed`) to the same routes regardless of PIN - routes wired in `server.js`: `GET /api/auth/status`, `POST /api/auth/login`, `/logout`, `/pin`, `/disable` ### `server/tts.js`, `server/tts_install.js`, `scripts/install-piper.js` Role: - local neural text-to-speech via Piper (CommonJS, required by `server.js`) Owns: - voice catalog in `tts_install.js`: one male + one female voice per language with gender/label (genders measured by pitch, not guessed); `pickVoice` prefers the voice chosen in Settings, then the catalog default - `tts.js`: finds the Piper binary (`RC_PIPER_BIN`, then `/piper/`, then PATH, then `~/.local/bin`) and voices (`RC_PIPER_VOICES_DIR`, `data/piper-voices`, `/piper/voices`, `~/.local/share/piper-voices`); generates one line at a time into a WAV cache under `data/tts/` keyed by voice + speed + text; dedupes identical in-flight requests; background pre-generation with progress - `tts_install.js`: downloads the standalone Piper release for the OS and the standard voices (NST, Lisa, Ryan, Lessac) into `/piper/`, skipping anything present. `extra` catalog voices (Alma, Alan, Northern English, Cori, Alba, Joe, John, Norman) are only fetched one at a time via `POST /api/tts/install { voice }`; `tts.status()` lists them as `downloadable` for the Settings panel. The catalog is used by both `npm run tts:install` (`scripts/install-piper.js`) and `POST /api/tts/install` - routes in `server.js`: `GET /api/tts/status` (open), `POST /api/tts/speech`, `POST /api/tts/pregenerate`, `POST /api/tts/install` (all PIN-protected once a PIN is set) ### `scripts/record-video-guides.mjs` and `scripts/capture-screenshots.mjs` Role: - documentation tooling - not run in production, not imported by `server.js` or any `src/*.js` module Owns: - both spin up an isolated `server.js` (temp `RC_TIMING_DATA_DIR`) plus a headless Chrome instance driven over the DevTools Protocol via a raw `WebSocket`, the same harness pattern `tests/browser_smoke.js` uses - own temp SQLite dir, own browser profile, never touches a real installation - `record-video-guides.mjs`: narrated MKV screen-capture clips for new-user video guides (`docs/videos/*.mkv`, linked from `README.md`/`README.sv.md` and the in-app Guide page). Each clip is a list of `{ text, action }` steps; narration audio is synthesized up front via Piper (`pipx install piper-tts` plus a downloaded voice model - not espeak-ng, which was tried first and found barely intelligible), so each line's real spoken duration is known before recording, and the final audio track is built from the actual measured wall-clock offset each line started at (`ffmpeg`'s `adelay`/`amix`), not an idealized schedule - avoids drifting out of sync with page-load/network latency over a 30+ second clip. A `prepare()` hook lets a clip spin up a live dependency (e.g. a fake decoder socket via `tests/browser_fake_decoder.js`) before the app is seeded. - `capture-screenshots.mjs`: single-frame JPEG captures (`docs/screenshots/*.jpg`, 1600x900 CSS at `deviceScaleFactor: 2`) against one shared seeded demo dataset ("JMK RB" branding, a finished "Klubbmästare Race 4" event) so screenshots taken at different times stay visually consistent with each other. ### 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[Platform AMMC runtime check] AMMC --> Glibc[Linux x64 GLIBC >= 2.39 warning] AMMC --> Sim[ammc-sim fake decoder test source] 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` - all bundled Linux x86-64 AMMC programs require GLIBC 2.39+; `verify-install.js` checks every program's executable permission and reports host compatibility without blocking the RC RaceCore server - `ammc-sim` emulates a physical decoder for test passings; its output must still pass through `ammc-amb` and the normal WebSocket, validation, timing, and persistence flow - `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/race_day_logic.js` Role: - pure Race Day queue and preflight calculations Owns: - current/next/after-next ordering within one event - blocking checks for Main decoder, participants, transponders, duplicates, lap window, and position grid - warnings for backend backup availability, optional decoder roles, and audio ### `src/race_day_view.js` Role: - focused Race Day operator UI Dependencies injected by `app.js`: - existing timing/session selectors and formatters - `buildRaceDayQueue` and `buildRaceDayPreflight` - centralized start, finish, pause, and resume controllers - navigation shortcuts to Timing, Judging, and Overlay The view does not own timing state and does not implement a second race clock. ### `src/session_control.js` Role: - small pure mutation helpers for begin, finish, pause, and resume The same helpers are used by the centralized operational start path so Race Day and Timing cannot drift into separate lifecycle behavior. Safety backup contract: - session start synchronizes state and creates a `pre_start` snapshot before the existing audio/start sequence - full data import and every clear-data scope synchronize state and create a `pre_change` snapshot before mutation - if backup is unavailable, the operator must explicitly confirm before the existing operation continues ### `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 - `buildEventSchedule(event, sessions, { getSessionSortWeight })`: cascades each race-mode session's planned clock time from `event.raceConfig.scheduleStartAt`, in running order (reusing `getSessionSortWeight`'s practice→qualifying→finals ordering from `race_render_helpers.js`), each session's own duration/follow-up plus `scheduleGapMin` between sessions. Once a session has actually started/finished, everything after it re-anchors to that real time - a late start or an overrun (a pause's wall-clock time is already baked into a session's real `endedAt`) pushes the rest of the day forward automatically, with no separate "reschedule" step. Returns `[]` when no `scheduleStartAt` is set. Consumed by Event Manager's Schedule tab and by `buildRaceStartListsHtml` (prints the planned time next to each session). ### `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: - for a race-mode event: a 4-tab layout (`.manage-tabs`: Setup/Participants & teams, Format, Live & results, Schedule & print) wrapping the panels below in `
` groups - session-plan (create/list sessions) and branding stay always visible above the tabs regardless of the active one, as does the permanent 4-card step-status summary (Setup/Format/Generate/Live) - session forms - race format panel (Format tab) - branding panel (always visible) - team section (Setup tab) - sponsor tools - assignments section - Schedule panel (Schedule tab): start-time + gap-between-sessions form, and (once set) a table of every session's planned/actual time and a "Delayed" flag, built from `buildEventSchedule` - summary/actions panels - team/session modals - track/sponsor (non-race-mode) events skip the tabs entirely and keep the simple, untabbed layout - they never had enough content to need them ### `src/event_manager_controller.js` Type: - `Hantera` controller Owns: - `activeManageTab` (module-scoped, same pattern as `settings.js`'s `activeSettingsTab`) and `MANAGE_TAB_TARGETS`, a lookup from a quick-jump target id (the step-status cards, the race-summary's warning links) to the tab it now lives inside, so jumping to it switches tabs first - `scrollIntoView` on an element behind an inactive tab does nothing on its own - 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 (opening a session's grid editor also switches to the Live tab, since that's where the editor renders) - schedule form save/clear (`scheduleForm`/`scheduleClear`) ### `src/overlays.js` Type: - overlay renderers Owns: - normal overlay - team overlay - OBS/public overlay - side panels - overlay leaderboard rows ### `src/session_replay.js` Type: - pure transient replay logic Owns: - normalization and chronological sorting of saved session passings - deterministic play, pause, speed, finish, and reset state - time-sliced replay leaderboard snapshots for race and practice ranking - strict isolation from `state.resultsBySession`, SQLite, decoder ingestion, and training ledger mutations Imported by: - `src/app.js` for the single transient replay runtime - `src/timing_views.js` for controls and incremental rendering - `tests/session_replay.test.js` ### `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/track_layouts_logic.js` Type: - pure track-layout identity/lookup helpers Owns: - normalizing a single layout (`{ id, label, startedAt }`) and the installation's whole layout list, sorted by `startedAt` - resolving which layout was active at a given timestamp (`getTrackLayoutIdAt`) - layouts are contiguous, so a layout's implicit end is the next one's `startedAt`, with no separate stop field - default/next label generation (`getTrackLayoutLabel`, `nextTrackLayoutLabel` - "Layout N") Imported by: - `src/app.js` to stamp `session.trackLayoutId` at the moment a session starts (never recomputed later, so editing the layout list afterwards can't retroactively reclassify old results) and to scope `getTrackRecordMs` (`src/timing_logic.js`) and training pace/consistency stats (`src/training_logic.js`, `server/public_training_profile.js`) to the current layout - `src/settings.js` for the "Banlayouter" panel (add/list/delete, delete blocked while any session still references the layout) - `src/results_archive_view.js` to show which layout a completed session ran on ### `src/auth_view.js` Type: - admin PIN UI Owns: - `installAuthFetchGuard`: wraps `window.fetch` once at startup (`app.js`) so a `401 auth_required` from `/api/*` opens the PIN dialog; `app.js` retries the pending sync after a successful login - `showPinDialog`: the login modal - `mountAuthSettings`: the "Admin PIN" panel in Settings -> System & backup (enable, change, log out, disable) ### `src/piper_speech.js` and `src/tts_settings_view.js` Type: - local voice playback and its settings panel Owns: - `piper_speech.js`: `speakWithPiper` fetches the server's WAV and plays it; on any error or after 2.5 s it calls the browser-voice fallback instead. `speakTextHelper` (`runtime_services.js`) routes through it when `settings.ttsEngine === "piper"`, so every race-audio voice uses it - `tts_settings_view.js`: the "Voice engine" panel in Settings -> Audio (engine choice, sv/en install status, install / pre-generate / test buttons) ### `src/track_layout_bar.js` Type: - shared UI strip Owns: - "Track layout: