Conversation
Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
Agent-Logs-Url: https://github.com/timderes/open-slalom/sessions/dd2c3c2d-e529-443c-a8bd-192dc717cb20 Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
…driver form message) Agent-Logs-Url: https://github.com/timderes/open-slalom/sessions/dd2c3c2d-e529-443c-a8bd-192dc717cb20 Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
…, session timer, event log, hotkey bar Agent-Logs-Url: https://github.com/timderes/open-slalom/sessions/879b3393-e263-4723-b7a3-c22e6281d4b6 Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
…, training view/list, driver history, PDF export, home page Agent-Logs-Url: https://github.com/timderes/open-slalom/sessions/666210ec-37f1-430f-9ddb-d383f3af57a6 Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
… in all time displays Agent-Logs-Url: https://github.com/timderes/open-slalom/sessions/666210ec-37f1-430f-9ddb-d383f3af57a6 Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
…SchemaError on orderBy Agent-Logs-Url: https://github.com/timderes/open-slalom/sessions/4ca03b7c-bd70-4e71-a8eb-cd4f718698a8 Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
createdAt on trainings table (Dexie v3 schema)There was a problem hiding this comment.
Pull request overview
This PR expands the renderer with end-to-end “Training” functionality (timing a session, persisting results, and viewing history) and updates the Dexie schema to support sorting trainings by createdAt without throwing a Dexie SchemaError.
Changes:
- Add Dexie
trainingstable support and a v3 schema that indexescreatedAtto enable.orderBy("createdAt"). - Introduce training session UI/pages (run session, list history, view details, print/PDF export) plus navigation + i18n strings (EN/DE).
- Add shared timing/ranking utilities with Vitest coverage, and a new stopwatch/timer dependency.
Reviewed changes
Copilot reviewed 28 out of 34 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| renderer/lib/db/database.ts | Adds trainings table and a v3 schema indexing createdAt. |
| renderer/utils/timing.ts | New timing/ranking formatting + helper functions for training UI. |
| renderer/utils/timing.test.ts | Unit tests covering the timing/ranking helpers. |
| renderer/utils/routes.tsx | Adds navigation entries for Training and Training History. |
| renderer/utils/constants.ts | Adds DEFAULT_STOPWATCH_INTERVAL for stopwatch refresh cadence. |
| renderer/hooks/useTrainingSession.ts | Core training session state machine (stopwatch, laps, faults, persistence, hotkeys). |
| renderer/hooks/useDriverForm.ts | Fixes a log message to reference “driver form”. |
| renderer/pages/[locale]/training/index.tsx | Training timing page integrating the session hook and UI components. |
| renderer/pages/[locale]/training/list.tsx | Lists saved trainings sorted by createdAt (most recent first). |
| renderer/pages/[locale]/training/view.tsx | Displays a saved training detail view by uuid query param. |
| renderer/pages/[locale]/index.tsx | Home page now shows recent trainings and quick actions. |
| renderer/pages/[locale]/driver/view.tsx | Fetches trainings and passes them into DriverDetail for history display. |
| renderer/pages/[locale]/driver/index.tsx | Uses explicit locale for birthdate formatting. |
| renderer/components/content/training/DriverQueue.tsx | UI for live starting order with personal bests/gaps. |
| renderer/components/content/training/FaultButtons.tsx | UI to record/undo cone/gate faults with hotkeys. |
| renderer/components/content/training/LapsTable.tsx | Current stint laps table with best-time highlighting. |
| renderer/components/content/training/StopwatchDisplay.tsx | Large stopwatch display with projected penalty time. |
| renderer/components/content/training/DriversDrawer.tsx | Drawer to select/reorder drivers for the session. |
| renderer/components/content/training/SettingsDrawer.tsx | Drawer to configure laps/mode/location/weather + hotkey hints. |
| renderer/components/content/training/SessionLog.tsx | Rankings + chronological event log with class filtering. |
| renderer/components/content/training/PrintModal.tsx | Print/PDF modal summarizing training results. |
| renderer/components/content/training/TrainingViewDetail.tsx | Full training results view (metadata, chart, rankings, per-driver laps). |
| renderer/components/content/driver/DriverDetail.tsx | Adds driver training history accordion and links to training view. |
| renderer/types/Training.d.ts | Adds global Training and DriverWithStints types. |
| renderer/types/TrainingWeather.d.ts | Adds global weather-related types used in training settings/view. |
| renderer/types/Stint.d.ts | Adds global Stint type. |
| renderer/types/Lap.d.ts | Adds global Lap type used by timing/session logic. |
| renderer/types/SessionEvent.d.ts | Adds global event-log types for session auditing. |
| renderer/public/locales/en/training.json | Adds EN translations for the training feature. |
| renderer/public/locales/de/training.json | Adds DE translations for the training feature. |
| renderer/public/locales/en/common.json | Adds navigation labels for training routes. |
| renderer/public/locales/de/common.json | Adds navigation labels for training routes. |
| package.json | Adds react-use-precision-timer dependency. |
| package-lock.json | Locks new timer dependency and transitive packages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this.version(2).stores({ | ||
| clubs: "&uuid", | ||
| drivers: "&uuid", | ||
| trainings: "&uuid", | ||
| }); | ||
|
|
||
| this.version(3).stores({ | ||
| clubs: "&uuid", | ||
| drivers: "&uuid", | ||
| // Added createdAt index so orderBy("createdAt") works in the training list/home pages | ||
| trainings: "&uuid, createdAt", | ||
| }); |
There was a problem hiding this comment.
PR title/description focus on indexing createdAt for trainings, but this PR also introduces a large training feature set (new pages, hooks, components, types, locales, and a new timer dependency). Please update the PR title/description to reflect the full scope or split into smaller PRs so the schema/index fix can be reviewed and released independently.
| export const getRankedDrivers = ( | ||
| drivers: DriverWithStints[], | ||
| ): DriverWithStints[] => { | ||
| const withTime = drivers.filter((d) => getDriverBestLap(d) !== null); | ||
| return [...withTime].sort((a, b) => { | ||
| // getDriverBestLap is guaranteed non-null here due to the filter above | ||
| return (getDriverBestLap(a) ?? Infinity) - (getDriverBestLap(b) ?? Infinity); | ||
| }); |
There was a problem hiding this comment.
getRankedDrivers calls getDriverBestLap multiple times per driver (in the filter and repeatedly in the sort comparator). This is both more expensive than needed and contradicts the comment about avoiding repeated calls. Consider computing each driver's best lap once (e.g., map to { driver, best }, sort by best, then return drivers) to keep complexity predictable.
| const advanceToNextDriver = () => { | ||
| const nextIndex = | ||
| (currentStint.currentDriverIndex + 1) % settings.values.drivers.length; | ||
|
|
||
| stopwatch.stop(); | ||
| setCurrentStint({ | ||
| currentDriverIndex: nextIndex, | ||
| currentLap: 1, | ||
| driver: settings.values.drivers[nextIndex], | ||
| laps: [], | ||
| time: 0, | ||
| pendingPylonFaults: 0, | ||
| pendingGateFaults: 0, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
advanceToNextDriver assumes settings.values.drivers.length > 0 and does modulo by that length. Because the N hotkey is always registered (even when there are no drivers), pressing N with an empty driver list will compute % 0 and corrupt state. Add an early return/guard in handleSkipDriver (and/or advanceToNextDriver) when there are no drivers.
| const settings = useForm<Training>({ | ||
| initialValues: { | ||
| lapsPerStint: 3, | ||
| drivers: [], | ||
| mode: "JKS", | ||
| uuid: getUUID(), | ||
| createdAt: Date.now(), | ||
| updatedAt: Date.now(), | ||
| }, | ||
| onValuesChange: () => { | ||
| settings.setFieldValue("updatedAt", Date.now()); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
useForm is configured with onValuesChange that calls settings.setFieldValue("updatedAt", ...). This is very likely to trigger onValuesChange again and create an update loop. Prefer updating updatedAt in the specific handlers that mutate the form (or use a form API option to update silently if available) rather than mutating the form from within onValuesChange.
| useEffect(() => { | ||
| if (savedTrainingUuid) { | ||
| database.trainings | ||
| .where("uuid") | ||
| .equals(savedTrainingUuid) | ||
| .first() | ||
| .then((tr) => { | ||
| setSavedTraining(tr ?? null); | ||
| printHandlers.open(); | ||
| clearSavedTrainingUuid(); | ||
| }) | ||
| .catch(() => {}); | ||
| } |
There was a problem hiding this comment.
The PDF-load effect swallows DB errors (catch(() => {})) and only clears savedTrainingUuid on success. If the DB query fails, the user gets no feedback and the state can remain stuck (no modal, UUID never cleared). Handle the error (at least log/show a notification) and clear savedTrainingUuid in a finally so the UI can recover.
| <NumberInput | ||
| label={t("training:settingsDrawer.lapsPerStint")} | ||
| description={t("training:settingsDrawer.lapsPerStintDescription")} | ||
| min={1} | ||
| max={20} | ||
| value={lapsPerStint} | ||
| onChange={(val) => onLapsPerStintChange(Number(val))} | ||
| /> |
There was a problem hiding this comment.
NumberInput for lapsPerStint does onChange={(val) => onLapsPerStintChange(Number(val))}. When the input is cleared, Mantine can emit an empty string; Number("") becomes 0, which violates the min={1} constraint and can break downstream logic that assumes lapsPerStint >= 1. Handle the empty-string case explicitly (e.g., ignore, or fall back to 1).
| const tabs: { value: string; label: string }[] = [ | ||
| { value: "overall", label: "Overall" }, | ||
| ]; | ||
|
|
||
| if (mode === "JKS" || mode === "SKS") { | ||
| const key = mode as "JKS" | "SKS"; | ||
| const classes = new Set<number>(); | ||
| for (const d of drivers) { | ||
| const cls = getDriverClass(d.birthDate)[key]; | ||
| if (cls !== undefined) classes.add(cls); | ||
| } | ||
| [...classes] | ||
| .sort((a, b) => a - b) | ||
| .forEach((c) => | ||
| tabs.push({ value: `${key}-${c}`, label: `${key} K${c}` }), | ||
| ); | ||
| } |
There was a problem hiding this comment.
The class filter tabs use hard-coded labels like "Overall" / "JKS K1" / etc. Since this UI is otherwise localized, these labels should come from i18n (or be built from translated pieces) so they render correctly in non-English locales.
| /** Builds the list of class-filter tabs for the current training mode. */ | ||
| const buildClassTabs = ( | ||
| drivers: DriverWithStints[], | ||
| mode: string, | ||
| ): { value: string; label: string }[] => { | ||
| const tabs: { value: string; label: string }[] = [ | ||
| { value: "overall", label: "Overall" }, | ||
| ]; | ||
|
|
||
| if (mode === "JKS" || mode === "SKS") { | ||
| const classKey = mode as "JKS" | "SKS"; | ||
| const classSet = new Set<number>(); | ||
|
|
||
| for (const d of drivers) { | ||
| const cls = getDriverClass(d.birthDate)[classKey]; | ||
| if (cls !== undefined) classSet.add(cls); | ||
| } | ||
|
|
||
| [...classSet] | ||
| .sort((a, b) => a - b) | ||
| .forEach((cls) => | ||
| tabs.push({ value: `${classKey}-${cls}`, label: `${classKey} K${cls}` }), | ||
| ); |
There was a problem hiding this comment.
The class filter tab labels are hard-coded (e.g., "Overall", "JKS K1"). To keep the session log fully localized, consider moving these labels to translations (or composing them from translated tokens) so they display correctly for de and other locales.
| const handleStopTraining = () => { | ||
| modals.openConfirmModal({ | ||
| title: t("training:modals.stopTraining.title"), | ||
| centered: true, | ||
| children: null, | ||
| labels: { | ||
| confirm: t("training:modals.stopTraining.confirm"), | ||
| cancel: t("common:back"), | ||
| }, | ||
| confirmProps: { color: "red" }, | ||
| onConfirm: () => { | ||
| sessionInterval.stop(); | ||
|
|
||
| const hasDriversWithStints = settings.values.drivers.some( |
There was a problem hiding this comment.
handleStopTraining stops the session interval but does not stop the running stopwatch. If the user stops training while the stopwatch is running, lapInterval will continue updating currentStint.time and the UI still behaves like the session is active. Consider stopping/resetting the stopwatch (and related intervals/state) when the training is confirmed to stop.
Clean up SessionLog.tsx by reordering and consolidating imports (move timing utils and useState), remove a redundant type assertion for classKey, drop the unused 'mode' parameter from filterDriversByClass, and remove the unused 'overallBest' prop from RankingsTable. These changes simplify types and eliminate unused variables.
In SettingsDrawer.tsx, replace verbose onChange handlers for the weather Selects with a concise call: setWeatherField('...', val ?? undefined). This removes unnecessary type assertions and multi-line null handling for the conditions and trackCondition fields, improving readability.
Add error handling when loading a saved training for PDF export: log the failure, show a user notification, and clear the saved training UUID. Add i18n strings for the PDF load error in English and German. Also reorganize and clean up imports in TrainingViewDetail and pages training index, and remove a redundant type assertion in the rankings logic.
Normalize spelling of "colour" to "color" across training components and reorder timing/import statements for consistent import ordering. Changes touch DriverQueue, LapsTable, and SessionLog, updating comments and moving timing util imports for clarity and consistency.
Correct the German localization for the key `gapInterval` in renderer/public/locales/de/training.json by changing "Interval" to the proper German spelling "Intervall" to fix a typo and improve translation accuracy.
Refine German training translations for clarity and consistency: change "lapsPerStintDescription" from "Anzahl der getimten Runden pro Fahrer und Stint" to "Anzahl der zu absolvierenden Runden pro Stint", and simplify the empty state message from "Noch keine Trainingssessions gespeichert." to "Noch keine Trainings gespeichert."
Introduce DEFAULT_TIME_PENALTIES for JKS and SKS and update penalty logic to be mode-aware. calculatePenaltyTime now accepts a mode ("JKS" | "SKS") and uses the constants to compute cone/gate penalties. Updated components (FaultButtons, StopwatchDisplay) and the training hook (useTrainingSession) to accept/pass mode when computing penalties; the hook also reads mode from settings when creating lap records. Tests were updated to use DEFAULT_TIME_PENALTIES and to assert JKS behavior. Minor import/order tidy-ups included.
Drop explicit variant="light" usage on Badge, Button, ActionIcon, ThemeIcon and other Mantine components to rely on default styling and simplify props. Also cleaned up import ordering for timing utilities in several files. Changes touch various training and index pages and components (DriverDetail, DriverQueue, FaultButtons, PrintModal, SessionLog, SettingsDrawer, StopwatchDisplay, TrainingViewDetail, index and training list pages) to tidy UI prop usage and formatting.
Replace IconCone/IconDoorEnter with IconTrafficCone/IconBarrierBlock from @tabler/icons-react, remove the uppercase section header text, and drop the leading '×' from pylon and gate badge counts for a cleaner UI.
Remove the session elapsed time block from StopwatchDisplay: drop the conditional rendering that showed sessionElapsed, remove sessionTime and sessionStarted props, and remove the now-unused formatSessionTime import. Simplifies the component by delegating/removing session elapsed UI.
This pull request introduces several new UI components and enhancements to the training and driver detail sections of the application, focusing on improving driver management, lap history, and fault tracking. It also adds a new dependency for timing utilities. The most important changes are grouped below:
New UI Components for Training Management:
DriversDrawercomponent for managing the driver starting list, including searching, adding/removing, and reordering drivers from the database. This provides a more efficient workflow for setting up training sessions.DriverQueuecomponent to display the starting order of drivers with live updates of personal and session-best lap times, using clear color conventions for best laps.FaultButtonscomponent to record and correct pylon (cone) and gate faults during laps, including hotkey support and real-time penalty calculation.Enhancements to Driver Detail View:
DriverDetailto display a detailed training history for each driver, including personal and session-best laps, lap-by-lap breakdowns, and direct navigation to training sessions. Training history is shown as an accordion with detailed tables per session. [1] [2]Dependency Updates:
react-use-precision-timerpackage topackage.jsonto support precise timing features, likely used in the new or updated components.