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

39 KiB

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
  1. Node backend
  • server.js
  • Express API
  • static file serving
  • AMMC process control
  • PDF export endpoint
  • app version endpoint
  1. Local persistence
  • SQLite via better-sqlite3
  • main file: data/rc_timing.sqlite
  • AMMC config JSON: data/ammc_config.json
  1. Decoder / AMMC runtime
  • AMMC process started by backend or manually elsewhere
  • browser decoder socket handled in frontend runtime

1.2 High-level diagram

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.

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:

state.events = state.events.map((item) =>
  item.id === eventId ? normalizeEvent({ ...item, raceConfig: nextConfig }) : item
);
saveState();
renderView();

Risky:

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

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

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

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

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

{
  classes: Class[],
  drivers: Driver[],
  cars: Car[],
  events: Event[],
  sessions: Session[],
  resultsBySession: Record<string, SessionResult>,
  activeSessionId: string,
  settings: Settings
}

15.2 Class

{
  id: string,
  name: string
}

Produced/normalized by:

  • normalizeImportedClass() in src/app.js

15.3 Driver

{
  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

{
  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:

{
  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

{
  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:

{
  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

{
  id: string,
  name: string,
  driverIds: string[],
  carIds: string[]
}

Normalized by:

  • normalizeRaceTeam() in src/event_race_logic.js

15.6 Session

Practical shape:

{
  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:

{
  [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:

{
  "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:

{
  "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:

{
  "state": { /* persisted frontend state */ },
  "updatedAt": "2026-03-30T...Z"
}

Empty response when no state exists:

{
  "state": null,
  "updatedAt": null
}

Error response:

{
  "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:

{
  "ok": true,
  "updatedAt": "2026-03-30T...Z"
}

Error response:

{
  "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:

{
  "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:

{
  "ok": true
}

Error response:

{
  "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:

{
  "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:

{
  "config": { /* normalized AMMC config */ },
  "status": { /* buildAmmcStatus() output */ }
}

16.8 POST /api/ammc/config

Request body:

  • config object to normalize and save

Success response:

{
  "ok": true,
  "config": { /* normalized config */ },
  "status": { /* buildAmmcStatus() output */ }
}

Error response:

{
  "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:

{
  "ok": true,
  "status": { /* buildAmmcStatus() */ }
}

Error response:

{
  "error": "...",
  "status": { /* buildAmmcStatus() */ }
}

16.11 POST /api/ammc/stop

Success response:

{
  "ok": true,
  "status": { /* buildAmmcStatus() */ }
}

Error response:

{
  "error": "...",
  "status": { /* buildAmmcStatus() */ }
}

16.12 POST /api/export/pdf

Request body:

{
  "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:

{
  "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

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