Conversation
💡 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.
Initialize lap start time conditionally
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>
…33779 Apply chat suggestion
…f33779 Apply chat suggestion
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>
…etrii toru i rozbudowa edytora tras
…, Ideal Lap, and Smart Heatmap
…a układu Cockpit HUD
…ych wykresów i poprawki bezpieczeństwa geometrii
…r Motion animations
…, and scale Cockpit start lights
…rów ze zgaśnięciem świateł F1
…for mobile viewports
… ukończonych okrążeń po kliknięciu ZAKOŃCZ
📝 WalkthroughWalkthroughPR 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. ChangesFlightDriving
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (5)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/icons/icon-192.pngis excluded by!**/*.pngpublic/icons/icon-192.svgis excluded by!**/*.svgpublic/icons/icon-512.pngis excluded by!**/*.pngpublic/icons/icon-512.svgis excluded by!**/*.svg
📒 Files selected for processing (15)
README.mdconvex/laps.tsconvex/schema.tsconvex/telemetry.tsconvex/tracks.tsindex.htmlpublic/sw.jssrc/components/Header.tsxsrc/index.csssrc/lib/audio.tssrc/lib/math.tssrc/lib/offlineQueue.tssrc/pages/Cockpit.tsxsrc/pages/RaceControl.tsxsrc/pages/TrackSetup.tsx
| 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(); | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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> |
There was a problem hiding this comment.
🎯 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.
| 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.
| @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'); |
There was a problem hiding this comment.
📐 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.
| @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
| 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 |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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); |
There was a problem hiding this comment.
🎯 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.
| 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.
| <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> |
There was a problem hiding this comment.
📐 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: dodajaria-labeldo obu selectów (typ pojazdu, wybór trasy).src/pages/RaceControl.tsx#L409-L416: dodajaria-labeldo polatype="range"(postęp odtwarzania).src/pages/RaceControl.tsx#L480-L491: powiąż<label>z selectami przezhtmlFor/id(lub dodajaria-labeldo 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-L416src/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
| <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> |
There was a problem hiding this comment.
🗄️ 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.
| <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.
| 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; | ||
| }); |
There was a problem hiding this comment.
🩺 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:
- 1: https://react.dev/reference/react/StrictMode
- 2: React.StrictMode causes setState to fire twice react/react#12856
- 3: Ensure useState and useReducer initializer functions are double invoked in StrictMode react/react#28248
- 4: https://react.dev/learn/keeping-components-pure
- 5: https://react.dev/reference/rules/components-and-hooks-must-be-pure
- 6: https://react.dev/blog/2024/04/25/react-19-upgrade-guide
- 7: https://github.com/facebook/react/blob/3cb2c420/CHANGELOG.md
🏁 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.tsxRepository: 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
|
|
||
| if (currentMode === 's1') return 's2'; | ||
| if (currentMode === 's2') return 'draw'; | ||
| return currentMode; | ||
| }); |
There was a problem hiding this comment.
🎯 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.
| 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]); |
There was a problem hiding this comment.
🎯 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.
| 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.
Summary by CodeRabbit
Nowe funkcje
Ulepszenia
Poprawki