Skip to content

copy main to tests to start tests - #13

Closed
natekk8 wants to merge 33 commits into
testsfrom
main
Closed

copy main to tests to start tests#13
natekk8 wants to merge 33 commits into
testsfrom
main

Conversation

@natekk8

@natekk8 natekk8 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Nowe funkcje

    • Rozbudowano kokpit wyścigu o ekran startowy, HUD, kąt nachylenia, komunikaty głosowe i sygnały dźwiękowe.
    • Dodano obsługę zapisu okrążeń offline z automatyczną synchronizacją.
    • Race Control oferuje heatmapę, porównanie kierowców, replay sesji i teoretycznie idealne okrążenie.
    • Track Setup umożliwia edycję tras, sektorów i oznaczanie zakrętów.
  • Ulepszenia

    • Dodano responsywne menu, zegar i status połączenia.
    • Usprawniono wykrywanie bramek, pomiar sektorów oraz walidację tras.
    • Zaktualizowano tryb instalowanej aplikacji, cache i działanie offline.
  • Poprawki

    • Usuwanie toru czyści teraz powiązane okrążenia, a dane telemetryczne są automatycznie wygaszane.

natekk8 and others added 30 commits July 21, 2026 15:39
💡 What:
- Fixed lap counting by properly incrementing lap number.
- Fixed lagging timer by using Date.now() in requestAnimationFrame.
- Fixed sector counting by dynamically checking gate count for missing sectors.
- Added ARIA-friendly htmlFor/id bindings for setup forms.
- Added window.confirm dialog to the destructive board reset button.

🎯 Why:
- The racing telemetry was entirely broken (timer skipped updates, lap always saved as 1, sectors crashed on simple tracks).
- Screen readers could not associate labels with inputs in the cockpit screen.
- Users could accidentally wipe all times without any warning.

♿ Accessibility:
- Inputs now have proper labels, increasing click targets and screen reader support.

Co-authored-by: natekk8 <258857220+natekk8@users.noreply.github.com>
…6431868039

🎨 Palette: Fix race telemetry bugs and add UX improvements
- Computes a time offset to allow `Date.now()` to drive a smooth 60fps timer while anchored to GPS timestamps
- Adds a distinct three-tone beep and visual "META" flash when a lap is completed
- Navigates immediately to Race Control (dashboard) with the current track selected when "Zakończ" (Exit Race) is clicked so users see their saved times

Co-authored-by: natekk8 <258857220+natekk8@users.noreply.github.com>
…535681274681918243

fix: Smooth timer, lap finish beep, and navigate to results on exit
- Adds a local reference `lapStartTimeLocalRef` decoupled from the exact GPS timestamps to drive a flawlessly smooth UI visual timer using `Date.now()`.
- Updates `index.html` to add `mobile-web-app-capable` meta tag and provide the exact matching integrity/crossorigin hashes for the preloaded Leaflet stylesheet, fixing console errors and MIME mismatch warnings that appeared alongside the Cloudflare Pages deployment issues.

Co-authored-by: natekk8 <258857220+natekk8@users.noreply.github.com>
- Updated `Date.now()` to `performance.now()` for `lapStartTimeLocalRef` setting and evaluating to ensure visual timer won't jump if system clock adjustments occur and keeps ticking even smoother.

Co-authored-by: natekk8 <258857220+natekk8@users.noreply.github.com>
- Checks `!== null` for `lapStartTimeLocalRef.current` because `performance.now()` can theoretically yield `0`, in which case truthiness would fail.

Co-authored-by: natekk8 <258857220+natekk8@users.noreply.github.com>
…269432776262

Fix UI Timer Jitter and HTML Warnings
- Modified Cockpit.tsx so that "GO GO GO" (setLights(-1)) step is skipped, immediately transitioning from sequence completion to "racing" phase.
- Initialize `lapStartTimeRef` and `lapStartTimeLocalRef` as soon as the sequence finishes and the geolocation check succeeds so the live lap time starts correctly.
- Removed deprecated `<meta name="apple-mobile-web-app-capable">` from index.html.
- Removed unused and failing leaflet.css preload tags from index.html.
- Replaced missing/corrupted icon-192.png and icon-512.png files with transparent placeholders to fix Manifest icon fetching errors.
- Increased `navigator.geolocation.watchPosition` timeout to 20 seconds in Cockpit.tsx to reduce "Timeout expired" exceptions.

Co-authored-by: natekk8 <258857220+natekk8@users.noreply.github.com>
…-13298601280943235114

Fix timer start delay and console errors
…lid rawTime

Move lapStartTimeRef and lapStartTimeLocalRef initialization from line 129-132 
(where rawTime was undefined) into the watchPosition callback at line 142, 
immediately after rawTime is assigned from pos.timestamp. This ensures timing 
is set on the first GPS measurement with a valid timestamp value.
Implemented from a Change Stack AI coding task.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Implemented from a Change Stack AI coding task.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Removed duplicate gpsError state declaration.
Implemented from a Change Stack AI coding task.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Implemented from a Change Stack AI coding task.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
…ych wykresów i poprawki bezpieczeństwa geometrii
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

PR rozszerza pomiar okrążeń, konfigurację torów, obsługę danych Convex, tryb offline, interfejs kokpitu i panel kontroli wyścigu oraz aktualizuje styl aplikacji i mechanizmy PWA.

Changes

FlightDriving

Layer / File(s) Summary
Indeksy i operacje danych
convex/schema.ts, convex/laps.ts, convex/telemetry.ts, convex/tracks.ts
Dodano indeksy Convex, filtrowanie aktywnej telemetrii, aktualizację rekordów przez patch, czyszczenie danych kierowcy oraz usuwanie okrążeń przy usuwaniu toru.
Obliczenia, audio i kolejka offline
src/lib/math.ts, src/lib/audio.ts, src/lib/offlineQueue.ts
Dodano geometrię toru, analizę narożników, komunikaty głosowe, sekwencje beepów i trwałą kolejkę zapisu okrążeń z retry.
Pomiar i zapis okrążeń
src/pages/Cockpit.tsx
Kokpit waliduje konfigurację, wykrywa bramki GPS z interpolacją, zapisuje sektory i okrążenia oraz obsługuje zapis offline, orientację urządzenia i hold-to-confirm.
Konfiguracja toru
src/pages/TrackSetup.tsx
Dodano edycję torów na mapie, snapowanie sektorów, markery narożników, walidację, cofanie, czyszczenie i usuwanie zapisanych tras.
Kontrola i analiza wyścigu
src/pages/RaceControl.tsx
Dodano heatmapę, porównanie kierowców, replay sesji, idealne okrążenie i prezentację kierowców będących w trakcie okrążenia.
Interfejs i tryb PWA
src/components/Header.tsx, src/index.css, index.html, public/sw.js, README.md
Przebudowano nawigację i style, dodano status połączenia, zegar, responsywne menu, wersjonowane strategie cache oraz konfigurację PWA.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Kierowca
  participant Cockpit
  participant GPS
  participant Convex
  participant RaceControl
  Kierowca->>Cockpit: rozpoczyna wyścig
  Cockpit->>GPS: uruchamia śledzenie pozycji
  GPS->>Cockpit: dostarcza poprawki GPS
  Cockpit->>Cockpit: wykrywa bramki i oblicza sektory
  Cockpit->>Convex: zapisuje telemetrię i okrążenie
  RaceControl->>Convex: pobiera timing i telemetrię
  Convex-->>RaceControl: zwraca dane wyścigu
Loading

Possibly related PRs

Poem

Królik przez kokpit pomknął w blasku świateł,
GPS bramki liczy, zapis trwa w komplecie.
Gdy sieć zniknie — kolejka hop do skrzyni,
A neonowy panel sektor pięknie mieni.
Marchewka finiszu brzmi: beep, beep, beep! 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning Tytuł opisuje kopiowanie main do tests, ale zmiany dotyczą rozbudowy aplikacji, logiki toru, offline kolejki i UI. Nadaj tytuł opisujący główną zmianę, np. „Rozbudowa timingu, mapy i trybu offline w FlightDriving”.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch main

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@convex/laps.ts`:
- Around line 47-59: Update clearBoard so an omitted trackId cannot implicitly
delete laps across all tracks. Require an explicit, separately named opt-in flag
for clearing the entire collection, and otherwise reject or no-op when no track
is selected; preserve track-scoped deletion when trackId is provided and update
the caller to use the explicit all-tracks option only when intended.

In `@src/components/Header.tsx`:
- Around line 10-29: Update the updateTime function in the Header component to
format the timestamp explicitly with the UTC time zone, preserving the existing
24-hour display format and UTC label.

In `@src/index.css`:
- Line 3: Update the Google Fonts `@import` declaration in src/index.css to use
the string notation required by Stylelint’s import-notation rule instead of
url(...) notation, preserving the existing font URL and query parameters.

In `@src/lib/offlineQueue.ts`:
- Around line 37-58: Update flushLapQueue so its final persistence merges
stillPending with entries currently in storage, preserving laps added by
queueLap during awaited recordLap calls instead of overwriting them; deduplicate
by _queueId and retain the latest pending state. In the retry branch, log an
error when nextAttempts reaches the retry limit before dropping the item, using
the module’s existing logging convention if available.

In `@src/pages/Cockpit.tsx`:
- Around line 288-306: Correct the off-by-one calculation in the gate-crossing
logic by dividing the combined sub-segment position in subFraction by the six
segments produced by interpolateSubPoints(..., 6), not five. Keep
detectedSubIndex and detectedIntersection unchanged so the resulting
exactTimestamp remains within the lastTime-to-rawTime interval.

In `@src/pages/RaceControl.tsx`:
- Around line 332-340: Dodaj dostępne etykiety do wszystkich wskazanych
kontrolek w komponencie RaceControl: w zakresie
src/pages/RaceControl.tsx#L332-L340 ustaw aria-label dla selectów typu pojazdu i
wyboru trasy; w `#L409-L416` ustaw aria-label dla suwaka postępu odtwarzania; w
`#L480-L491` powiąż etykiety z selectami kierowców A/B przez htmlFor/id albo dodaj
im odpowiednie aria-label.
- Around line 388-394: Update the reset handler in the “Reset Wyników” button to
require a selected track before calling clearBoard; when selectedTrack is empty,
prevent the action instead of passing an undefined trackId that clears all
routes. Preserve the existing confirmation and track-specific reset behavior for
a valid selection.
- Around line 239-277: The heatmap effect unnecessarily rebuilds every segment
whenever telemetry changes. In the effect around the heatmap layer setup,
resolve the selected track’s driver telemetry once before the segment loop and
reuse its gForce for coloring, then narrow the effect dependencies or otherwise
prevent unrelated telemetry updates from triggering a redraw while preserving
updates for the relevant track telemetry.

In `@src/pages/TrackSetup.tsx`:
- Around line 206-210: Update the mode transition logic in TrackSetup’s setMode
callback so it advances from s1 to s2 or s2 to draw only when sector-point
insertion succeeds. Preserve the current mode when snapping fails because
minDistance is not below 35, and keep the existing transition behavior for
successful insertions.
- Around line 220-261: Usuń zduplikowany efekt useEffect obejmujący rysowanie
polilinii i markerów bez Corner Badges. Zachowaj wyłącznie wcześniejszy efekt
rysujący warstwę wraz ze znacznikami narożników, aby layerGroup.current nie był
czyszczony i odrysowywany drugi raz.
- Around line 132-205: Refactor clickHandler so the path and index calculations
occur before any state updater, rather than calling setPath, setS1Index, or
setS2Index inside the setMode callback. Use the current mode to compute the next
path and index values once, then invoke flat setPath, setS1Index, and setS2Index
calls outside setMode; preserve the existing draw behavior, snapping logic, and
index reindexing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9619e658-22a6-4364-98a9-ab05bf999ae9

📥 Commits

Reviewing files that changed from the base of the PR and between e232429 and bf3c5fb.

⛔ Files ignored due to path filters (5)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/icons/icon-192.png is excluded by !**/*.png
  • public/icons/icon-192.svg is excluded by !**/*.svg
  • public/icons/icon-512.png is excluded by !**/*.png
  • public/icons/icon-512.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • README.md
  • convex/laps.ts
  • convex/schema.ts
  • convex/telemetry.ts
  • convex/tracks.ts
  • index.html
  • public/sw.js
  • src/components/Header.tsx
  • src/index.css
  • src/lib/audio.ts
  • src/lib/math.ts
  • src/lib/offlineQueue.ts
  • src/pages/Cockpit.tsx
  • src/pages/RaceControl.tsx
  • src/pages/TrackSetup.tsx

Comment thread convex/laps.ts
Comment on lines +47 to +59
args: {
trackId: v.optional(v.id("tracks")),
},
handler: async (ctx, args) => {
let laps;
if (args.trackId) {
laps = await ctx.db
.query("laps")
.withIndex("by_trackId", (q) => q.eq("trackId", args.trackId!))
.collect();
} else {
laps = await ctx.db.query("laps").collect();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Brak trackId skutkuje usunięciem WSZYSTKICH okrążeń ze wszystkich tras, nie tylko wybranej.

clearBoard traktuje pominięty trackId jako „wyczyść cały zbiór” (linia 58: laps = await ctx.db.query("laps").collect();). Wywołanie w RaceControl.tsx:391 to clearBoard({ trackId: selectedTrack || undefined }) — jeśli selectedTrack jest puste (np. przed automatycznym wyborem trasy w efekcie inicjalizującym, gdy tracks.length === 0), użytkownik klikający przycisk z opisem „zresetować wyniki dla tej trasy” bezpowrotnie usunie dane wszystkich tras (dialog potwierdzenia mówi „Tej akcji nie można cofnąć”). To niebezpieczne zachowanie „silent fallback to delete-all” bez świadomej zgody użytkownika.

Rozważ rozdzielenie intencji „brak wyboru” od „wyczyść wszystko”, np. wymagając explicit flagi do usunięcia całości.

🛡️ Przykładowa poprawka
 export const clearBoard = mutation({
   args: {
     trackId: v.optional(v.id("tracks")),
+    confirmDeleteAll: v.optional(v.boolean()),
   },
   handler: async (ctx, args) => {
     let laps;
     if (args.trackId) {
       laps = await ctx.db
         .query("laps")
         .withIndex("by_trackId", (q) => q.eq("trackId", args.trackId!))
         .collect();
+    } else if (args.confirmDeleteAll) {
+      laps = await ctx.db.query("laps").collect();
     } else {
-      laps = await ctx.db.query("laps").collect();
+      throw new Error("trackId is required unless confirmDeleteAll is explicitly set.");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
args: {
trackId: v.optional(v.id("tracks")),
},
handler: async (ctx, args) => {
let laps;
if (args.trackId) {
laps = await ctx.db
.query("laps")
.withIndex("by_trackId", (q) => q.eq("trackId", args.trackId!))
.collect();
} else {
laps = await ctx.db.query("laps").collect();
}
args: {
trackId: v.optional(v.id("tracks")),
confirmDeleteAll: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
let laps;
if (args.trackId) {
laps = await ctx.db
.query("laps")
.withIndex("by_trackId", (q) => q.eq("trackId", args.trackId!))
.collect();
} else if (args.confirmDeleteAll) {
laps = await ctx.db.query("laps").collect();
} else {
throw new Error("trackId is required unless confirmDeleteAll is explicitly set.");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/laps.ts` around lines 47 - 59, Update clearBoard so an omitted trackId
cannot implicitly delete laps across all tracks. Require an explicit, separately
named opt-in flag for clearing the entire collection, and otherwise reject or
no-op when no track is selected; preserve track-scoped deletion when trackId is
provided and update the caller to use the explicit all-tracks option only when
intended.

Comment thread src/components/Header.tsx
Comment on lines +10 to +29
useEffect(() => {
const updateTime = () => {
const now = new Date();
setTime(now.toLocaleTimeString('pl-PL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }));
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, []);

return (
<div style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '5px 10px', borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.04)', border: '1px solid rgba(255, 255, 255, 0.1)',
color: '#a0a0b8', fontSize: '11px', fontWeight: 600, letterSpacing: '0.5px'
}} className="font-digital">
<Clock size={12} style={{ color: 'var(--neon-cyan)' }} />
<span>{time || '00:00:00'} UTC</span>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Etykieta „UTC” nie odpowiada wyświetlanemu czasowi.

toLocaleTimeString('pl-PL', …) bez timeZone renderuje czas lokalny urządzenia, a interfejs oznacza go jako „UTC”. Dla użytkowników w innej strefie niż UTC informacja jest myląca. Ustaw jawnie strefę na UTC albo usuń etykietę.

🛠️ Proponowana poprawka (jawne UTC)
-      setTime(now.toLocaleTimeString('pl-PL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }));
+      setTime(now.toLocaleTimeString('pl-PL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'UTC' }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const updateTime = () => {
const now = new Date();
setTime(now.toLocaleTimeString('pl-PL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }));
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, []);
return (
<div style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '5px 10px', borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.04)', border: '1px solid rgba(255, 255, 255, 0.1)',
color: '#a0a0b8', fontSize: '11px', fontWeight: 600, letterSpacing: '0.5px'
}} className="font-digital">
<Clock size={12} style={{ color: 'var(--neon-cyan)' }} />
<span>{time || '00:00:00'} UTC</span>
</div>
useEffect(() => {
const updateTime = () => {
const now = new Date();
setTime(now.toLocaleTimeString('pl-PL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'UTC' }));
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, []);
return (
<div style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '5px 10px', borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.04)', border: '1px solid rgba(255, 255, 255, 0.1)',
color: '`#a0a0b8`', fontSize: '11px', fontWeight: 600, letterSpacing: '0.5px'
}} className="font-digital">
<Clock size={12} style={{ color: 'var(--neon-cyan)' }} />
<span>{time || '00:00:00'} UTC</span>
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Header.tsx` around lines 10 - 29, Update the updateTime
function in the Header component to format the timestamp explicitly with the UTC
time zone, preserving the existing 24-hour display format and UTC label.

Comment thread src/index.css
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Outfit:wght@300;400;600;800&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Zmień @import url(...) na notację stringową (błąd Stylelint).

Stylelint (import-notation) zgłasza to jako błąd i zatrzyma lint w pipeline.

🎨 Proponowana poprawka
-@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap');
+@import 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap');
`@import` 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap';
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 3-3: Expected "url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap')" to be "'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;800;900&family=Outfit:wght@300;400;600;700;800;900&display=swap'" (import-notation)

(import-notation)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.css` at line 3, Update the Google Fonts `@import` declaration in
src/index.css to use the string notation required by Stylelint’s import-notation
rule instead of url(...) notation, preserving the existing font URL and query
parameters.

Source: Linters/SAST tools

Comment thread src/lib/offlineQueue.ts
Comment on lines +37 to +58
export async function flushLapQueue(
recordLap: (args: any) => Promise<unknown>
): Promise<number> {
const queue = readQueue();
if (queue.length === 0) return 0;

const stillPending: PendingLap[] = [];
for (const item of queue) {
const { _queueId, _attempts = 0, ...lapArgs } = item;
try {
await recordLap(lapArgs);
} catch {
const nextAttempts = _attempts + 1;
if (nextAttempts < 5) {
stillPending.push({ ...item, _attempts: nextAttempts });
}
}
}

writeQueue(stillPending);
return stillPending.length;
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Wyścig o dane (TOCTOU) między queueLap a flushLapQueue może bezpowrotnie usunąć nowo dodane okrążenie.

flushLapQueue robi snapshot kolejki na wejściu (const queue = readQueue();), a na końcu nadpisuje całą kolejkę wynikiem stillPending (writeQueue(stillPending)). Jeśli w trakcie sekwencyjnych await recordLap(...) (np. przy kilku elementach lub słabym połączeniu) użytkownik zakończy kolejne okrążenie i wywoła queueLap() (dopisując do localStorage), to writeQueue(stillPending) na końcu flush nadpisze cały wpis, gubiąc właśnie dodany lap bez żadnego logu czy ostrzeżenia.

Dodatkowo po nextAttempts >= 5 element jest po prostu odrzucany (linia 50) bez żadnego console.error/logu — dane wyniku okrążenia giną bezgłośnie.

🛡️ Przykładowa poprawka (scalanie po _queueId zamiast pełnego nadpisania) + logowanie odrzuconych elementów
 export async function flushLapQueue(
   recordLap: (args: any) => Promise<unknown>
 ): Promise<number> {
   const queue = readQueue();
   if (queue.length === 0) return 0;

-  const stillPending: PendingLap[] = [];
+  const failedIds = new Set<string>();
+  const attemptsById = new Map<string, number>();
   for (const item of queue) {
     const { _queueId, _attempts = 0, ...lapArgs } = item;
     try {
       await recordLap(lapArgs);
     } catch {
       const nextAttempts = _attempts + 1;
       if (nextAttempts < 5) {
-        stillPending.push({ ...item, _attempts: nextAttempts });
+        failedIds.add(_queueId);
+        attemptsById.set(_queueId, nextAttempts);
+      } else {
+        console.error('Lap odrzucony po 5 nieudanych próbach synchronizacji', item);
       }
     }
   }

-  writeQueue(stillPending);
-  return stillPending.length;
+  // Re-read the current queue at write time to avoid clobbering items
+  // added by queueLap() while this flush was in-flight.
+  const currentQueue = readQueue();
+  const merged = currentQueue
+    .filter((item) => !queue.some((q) => q._queueId === item._queueId) || failedIds.has(item._queueId))
+    .map((item) => attemptsById.has(item._queueId) ? { ...item, _attempts: attemptsById.get(item._queueId)! } : item);
+  writeQueue(merged);
+  return merged.length;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function flushLapQueue(
recordLap: (args: any) => Promise<unknown>
): Promise<number> {
const queue = readQueue();
if (queue.length === 0) return 0;
const stillPending: PendingLap[] = [];
for (const item of queue) {
const { _queueId, _attempts = 0, ...lapArgs } = item;
try {
await recordLap(lapArgs);
} catch {
const nextAttempts = _attempts + 1;
if (nextAttempts < 5) {
stillPending.push({ ...item, _attempts: nextAttempts });
}
}
}
writeQueue(stillPending);
return stillPending.length;
}
export async function flushLapQueue(
recordLap: (args: any) => Promise<unknown>
): Promise<number> {
const queue = readQueue();
if (queue.length === 0) return 0;
const failedIds = new Set<string>();
const attemptsById = new Map<string, number>();
for (const item of queue) {
const { _queueId, _attempts = 0, ...lapArgs } = item;
try {
await recordLap(lapArgs);
} catch {
const nextAttempts = _attempts + 1;
if (nextAttempts < 5) {
failedIds.add(_queueId);
attemptsById.set(_queueId, nextAttempts);
} else {
console.error('Lap odrzucony po 5 nieudanych próbach synchronizacji', item);
}
}
}
// Re-read the current queue at write time to avoid clobbering items
// added by queueLap() while this flush was in-flight.
const currentQueue = readQueue();
const merged = currentQueue
.filter((item) => !queue.some((q) => q._queueId === item._queueId) || failedIds.has(item._queueId))
.map((item) => attemptsById.has(item._queueId) ? { ...item, _attempts: attemptsById.get(item._queueId)! } : item);
writeQueue(merged);
return merged.length;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/offlineQueue.ts` around lines 37 - 58, Update flushLapQueue so its
final persistence merges stillPending with entries currently in storage,
preserving laps added by queueLap during awaited recordLap calls instead of
overwriting them; deduplicate by _queueId and retain the latest pending state.
In the retry branch, log an error when nextAttempts reaches the retry limit
before dropping the item, using the module’s existing logging convention if
available.

Comment thread src/pages/Cockpit.tsx
Comment on lines +288 to +306
const subPoints = interpolateSubPoints(lastPoint, currentPoint, 6);
let detectedIntersection: number | null = null;
let detectedSubIndex = 0;

for (let step = 0; step < subPoints.length - 1; step++) {
const ua = checkLineIntersection(subPoints[step], subPoints[step + 1], gate[0], gate[1]);
if (ua !== null) {
detectedIntersection = ua;
detectedSubIndex = step;
break;
}
}

if (detectedIntersection !== null && Date.now() - lastGateCrossTime < MIN_GATE_REARM_MS) {
// Re-arm ignore
} else if (detectedIntersection !== null) {
lastGateCrossTime = Date.now();
const subFraction = (detectedSubIndex + detectedIntersection) / 5;
const exactTimestamp = lastTime + subFraction * (rawTime - lastTime);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Błąd off-by-one w obliczaniu subFraction zaniża dokładność czasu przecięcia bramki.

interpolateSubPoints(lastPoint, currentPoint, 6) zwraca 7 punktów, czyli 6 odcinków (pętla step iteruje 0..5). Ułamek pozycji przecięcia względem całego interwału GPS to (detectedSubIndex + detectedIntersection) / 6, ale kod dzieli przez 5. Powoduje to przeskalowanie o współczynnik 1.2 — dla detectedSubIndex = 5, ua = 1 subFraction = 1.2, więc exactTimestamp wypada po rawTime. Przekłada się to bezpośrednio na błąd czasów sektorów i okrążenia (dziesiątki–setki ms przy typowym interwale fixów).

🐛 Proponowana poprawka
-            const subFraction = (detectedSubIndex + detectedIntersection) / 5;
+            const subFraction = (detectedSubIndex + detectedIntersection) / (subPoints.length - 1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const subPoints = interpolateSubPoints(lastPoint, currentPoint, 6);
let detectedIntersection: number | null = null;
let detectedSubIndex = 0;
for (let step = 0; step < subPoints.length - 1; step++) {
const ua = checkLineIntersection(subPoints[step], subPoints[step + 1], gate[0], gate[1]);
if (ua !== null) {
detectedIntersection = ua;
detectedSubIndex = step;
break;
}
}
if (detectedIntersection !== null && Date.now() - lastGateCrossTime < MIN_GATE_REARM_MS) {
// Re-arm ignore
} else if (detectedIntersection !== null) {
lastGateCrossTime = Date.now();
const subFraction = (detectedSubIndex + detectedIntersection) / 5;
const exactTimestamp = lastTime + subFraction * (rawTime - lastTime);
const subPoints = interpolateSubPoints(lastPoint, currentPoint, 6);
let detectedIntersection: number | null = null;
let detectedSubIndex = 0;
for (let step = 0; step < subPoints.length - 1; step++) {
const ua = checkLineIntersection(subPoints[step], subPoints[step + 1], gate[0], gate[1]);
if (ua !== null) {
detectedIntersection = ua;
detectedSubIndex = step;
break;
}
}
if (detectedIntersection !== null && Date.now() - lastGateCrossTime < MIN_GATE_REARM_MS) {
// Re-arm ignore
} else if (detectedIntersection !== null) {
lastGateCrossTime = Date.now();
const subFraction = (detectedSubIndex + detectedIntersection) / (subPoints.length - 1);
const exactTimestamp = lastTime + subFraction * (rawTime - lastTime);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/Cockpit.tsx` around lines 288 - 306, Correct the off-by-one
calculation in the gate-crossing logic by dividing the combined sub-segment
position in subFraction by the six segments produced by
interpolateSubPoints(..., 6), not five. Keep detectedSubIndex and
detectedIntersection unchanged so the resulting exactTimestamp remains within
the lastTime-to-rawTime interval.

Comment thread src/pages/RaceControl.tsx
Comment on lines +332 to 340
<select className="custom-select" style={{ flex: '1 1 160px', width: 'auto', minWidth: '140px' }} value={activeTab} onChange={e => setActiveTab(e.target.value as any)}>
<option value="scooter">🛵 Wyniki: HULAJNOGI</option>
<option value="bike">🚴 Wyniki: ROWERY</option>
</select>
<select className="custom-select" style={{ width: '300px', transform: 'skewX(-12deg)' }} value={selectedTrack} onChange={e => setSelectedTrack(e.target.value)}>

<select className="custom-select" style={{ flex: '1 1 200px', width: 'auto', minWidth: '160px' }} value={selectedTrack} onChange={e => setSelectedTrack(e.target.value)}>
<option value="">-- Wybierz Trasę --</option>
{tracks.map((t: any) => <option key={t._id} value={t._id}>{t.name}</option>)}
</select>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Brakujące etykiety dostępne dla kontrolek interaktywnych. Wspólna przyczyna: kontrolki w RaceControl nie mają nazw/powiązań dostępnych, więc czytniki ekranu nie potrafią określić ich przeznaczenia.

  • src/pages/RaceControl.tsx#L332-L340: dodaj aria-label do obu selectów (typ pojazdu, wybór trasy).
  • src/pages/RaceControl.tsx#L409-L416: dodaj aria-label do pola type="range" (postęp odtwarzania).
  • src/pages/RaceControl.tsx#L480-L491: powiąż <label> z selectami przez htmlFor/id (lub dodaj aria-label do selectów kierowcy A/B).
🧰 Tools
🪛 React Doctor (0.7.6)

[warning] 332-332: Blind users can't tell what this control does because screen readers find no label, so add visible text, aria-label, or aria-labelledby.

Give every interactive control a label screen readers can read.

(control-has-associated-label)


[warning] 337-337: Blind users can't tell what this control does because screen readers find no label, so add visible text, aria-label, or aria-labelledby.

Give every interactive control a label screen readers can read.

(control-has-associated-label)

📍 Affects 1 file
  • src/pages/RaceControl.tsx#L332-L340 (this comment)
  • src/pages/RaceControl.tsx#L409-L416
  • src/pages/RaceControl.tsx#L480-L491
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/RaceControl.tsx` around lines 332 - 340, Dodaj dostępne etykiety do
wszystkich wskazanych kontrolek w komponencie RaceControl: w zakresie
src/pages/RaceControl.tsx#L332-L340 ustaw aria-label dla selectów typu pojazdu i
wyboru trasy; w `#L409-L416` ustaw aria-label dla suwaka postępu odtwarzania; w
`#L480-L491` powiąż etykiety z selectami kierowców A/B przez htmlFor/id albo dodaj
im odpowiednie aria-label.

Source: Linters/SAST tools

Comment thread src/pages/RaceControl.tsx
Comment on lines +388 to +394
<button
className="btn-danger"
style={{ fontSize: '12px', padding: '12px 16px' }}
onClick={() => { if (window.confirm('Czy na pewno chcesz zresetować wyniki dla tej trasy? (Tej akcji nie można cofnąć)')) { clearBoard({ trackId: selectedTrack || undefined }); } }}
>
Reset Wyników
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reset usuwa wyniki wszystkich tras, gdy trasa nie jest wybrana.

Gdy selectedTrack jest puste (opcja „-- Wybierz Trasę --"), selectedTrack || undefined daje undefined, a clearBoard bez trackId usuwa wszystkie okrążenia ze wszystkich tras (patrz kontrakt convex/laps.ts clearBoard). Tymczasem komunikat potwierdzenia mówi „zresetować wyniki dla tej trasy". To rozbieżność grożąca przypadkową utratą danych.

🛡️ Proponowana poprawka
         <button 
           className="btn-danger" 
           style={{ fontSize: '12px', padding: '12px 16px' }} 
-          onClick={() => { if (window.confirm('Czy na pewno chcesz zresetować wyniki dla tej trasy? (Tej akcji nie można cofnąć)')) { clearBoard({ trackId: selectedTrack || undefined }); } }}
+          disabled={!selectedTrack}
+          onClick={() => { if (selectedTrack && window.confirm('Czy na pewno chcesz zresetować wyniki dla tej trasy? (Tej akcji nie można cofnąć)')) { clearBoard({ trackId: selectedTrack }); } }}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className="btn-danger"
style={{ fontSize: '12px', padding: '12px 16px' }}
onClick={() => { if (window.confirm('Czy na pewno chcesz zresetować wyniki dla tej trasy? (Tej akcji nie można cofnąć)')) { clearBoard({ trackId: selectedTrack || undefined }); } }}
>
Reset Wyników
</button>
<button
className="btn-danger"
style={{ fontSize: '12px', padding: '12px 16px' }}
disabled={!selectedTrack}
onClick={() => { if (selectedTrack && window.confirm('Czy na pewno chcesz zresetować wyniki dla tej trasy? (Tej akcji nie można cofnąć)')) { clearBoard({ trackId: selectedTrack }); } }}
>
Reset Wyników
</button>
🧰 Tools
🪛 React Doctor (0.7.6)

[warning] 388-388: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/RaceControl.tsx` around lines 388 - 394, Update the reset handler
in the “Reset Wyników” button to require a selected track before calling
clearBoard; when selectedTrack is empty, prevent the action instead of passing
an undefined trackId that clears all routes. Preserve the existing confirmation
and track-specific reset behavior for a valid selection.

Comment thread src/pages/TrackSetup.tsx
Comment on lines +132 to 205
const clickHandler = (e: L.LeafletMouseEvent) => {
setMode((currentMode) => {
setPath(currentPath => {
if (currentMode === 'draw') {
return [...currentPath, { lat: e.latlng.lat, lon: e.latlng.lng }];
}

if (currentMode === 's1' || currentMode === 's2') {
if (currentPath.length < 2) return currentPath;

const oldIndex = currentMode === 's1' ? s1IndexRef.current : s2IndexRef.current;
const workingPath = oldIndex !== undefined
? currentPath.filter((_, idx) => idx !== oldIndex)
: currentPath;

const map = leafletMap.current!;
const clickPt = map.latLngToLayerPoint(e.latlng);
let minDistance = Infinity;
let bestSegmentIndex = -1;
let bestProjectedPt: any = clickPt;

for (let i = 0; i < workingPath.length - 1; i++) {
const p1 = map.latLngToLayerPoint(L.latLng(workingPath[i].lat, workingPath[i].lon));
const p2 = map.latLngToLayerPoint(L.latLng(workingPath[i+1].lat, workingPath[i+1].lon));

const v = { x: p2.x - p1.x, y: p2.y - p1.y };
const w = { x: clickPt.x - p1.x, y: clickPt.y - p1.y };

const c1 = w.x * v.x + w.y * v.y;
const c2 = v.x * v.x + v.y * v.y;

let projPt;
if (c1 <= 0) {
projPt = p1;
} else if (c2 <= c1) {
projPt = p2;
} else {
const b = c1 / c2;
projPt = { x: p1.x + b * v.x, y: p1.y + b * v.y };
}

const dist = Math.sqrt(Math.pow(clickPt.x - projPt.x, 2) + Math.pow(clickPt.y - projPt.y, 2));

if (dist < minDistance) {
minDistance = dist;
bestSegmentIndex = i;
bestProjectedPt = projPt;
}
}

// Accurate snap distance threshold (35 screen pixels)
if (minDistance < 35) {
const newLatLng = map.layerPointToLatLng(bestProjectedPt as any);
const newPath = [...workingPath];
newPath.splice(bestSegmentIndex + 1, 0, { lat: newLatLng.lat, lon: newLatLng.lng });
const newIndex = bestSegmentIndex + 1;

const reindexOther = (idx: number | undefined) => {
if (idx === undefined) return idx;
let adjusted = idx;
if (oldIndex !== undefined && oldIndex < adjusted) adjusted--;
if (bestSegmentIndex < adjusted) adjusted++;
return adjusted;
};

setS1Index(s1 => currentMode === 's1' ? newIndex : reindexOther(s1));
setS2Index(s2 => currentMode === 's2' ? newIndex : reindexOther(s2));

return newPath;
}
return currentPath;
}
return currentPath;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

React 19 StrictMode state updater function called twice purity

💡 Result:

In React 19, Strict Mode intentionally invokes state updater functions twice during development to ensure they remain pure and idempotent [1][2][3]. This behavior is a diagnostic tool designed to surface bugs caused by side effects or mutations during the rendering phase [1][4]. Key points regarding this behavior: Purpose of Purity: React assumes that components and their associated logic—including state updater functions, component bodies, and initializer functions—are pure [1][5]. A pure function must produce the same output for the same input without side effects [1][4][5]. Because a pure function's result is identical regardless of how many times it is executed, double-invoking it in development does not change the application's final state [1][4]. Diagnostic Utility: If an updater function is impure (e.g., it mutates external data or performs side effects), the double invocation will often produce observable differences in behavior or state, making the bug immediately apparent [1][4]. This proactively helps developers identify and fix code that would otherwise cause unpredictable behavior in production [1][6]. Scope of Strict Mode: The double-invocation behavior applies exclusively to development environments [1][4]. It has no effect on production builds, so it does not impact application performance for end users [4]. Evolution in React 19: React 19 maintains and reinforces these diagnostic behaviors [6]. For example, in addition to updater functions, Strict Mode also double-invokes useState and useReducer initializer functions to maintain consistency in detecting impure logic [3][7]. Furthermore, React 19 improves Strict Mode by reusing memoized results from the first render during the second render for useMemo and useCallback to minimize redundant calculations while still verifying purity [6][7]. If you observe unexpected behavior (such as incorrect state values or side effects appearing twice) during development, it is a signal that the logic within the updater function or component is impure and should be refactored to remove side effects or mutations [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first, then inspect relevant ranges.
ast-grep outline src/pages/TrackSetup.tsx --view expanded || true

echo
echo '--- src/pages/TrackSetup.tsx (lines 1-260) ---'
sed -n '1,260p' src/pages/TrackSetup.tsx

Repository: natekk8/flightdriving

Length of output: 11378


Usuń efekty uboczne z updaterów stanu
setPath, setS1Index i setS2Index są wywoływane wewnątrz updatera setMode. W StrictMode taki updater może zostać uruchomiony więcej niż raz, więc jedno kliknięcie może dodać punkt lub przestawić indeksy podwójnie. Przenieś obliczenia poza callbacki i użyj płaskich setPath/setS1Index/setS2Index.

🧰 Tools
🪛 React Doctor (0.7.6)

[error] 133-133: This state updater performs the nested state update "setPath()". React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.

Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.

(no-impure-state-updater)


[error] 134-134: This state updater performs the nested state update "setS1Index()". React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.

Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.

(no-impure-state-updater)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/TrackSetup.tsx` around lines 132 - 205, Refactor clickHandler so
the path and index calculations occur before any state updater, rather than
calling setPath, setS1Index, or setS2Index inside the setMode callback. Use the
current mode to compute the next path and index values once, then invoke flat
setPath, setS1Index, and setS2Index calls outside setMode; preserve the existing
draw behavior, snapping logic, and index reindexing.

Source: Linters/SAST tools

Comment thread src/pages/TrackSetup.tsx
Comment on lines +206 to 210

if (currentMode === 's1') return 's2';
if (currentMode === 's2') return 'draw';
return currentMode;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tryb przechodzi dalej nawet przy nietrafionym „snapowaniu”.

setMode przechodzi s1 → s2 → draw bezwarunkowo, niezależnie od tego, czy kliknięcie faktycznie ustawiło sektor. Jeśli użytkownik kliknie dalej niż próg 35 px (minDistance < 35 niespełnione), sektor nie zostaje ustawiony, ale tryb i tak się przełącza — co pozostawia niedokończony sektor i wymusza ponowne wejście w tryb. Awansuj tryb tylko po udanym wstawieniu punktu sektora.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/TrackSetup.tsx` around lines 206 - 210, Update the mode transition
logic in TrackSetup’s setMode callback so it advances from s1 to s2 or s2 to
draw only when sector-point insertion succeeds. Preserve the current mode when
snapping fails because minDistance is not below 35, and keep the existing
transition behavior for successful insertions.

Comment thread src/pages/TrackSetup.tsx
Comment on lines +220 to 261
useEffect(() => {
if (!layerGroup.current) return;
layerGroup.current.clearLayers();

let s1 = s1Index ?? path.length;
let s2 = s2Index ?? path.length;

const p1 = path.slice(0, s1 + 1);
const p2 = path.slice(s1, s2 + 1);
const p3 = path.slice(s2, path.length);

if (p1.length > 0) L.polyline(p1 as any, { color: '#00f0ff', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
if (p2.length > 0) L.polyline(p2 as any, { color: '#f3123c', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
if (p3.length > 0) L.polyline(p3 as any, { color: '#39ff14', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);

path.forEach((pt, idx) => {
const isStart = idx === 0;
const isFinish = idx === path.length - 1;
const isS1 = idx === s1Index;
const isS2 = idx === s2Index;

let html = `<div style="width:100%;height:100%;border-radius:50%;background:rgba(255,255,255,0.4);border:1px solid white;"></div>`;
let size = 12;

if (isStart) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#00f0ff;box-shadow:0 0 10px #00f0ff;border:2px solid white;"></div>`;
size = 18;
} else if (isFinish) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#39ff14;box-shadow:0 0 10px #39ff14;border:2px solid white;"></div>`;
size = 18;
} else if (isS1) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#f3123c;box-shadow:0 0 15px #f3123c;border:3px solid white;animation: pulse 1.5s infinite;"></div>`;
size = 24;
} else if (isS2) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#ff9100;box-shadow:0 0 15px #ff9100;border:3px solid white;animation: pulse 1.5s infinite;"></div>`;
size = 24;
}

const icon = L.divIcon({ html, className: '', iconSize: [size, size] });
L.marker([pt.lat, pt.lon], { icon }).addTo(layerGroup.current!);
});
}, [path, s1Index, s2Index]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Zduplikowany efekt kasuje znaczniki narożników.

Ten efekt (220-261) robi dokładnie to samo co efekt z linii 76-126: wywołuje layerGroup.current.clearLayers() i ponownie rysuje polilinie oraz markery punktów — ale bez „Corner Badges”. Ponieważ corners jest wyliczane z path, oba efekty uruchamiają się razem przy każdej zmianie path/s1Index/s2Index. Efekty wykonują się w kolejności deklaracji, więc ten, uruchamiając się jako drugi, czyści warstwę i odrysowuje ją bez znaczników narożników — badge'y z pierwszego efektu są natychmiast nadpisywane i praktycznie nigdy nie są widoczne. Dodatkowo cała warstwa jest rysowana dwukrotnie przy każdej aktualizacji.

Usuń ten duplikat i pozostaw wyłącznie efekt z linii 76-126 (jest jego nadzbiorem).

🐛 Proponowana poprawka — usunięcie zduplikowanego efektu
-  useEffect(() => {
-    if (!layerGroup.current) return;
-    layerGroup.current.clearLayers();
-
-    let s1 = s1Index ?? path.length;
-    let s2 = s2Index ?? path.length;
-
-    const p1 = path.slice(0, s1 + 1);
-    const p2 = path.slice(s1, s2 + 1);
-    const p3 = path.slice(s2, path.length);
-
-    if (p1.length > 0) L.polyline(p1 as any, { color: '`#00f0ff`', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
-    if (p2.length > 0) L.polyline(p2 as any, { color: '`#f3123c`', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
-    if (p3.length > 0) L.polyline(p3 as any, { color: '`#39ff14`', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
-
-    path.forEach((pt, idx) => {
-      const isStart = idx === 0;
-      const isFinish = idx === path.length - 1;
-      const isS1 = idx === s1Index;
-      const isS2 = idx === s2Index;
-      
-      let html = `<div style="width:100%;height:100%;border-radius:50%;background:rgba(255,255,255,0.4);border:1px solid white;"></div>`;
-      let size = 12;
-
-      if (isStart) {
-        html = `<div style="width:100%;height:100%;border-radius:50%;background:`#00f0ff`;box-shadow:0 0 10px `#00f0ff`;border:2px solid white;"></div>`;
-        size = 18;
-      } else if (isFinish) {
-        html = `<div style="width:100%;height:100%;border-radius:50%;background:`#39ff14`;box-shadow:0 0 10px `#39ff14`;border:2px solid white;"></div>`;
-        size = 18;
-      } else if (isS1) {
-        html = `<div style="width:100%;height:100%;border-radius:50%;background:`#f3123c`;box-shadow:0 0 15px `#f3123c`;border:3px solid white;animation: pulse 1.5s infinite;"></div>`;
-        size = 24;
-      } else if (isS2) {
-        html = `<div style="width:100%;height:100%;border-radius:50%;background:`#ff9100`;box-shadow:0 0 15px `#ff9100`;border:3px solid white;animation: pulse 1.5s infinite;"></div>`;
-        size = 24;
-      }
-
-      const icon = L.divIcon({ html, className: '', iconSize: [size, size] });
-      L.marker([pt.lat, pt.lon], { icon }).addTo(layerGroup.current!);
-    });
-  }, [path, s1Index, s2Index]);
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!layerGroup.current) return;
layerGroup.current.clearLayers();
let s1 = s1Index ?? path.length;
let s2 = s2Index ?? path.length;
const p1 = path.slice(0, s1 + 1);
const p2 = path.slice(s1, s2 + 1);
const p3 = path.slice(s2, path.length);
if (p1.length > 0) L.polyline(p1 as any, { color: '#00f0ff', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
if (p2.length > 0) L.polyline(p2 as any, { color: '#f3123c', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
if (p3.length > 0) L.polyline(p3 as any, { color: '#39ff14', weight: 6, opacity: 0.8 }).addTo(layerGroup.current);
path.forEach((pt, idx) => {
const isStart = idx === 0;
const isFinish = idx === path.length - 1;
const isS1 = idx === s1Index;
const isS2 = idx === s2Index;
let html = `<div style="width:100%;height:100%;border-radius:50%;background:rgba(255,255,255,0.4);border:1px solid white;"></div>`;
let size = 12;
if (isStart) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#00f0ff;box-shadow:0 0 10px #00f0ff;border:2px solid white;"></div>`;
size = 18;
} else if (isFinish) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#39ff14;box-shadow:0 0 10px #39ff14;border:2px solid white;"></div>`;
size = 18;
} else if (isS1) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#f3123c;box-shadow:0 0 15px #f3123c;border:3px solid white;animation: pulse 1.5s infinite;"></div>`;
size = 24;
} else if (isS2) {
html = `<div style="width:100%;height:100%;border-radius:50%;background:#ff9100;box-shadow:0 0 15px #ff9100;border:3px solid white;animation: pulse 1.5s infinite;"></div>`;
size = 24;
}
const icon = L.divIcon({ html, className: '', iconSize: [size, size] });
L.marker([pt.lat, pt.lon], { icon }).addTo(layerGroup.current!);
});
}, [path, s1Index, s2Index]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/TrackSetup.tsx` around lines 220 - 261, Usuń zduplikowany efekt
useEffect obejmujący rysowanie polilinii i markerów bez Corner Badges. Zachowaj
wyłącznie wcześniejszy efekt rysujący warstwę wraz ze znacznikami narożników,
aby layerGroup.current nie był czyszczony i odrysowywany drugi raz.

@natekk8 natekk8 closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant