Add class-specific driver training profiles and metadata

This commit is contained in:
larssand
2026-08-24 22:05:50 +02:00
parent 3aeb77a042
commit 6f02e955a5
14 changed files with 559 additions and 128 deletions

View File

@@ -160,18 +160,19 @@ Runtime environment variables:
### `server/public_training_profile.js`
Role:
- pure CommonJS backend helper for tokenized driver profiles
- pure CommonJS backend helper for tokenized personal training profiles
Owns:
- enabled share-token lookup
- selected-driver lap filtering and aggregate statistics
- sanitized branding, class, goal, rank, and recommendation response
- profile resolution across several race-specific driver records
- strict per-class lap filtering and aggregate statistics
- sanitized branding, class-specific goal, rank, and recommendation response
- clock-skew-safe personal timer start/window normalization
- bounded current-session lap, best-lap, and average calculations
Security boundary:
- returns no driver IDs, transponders, raw passings, decoder settings, or other driver names
- disabled, short, unknown, or driverless tokens return no profile
- disabled, short, unknown, or profileless tokens return no profile
- logo data is restricted to bounded PNG/JPEG/WebP data URLs
### `scripts/serverctl.js`
@@ -406,6 +407,20 @@ Owns:
- Drivers
- Cars
### `src/driver_profile_logic.js`
Type:
- pure personal-profile identity helpers
Owns:
- resolving an explicit `profileId` or a legacy normalized-name profile key
- grouping race-specific driver records that belong to one person
- listing all driver IDs and class IDs attached to one personal profile
Imported by:
- `src/core_views.js` for profile assignment in the Drivers UI
- `src/training_logic.js` for class-scoped lap aggregation
- `src/training_views.js` for person and class selectors
### `src/misc_views.js`
Type:
- misc page renderers
@@ -431,9 +446,9 @@ Type:
Owns:
- training-state normalization
- durable ledger synchronization from `resultsBySession`
- valid driver-lap aggregation across the training ledger
- valid driver-lap aggregation across the training ledger, scoped by personal profile and class
- lifetime/session/day/best-lap/average/consistency calculations
- active Open/Free/regular Practice statistics scoped to one driver and the current session start
- active Open/Free/regular Practice statistics scoped to one personal profile, one class, and the current session start
- stint splitting after passing gaps greater than `120000 ms`
- best N consecutive laps and fixed-duration result calculation
- five-minute (`300000 ms`) results include the final lap when its start is before the cutoff and expose average pace for the counted laps
@@ -443,6 +458,10 @@ Owns:
Rules:
- only passings with a matching `driverId` are included
- `profileId` links multiple race-specific driver records to one person without changing decoder or race identity
- statistics combine linked driver records only when their `classId` matches the selected class
- 2WD and 4WD lap times, stints, best-three, five-minute results, and recommendations are never combined
- legacy driver records without `profileId` are grouped by normalized driver name until explicitly linked or separated in the Drivers UI
- invalid laps and lap times at or below `500 ms` are excluded
- anonymous Open Practice transponders are excluded until registered as drivers
- consistency is the P10-to-P90 spread of the latest 30 valid laps divided by their mean
@@ -474,8 +493,8 @@ Mutations:
Route behavior:
- `/?view=training` opens the training view directly
- `driver=<driverId>` selects a driver and supports mobile bookmarks
- an explicitly enabled driver receives `/public-training/<shareToken>` as a separate read-only profile
- `driver=<profileId>` selects a personal profile and `class=<classId>` selects its performance class for bookmarks
- an explicitly enabled personal profile receives `/public-training/<shareToken>` as a separate read-only profile with a class selector
- replacing or disabling the share invalidates its old route immediately after state sync
### `public-training.html`
@@ -494,7 +513,8 @@ Type:
Owns:
- fetching only `/public-training/api/:shareToken`
- rendering one driver's sanitized statistics, goals, rewards, and daily history
- rendering one person's sanitized statistics, goals, rewards, and daily history with a class selector
- switching between class-specific statistics without combining lap performance across classes
- local 20-minute practice timer with 10-minute push/control phases
- token-scoped start-age and bounded-window polling for aggregate lap count, best lap, and average during the personal timer; using an age avoids mobile/server clock-skew errors and the bounded window freezes results after the timer finishes
- Swedish/English copy selected by `lang` query or browser locale
@@ -503,7 +523,7 @@ Important isolation:
- does not import `app.js`
- does not call `/api/state`
- does not mutate backend or race state
- timer state is stored in the mobile browser under a token-scoped localStorage key
- timer state is stored in the mobile browser under a token-and-class-scoped localStorage key
- session-window responses contain aggregates only; individual lap rows and timestamps are not exposed
### `src/public_training.css`
@@ -538,6 +558,11 @@ Owns:
- `src/training_logic.js`
- `src/training_views.js`
### Training/profile imports
- `src/core_views.js` imports `src/driver_profile_logic.js`
- `src/training_logic.js` imports `src/driver_profile_logic.js`
- `src/training_views.js` imports `src/driver_profile_logic.js`
### `src/event_workspace_controller.js` imports
- `src/event_views.js`
@@ -1339,7 +1364,10 @@ Produced/normalized by:
classId: string,
transponder: string,
brand: string,
speechName: string // optional phonetic form used only by browser speech
speechName: string, // optional phonetic form used only by browser speech
profileId: string, // shared personal practice-profile identity; race identity remains `id`
club: string, // personal profile metadata, not class performance data
country: string // personal profile metadata, not class performance data
}
```
@@ -1353,6 +1381,14 @@ Used by:
- decoder passing resolution
- exports/imports
Profile rules:
- `src/driver_profile_logic.js` resolves `profileId`, groups linked driver records, and returns all references belonging to one person
- a driver without an explicit `profileId` receives a legacy grouping key based on its normalized name
- creating another same-name driver links to that profile by default; the Drivers editor can link to another profile or create a separate one
- `driverId`, `classId`, and transponder remain independent per race entry
- `club` and `country` are inherited and synchronized across explicitly linked entries by the Drivers UI
- practice goals and public share tokens belong to the personal profile, while measured performance belongs to a selected class
## 15.4 `Car`
```js
@@ -1548,6 +1584,7 @@ This bucket is the most dynamic part of state and is mutated heavily during live
{
id: string,
driverId: string,
classId: string, // class whose lap total advances the goal
title: string,
reward: string,
targetLaps: number,
@@ -1568,8 +1605,8 @@ This bucket is the most dynamic part of state and is mutated heavily during live
```
Share rules:
- no share exists until an admin explicitly enables the selected driver profile
- only one normalized share entry is retained per driver
- no share exists until an admin explicitly enables the selected personal profile
- only one normalized share entry is retained per personal profile
- disabled shares fail public route lookup
- rotating a share replaces its token; the previous URL no longer resolves after persistence
- tokens are generated in the browser with `crypto.getRandomValues()`, never from the shorter UI `uid()` helper
@@ -1604,6 +1641,8 @@ Persisted through:
Derived statistics are intentionally not stored. They are recalculated from `state.training.ledger`. Before persistence, `saveState()` synchronizes every session currently present in `resultsBySession`: existing rows for those session IDs are replaced from current valid passings, so judging corrections are reflected. Session IDs absent from `resultsBySession` are not removed from the ledger, preserving lifetime totals after archival or deletion.
`TrainingLap.driverId` always records the exact race/decoder driver entry that produced the lap. At read time, `src/driver_profile_logic.js` resolves all driver IDs belonging to the selected personal profile, then `src/training_logic.js` narrows them to the selected `classId`. This permits several transponders in one class while preventing incomparable classes from being mixed.
Migration behavior:
- older state without `training.ledger` normalizes to an empty ledger
- the next `saveState()` imports valid registered-driver laps from all current session results
@@ -1950,17 +1989,17 @@ Authorization:
- missing, disabled, replaced, or unknown tokens return `404` rather than revealing whether a driver exists
Sanitized response fields:
- selected driver display name, class, and brand
- numeric club rank without other driver names
- aggregate totals, best/average time, consistency, controlled streak, and daily summaries
- optional `currentSession` aggregates when a valid start age (or backward-compatible recent `since` timestamp) is supplied
- next automatic milestone
- recommendation level and target lap
- selected driver's goal titles, reward text, progress, and claimed state
- selected person's display name plus linked class names and brands
- one `classProfiles` entry per linked class; no cross-class performance aggregate is emitted
- class-specific numeric club rank without other driver names
- class-specific totals, best/average time, consistency, controlled streak, stints, best-three, five-minute result, and daily summaries
- optional class-specific `currentSession` aggregates when a valid start age (or backward-compatible recent `since` timestamp) is supplied
- class-specific next automatic milestone, recommendation level, target lap, goals, and reward progress
- public club branding and safe PNG/JPEG/WebP logo data
Explicitly excluded:
- transponder IDs
- club and country profile metadata
- driver IDs and names of other drivers
- cars, events, sessions, assignments, and raw passings
- race settings and decoder/AMMC configuration
@@ -2088,16 +2127,16 @@ npm test
- invalid, anonymous, and sub-500 ms lap exclusion
- replacement of current-session ledger rows after corrections
- retention of archived/deleted-session history
- per-driver isolation and day/session aggregation
- personal-profile isolation, same-class multi-transponder aggregation, and 2WD/4WD separation
- recommendation branch selection
- custom goal and automatic milestone progress
- selected-driver active-practice isolation, current-start filtering, and non-practice rejection
- selected-profile/class active-practice isolation, current-start filtering, and non-practice rejection
- two-minute stint boundaries, cross-session isolation, and boundary-lap exclusion
- best-three consecutive calculation and five-minute final-lap/average-pace behavior
`tests/public_training_profile.test.js` covers:
- enabled/disabled/unknown share-token handling
- selected-driver isolation and response sanitization
- personal-profile isolation, class-specific statistics, and response sanitization
- personal timer aggregate and frozen-window calculations
- clock-skew-safe query normalization and bounds