Skip to content

feat(timing): add timing page#34

Draft
Copilot wants to merge 23 commits intomainfrom
copilot/create-training-timing-page
Draft

feat(timing): add timing page#34
Copilot wants to merge 23 commits intomainfrom
copilot/create-training-timing-page

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

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:

  • Added DriversDrawer component 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.
  • Introduced DriverQueue component to display the starting order of drivers with live updates of personal and session-best lap times, using clear color conventions for best laps.
  • Implemented FaultButtons component to record and correct pylon (cone) and gate faults during laps, including hotkey support and real-time penalty calculation.

Enhancements to Driver Detail View:

  • Extended DriverDetail to 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:

  • Added the react-use-precision-timer package to package.json to support precise timing features, likely used in the new or updated components.

Copilot AI and others added 10 commits April 8, 2026 15:48
Co-authored-by: timderes <50623315+timderes@users.noreply.github.com>
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>
@timderes timderes marked this pull request as ready for review April 8, 2026 17:26
@timderes timderes requested a review from Copilot April 8, 2026 17:26
@timderes timderes added the enhancement New feature or request label Apr 8, 2026
@timderes timderes changed the title fix: index createdAt on trainings table (Dexie v3 schema) feat(timing): add timing page Apr 8, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 trainings table support and a v3 schema that indexes createdAt to 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.

Comment on lines +26 to +37
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",
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread renderer/utils/timing.ts
Comment on lines +108 to +115
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);
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +306 to +320
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,
});
};
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +92
const settings = useForm<Training>({
initialValues: {
lapsPerStint: 3,
drivers: [],
mode: "JKS",
uuid: getUUID(),
createdAt: Date.now(),
updatedAt: Date.now(),
},
onValuesChange: () => {
settings.setFieldValue("updatedAt", Date.now());
},
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +107
useEffect(() => {
if (savedTrainingUuid) {
database.trainings
.where("uuid")
.equals(savedTrainingUuid)
.first()
.then((tr) => {
setSavedTraining(tr ?? null);
printHandlers.open();
clearSavedTrainingUuid();
})
.catch(() => {});
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +94
<NumberInput
label={t("training:settingsDrawer.lapsPerStint")}
description={t("training:settingsDrawer.lapsPerStintDescription")}
min={1}
max={20}
value={lapsPerStint}
onChange={(val) => onLapsPerStintChange(Number(val))}
/>
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread renderer/public/locales/de/training.json
Comment on lines +215 to +231
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}` }),
);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +59
/** 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}` }),
);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +382 to +395
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(
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
timderes added 4 commits April 8, 2026 19:38
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.
@timderes timderes marked this pull request as draft April 8, 2026 22:03
timderes added 6 commits April 9, 2026 00:08
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants