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:
- Browser UI
index.htmlsrc/styles.csssrc/app.js- the rest of
src/*.js
- Node backend
server.js- Express API
- static file serving
- AMMC process control
- PDF export endpoint
- app version endpoint
- Local persistence
- SQLite via
better-sqlite3 - main file:
data/rc_timing.sqlite - AMMC config JSON:
data/ammc_config.json
- 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:
expressbetter-sqlite3
Scripts:
npm start->node scripts/serverctl.js startnpm run start:fg->node server.jsnpm stopnpm run statusnpm restart
server.js
Role:
- backend runtime
Uses:
fspathoschild_process.spawnexpressbetter-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.pidlogs/server.out.loglogs/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:
Hanteramarkup
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:
Hanteracontroller
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.jssrc/settings.jssrc/race_setup_ui.jssrc/event_race_logic.jssrc/timing_logic.jssrc/core_views.jssrc/misc_views.jssrc/event_common.jssrc/event_workspace_controller.jssrc/event_manager_view.jssrc/event_manager_controller.jssrc/runtime_services.jssrc/decoder_runtime.jssrc/timing_views.jssrc/race_render_helpers.jssrc/judging_logic.js
src/event_workspace_controller.js imports
src/event_views.js
Backend/root imports
server.jsimports Node core +express+better-sqlite3scripts/serverctl.jsimports 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
normalizeRaceTeamnormalizeStoredRacePresetgetRaceFormatPresetsapplyRaceFormatPresetbuildRaceFormatConfigFromFormnormalizeBrandingConfignormalizeEventresolveEventBrandingbuildDefaultRaceWizardDraftgetRaceWizardPresetapplyRaceWizardPresetDefaultsensureRaceParticipantsConfiguredbuildRaceSessionbuildTrackSessioncreateSponsorRoundsbuildRaceSessionsFromWizardgetRaceWizardSessionPlangetEventDriversgetEventTeamsgetTeamDriverPoolfindEventTeamForPassinggenerateQualifyingForRacereseedUpcomingQualifyinggenerateFinalsForRaceapplyBumpsForRace
4.2 Timing wrappers
getSessionTypeLabelgetStatusLabelisUntimedSessiongetActiveSessiongetSessionTargetMsgetSessionLapWindowisCountedPassinggetVisiblePassingsgetPassingValidationLabelgetSessionTimingensureSessionResultformatLapDeltaformatLeaderboardGapgetCompetitorElapsedMsgetCompetitorPassingsgetCompetitorSeedMetricgetSessionEntrantsbuildPracticeStandingsgetQualifyingPointsValueisHighPointsTablecompareNumberSetbuildQualifyingTieBreakNotehasQualifyingPrimaryTiebuildQualifyingStandingsformatTeamActiveMemberLabelbuildTeamRaceStandingsbuildTeamStintLoggetSessionGridEntriesgetSessionGridOrderensureSessionDriverOrderbuildFinalStandings
4.3 Judging wrappers
getManualCorrectionSummaryapplyCompetitorCorrectionrecalculateCompetitorFromPassingsinvalidateCompetitorLastLaprestoreCompetitorLastInvalidLapfindPassingByUndoMarkerundoJudgingAdjustmentgetJudgeFilteredRowsgetJudgeFilteredLog
4.4 Event/session shared wrappers
getSessionsForEventgetModeLabelnormalizeStartModegetStartModeLabelgetClassNamegetEventNamerenderAssignmentListrenderSessionsTable
4.5 Runtime wrappers
createDefaultAmmcConfiggetManagedWsUrlloadAmmcConfigFromBackendsaveAmmcConfigToBackendrefreshAmmcStatusstartManagedAmmcstopManagedAmmcapplyPersistedStatehydrateFromBackendscheduleBackendSyncsyncStateToBackendpingBackendcheckAppVersionstartAppVersionPollingstartOverlaySyncstartOverlayRotationstartOverlayLiveRefreshensureAudioContextplayPassingBeepplayFinishSirenplayLeaderCueplayStartCueplayBestLapCuepushOverlayEventspeakTextannouncePassingannounceRaceFinishedhandleSessionTimerTicktickClock
4.6 Decoder wrappers
connectDecoderdisconnectDecoderprocessDecoderMessage
5. State Model
Global state owner:
src/app.js
Primary state buckets:
state.eventsstate.sessionsstate.driversstate.carsstate.classesstate.resultsBySessionstate.settingsstate.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
eventreference - 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.jssrc/event_workspace_controller.js
7. Screen Ownership
Overview
Render chain:
app.js->renderView()->renderDashboardView()insrc/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()fromevent_views.js
Event / Race Setup manager (Hantera)
Render chain:
app.js->renderEventManager(eventId)event_manager_controller.js->renderEventManagerMarkup()fromevent_manager_view.js
Timing / Judging
Render chain:
app.js->renderView()->renderTimingView()/renderJudgingView()fromtiming_views.js
Overlay admin page
Render chain:
app.js->renderOverlay()->renderOverlayPageView()frommisc_views.js
Public overlays
Render chain:
app.js->renderOverlay()-> helpers inoverlays.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.jssrc/event_race_logic.jssrc/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.jssrc/event_manager_controller.jssrc/event_manager_view.js
Decoder online but no laps
Open first:
src/decoder_runtime.jssrc/timing_logic.jssrc/runtime_services.js
Public overlay works differently from admin overlay
Open first:
src/overlays.jssrc/misc_views.jssrc/runtime_services.jsserver.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.eventsandstate.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.jswiring - 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.eventsupdate path
- canonical
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/statePOST /api/statePOST /api/passingsGET/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()insrc/app.js
15.3 Driver
{
id: string,
name: string,
classId: string,
transponder: string,
brand: string
}
Normalized by:
normalizeDriver()insrc/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()insrc/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()insrc/event_race_logic.js
BrandingConfig
{
brandName: string,
brandTagline: string,
pdfFooter: string,
pdfTheme: "" | "classic" | "minimal" | "motorsport",
logoDataUrl: string
}
Normalized by:
normalizeBrandingConfig()insrc/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()insrc/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()insrc/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
sessionResultis provided, backend also hot-updatesapp_state.resultsBySession[sessionId] - this is important for overlays hydrating from backend instead of direct decoder websocket
16.6 GET /api/passings
Query parameters:
sessionIdoptionallimitoptional, max1000
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_TOKENis set, query paramtokenmust 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_SETTINGSbefore initializationOBS_LAYOUTSbefore initializationformatLap is not definedformatPredictedLapDelta is not a functiongetPassingValidationLabeldependency missing
Pattern:
- helper/module was extracted or wrapper changed
- consumer module signature expected injected helper
app.jsno 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
- imported function in
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
eventobject mutated - canonical
state.eventswas 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 practiceinstead of selected type - team save buttons did nothing
- team save worked for create but not edit
- race format save reverted min/max values
Pattern:
Hanteramixes many forms, rerenders, and local references in one controller
Mitigation:
- treat
src/event_manager_controller.jsas 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.jstogether with the affected save path
18. Recommended Future Additions
If the project grows further, the next useful additions to this document are:
- exact
resultsBySessioncompetitor row schema - exact
settingsschema snapshot - SQLite table contract section
- controller-to-view ID map for critical forms/buttons
- test checklist per module