Skip to content

Conversation

@Freika
Copy link
Owner

@Freika Freika commented Aug 1, 2025

Summary by CodeRabbit

  • New Features

    • Introduced real-time Live Mode for map updates with improved performance and memory efficiency.
    • Added centralized marker creation for consistent styling and behavior.
    • Enhanced photo and visits layers with improved loading, display, and error handling.
    • Improved scratch map and fog of war layers for better interactivity and performance.
  • Bug Fixes

    • Restored Photos, Suggested Visits, and Confirmed Visits layers on the map page.
    • Fixed Fog of War and Areas layer behavior.
    • Eliminated memory leaks in Live map mode.
  • Tests

    • Added comprehensive end-to-end tests for map features, Live Mode, marker factory, and memory leak prevention.
  • Chores

    • Updated documentation and changelog to reflect new release and fixes.
    • Added Playwright test configuration for automated testing.
  • Style

    • Improved marker and map layer styling for a more consistent user experience.

@coderabbitai
Copy link

coderabbitai bot commented Aug 1, 2025

Walkthrough

This update refactors the map's live mode, marker creation, and photo/visits layer management for improved modularity, performance, and memory efficiency. It introduces new JavaScript modules for live map handling, marker factory, and photo display, with corresponding updates to controllers and helpers. Comprehensive end-to-end Playwright test suites and configuration are added. Minor Ruby test and documentation updates are included.

Changes

Cohort / File(s) Change Summary
Version & Changelog
.app_version, CHANGELOG.md
Incremented app version to 0.30.7 and updated changelog with new release notes and fixes.
Maps Controller Refactor
app/javascript/controllers/maps_controller.js
Refactored to delegate live map updates to LiveMapHandler, modularized layer management for scratch map, photos, visits, and fog, improved event handling, and updated method signatures.
Trips Controller Import Update
app/javascript/controllers/trips_controller.js
Refactored imports for photo fetching to use new photos.js module.
Fog of War Overlay
app/javascript/maps/fog_of_war.js
Refactored overlay to use function expressions, maintain controller/map state, add updateFog method, and improve event handling.
Photos Layer Refactor
app/javascript/maps/helpers.js, app/javascript/maps/photos.js
Moved all photo fetching/marker logic from helpers.js to new photos.js module with improved structure, error handling, and marker creation.
Live Map Handler
app/javascript/maps/live_map_handler.js
Added new LiveMapHandler class to manage real-time GPS streaming, incremental marker/polyline updates, and memory-bounded data structures.
Marker Factory Refactor
app/javascript/maps/marker_factory.js, app/javascript/maps/markers.js
Introduced centralized marker factory for consistent, memory-safe marker creation; refactored marker creation logic in markers.js to use factory functions.
Visits Layer Refactor
app/javascript/maps/visits.js
Decoupled visits data/layer management from drawer UI, added createMapCircles method, and improved logging.
End-to-End Tests and Config
e2e/live-map-handler.spec.js, e2e/live-mode.spec.js, e2e/map.spec.js, e2e/marker-factory.spec.js, e2e/memory-leak-fix.spec.js, playwright.config.js
Added comprehensive Playwright test suites and configuration for map, live mode, marker factory, and memory leak validation.
Ruby Spec Minor Update
spec/services/tracks/track_builder_spec.rb
Relaxed duration assertion to allow 1-second tolerance in integration test.
Map Controller Internal Fix
app/controllers/map_controller.rb
Changed pluck attribute from :country to :country_name in private build_coordinates method.

Sequence Diagram(s)

Live Map Point Streaming and Update Flow

sequenceDiagram
    participant User
    participant UI (Map Page)
    participant MapsController
    participant LiveMapHandler
    participant LeafletMap

    User->>UI (Map Page): Enables Live Mode
    UI (Map Page)->>MapsController: Initialize LiveMapHandler
    MapsController->>LiveMapHandler: enable()
    loop For each incoming GPS point
        MapsController->>LiveMapHandler: appendPoint(point)
        LiveMapHandler->>LeafletMap: Add marker, update heatmap, polylines, fog
    end
    User->>UI (Map Page): Disables Live Mode
    MapsController->>LiveMapHandler: disable()
    LiveMapHandler->>LeafletMap: Cleanup last marker
Loading

Photo Layer Fetch and Display

sequenceDiagram
    participant User
    participant MapsController
    participant fetchAndDisplayPhotos
    participant LeafletMap

    User->>MapsController: Enables Photos Layer
    MapsController->>fetchAndDisplayPhotos: fetchAndDisplayPhotos(params)
    fetchAndDisplayPhotos->>LeafletMap: Show loading spinner
    fetchAndDisplayPhotos->>fetchAndDisplayPhotos: Fetch photo metadata
    fetchAndDisplayPhotos->>LeafletMap: Add photo markers with popups
    fetchAndDisplayPhotos->>LeafletMap: Remove loading spinner
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~80 minutes

Poem

🗺️
In fields of code, a bunny hops,
Refactoring maps with memory props.
Markers now crafted with factory care,
Live points stream with nary a scare.
Photos and visits, modular delight,
Playwrights test deep into the night.
Version bumped—release feels right!
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5224dae and 559e7c2.

📒 Files selected for processing (1)
  • app/controllers/map_controller.rb (1 hunks)
🔇 Additional comments (1)
app/controllers/map_controller.rb (1)

24-24: country_name attribute confirmed
The points table schema includes a country_name column and the Point model implements a country_name method that falls back to the legacy country or associated Country name. No further changes are required here.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@cursor
Copy link

cursor bot commented Aug 1, 2025

Bugbot found 1 bug

To see it, activate your membership in the Cursor dashboard.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

♻️ Duplicate comments (1)
e2e/marker-factory.spec.js (1)

32-32: Same hard-coded date issue as other test files.

Consider using dynamic dates for better test reliability across different environments and time periods.

🧹 Nitpick comments (21)
app/javascript/controllers/trips_controller.js (1)

10-11: Import path update LGTM, but keep quote style consistent

The new imports are correct and necessary for the refactor.
Minor style nit: surrounding imports in this file use double-quotes, while the two new lines use single-quotes. Aligning on one quote style keeps the file tidy.

-import { showFlashMessage } from '../maps/helpers';
-import { fetchAndDisplayPhotos } from '../maps/photos';
+import { showFlashMessage } from "../maps/helpers";
+import { fetchAndDisplayPhotos } from "../maps/photos";
e2e/live-map-handler.spec.js (1)

31-31: Consider using dynamic date ranges for better test reliability.

The hard-coded date range 2025-06-04 may become stale over time and could cause tests to fail when no data exists for that specific date.

Consider using dynamic date ranges based on the current date:

-    await page.goto('/map?start_at=2025-06-04T00:00&end_at=2025-06-04T23:59');
+    const today = new Date();
+    const startDate = today.toISOString().split('T')[0];
+    await page.goto(`/map?start_at=${startDate}T00:00&end_at=${startDate}T23:59`);
e2e/memory-leak-fix.spec.js (1)

33-33: Use dynamic date ranges for test reliability.

Same issue as the previous test file - hard-coded dates may cause test failures when no data exists for that specific date.

Apply the same dynamic date solution:

-    await page.goto('/map?start_at=2025-06-04T00:00&end_at=2025-06-04T23:59');
+    const today = new Date();
+    const startDate = today.toISOString().split('T')[0];
+    await page.goto(`/map?start_at=${startDate}T00:00&end_at=${startDate}T23:59`);
app/javascript/maps/markers.js (1)

32-36: Distance calculation is acceptable but could be improved.

The simplified distance calculation using Euclidean distance is a reasonable performance optimization for marker simplification. However, the TODO comment suggests haversine distance was considered.

The current implementation is adequate for marker filtering, but if more accurate distance calculation is needed in the future, consider implementing a simple haversine approximation:

-    // Note: haversineDistance function would need to be imported or implemented
-    // For now, using simple distance calculation
-    const latDiff = currLat - prevLat;
-    const lngDiff = currLon - prevLon;
-    const distance = Math.sqrt(latDiff * latDiff + lngDiff * lngDiff) * 111000; // Rough conversion to meters
+    // Simple haversine approximation for better accuracy
+    const toRad = (deg) => deg * (Math.PI / 180);
+    const dLat = toRad(currLat - prevLat);
+    const dLng = toRad(currLon - prevLon);
+    const a = Math.sin(dLat/2) * Math.sin(dLat/2) + 
+              Math.cos(toRad(prevLat)) * Math.cos(toRad(currLat)) * 
+              Math.sin(dLng/2) * Math.sin(dLng/2);
+    const distance = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * 6371000; // Earth radius in meters
app/javascript/maps/live_map_handler.js (3)

36-36: Fix typo in property name

The property name fogLinethreshold appears to be a typo.

-    this.fogLinethreshold = options.fogLinethreshold || 10;
+    this.fogLineThreshold = options.fogLineThreshold || 10;

206-210: Make polyline styling configurable

The polyline color and weight are hardcoded, limiting customization options.

Add configuration options and use them:

+    this.polylineColor = options.polylineColor || '#3388ff';
+    this.polylineWeight = options.polylineWeight || 3;

       const newSegment = L.polyline([
         [prevPoint[0], prevPoint[1]],
         [newPoint[0], newPoint[1]]
       ], {
-        color: this.routeOpacity > 0 ? '#3388ff' : 'transparent',
-        weight: 3,
+        color: this.routeOpacity > 0 ? this.polylineColor : 'transparent',
+        weight: this.polylineWeight,
         opacity: this.routeOpacity
       });

221-227: Implement fog of war update logic

The fog of war update is currently just a placeholder with no actual implementation.

The method needs to implement the actual fog clearing logic. Would you like me to help implement this feature or create an issue to track it?

   _updateFogOfWar() {
     if (this.map.hasLayer(this.fogOverlay)) {
-      // This would need to be implemented based on the existing fog logic
-      // For now, we'll just log that it needs updating
-      console.log('LiveMapHandler: Fog of war update needed');
+      // TODO: Implement fog of war clearing logic
+      // - Clear fog around the new point using this.clearFogRadius
+      // - Update fog overlay visualization
+      console.warn('LiveMapHandler: Fog of war update not yet implemented');
     }
   }
app/javascript/maps/visits.js (3)

494-499: Improve user messaging and remove debug logging

The placeholder message could be more helpful, and debug console.log should be removed.

-      console.log('Drawer opened - showing placeholder message');
       // Just show a placeholder message in the drawer, don't fetch visits
       const container = document.getElementById('visits-list');
       if (container) {
-        container.innerHTML = '<p class="text-gray-500">Enable "Suggested Visits" or "Confirmed Visits" layers to see visits data</p>';
+        container.innerHTML = `
+          <div class="text-gray-500 text-center p-4">
+            <p class="mb-2">No visits data loaded</p>
+            <p class="text-sm">Enable "Suggested Visits" or "Confirmed Visits" layers from the map controls to view visits.</p>
+          </div>
+        `;
       }

540-541: Remove debug logging statements

Multiple console.log statements should be removed before merging to production.

Remove all the console.log statements or replace with a proper logging solution that can be toggled via configuration.

Also applies to: 556-557, 574-575, 578-581, 597-597


613-614: Remove excessive debug logging

The createMapCircles method has too many console.log statements that should be removed.

Remove all console.log statements or wrap them in a development-only check:

-      console.log('No visits to create circles for');
+      if (window.DEBUG) console.debug('No visits to create circles for');

Also applies to: 618-619, 622-624, 633-641, 658-659, 662-663, 668-668, 672-673

app/javascript/maps/marker_factory.js (2)

20-27: Add parameter validation for robustness

The function should validate inputs to prevent invalid CSS or negative sizes.

 export function createStandardIcon(color = 'blue', size = 8) {
+  // Validate size
+  if (size <= 0 || !Number.isFinite(size)) {
+    console.warn('Invalid marker size, using default');
+    size = 8;
+  }
+  
   return L.divIcon({
     className: 'custom-div-icon',
     html: `<div style='background-color: ${color}; width: ${size}px; height: ${size}px; border-radius: 50%;'></div>`,
     iconSize: [size, size],
     iconAnchor: [size / 2, size / 2]
   });
 }

231-240: Consider using constants for array indices

The marker data array uses hardcoded indices which reduces maintainability.

Define constants at the module level:

// At the top of the file
const MARKER_DATA_INDICES = {
  LATITUDE: 0,
  LONGITUDE: 1,
  BATTERY: 2,
  ALTITUDE: 3,
  TIMESTAMP: 4,
  VELOCITY: 5,
  ID: 6,
  COUNTRY: 7
};

// Then use them in the code:
const updatedMarkerData = [
  parseFloat(data.latitude),
  parseFloat(data.longitude),
  originalMarkerData[MARKER_DATA_INDICES.BATTERY],
  originalMarkerData[MARKER_DATA_INDICES.ALTITUDE],
  // etc...
];
e2e/map.spec.js (5)

191-191: Remove console.log statements from tests

Console logs should be avoided in test files. Use Playwright's test reporter features instead if debugging information is needed.

-        console.log(`Stats control displays: ${distance} ${unit} | ${points} points`);

591-591: Remove debugging console.log statements

Multiple console.log statements are present throughout the settings panel tests. These should be removed for production test suites.

Remove all console.log statements:

-      console.log(`Settings panel closed after submission: ${isPanelClosed}`);
-      console.log(`Fog settings panel closed after submission: ${isPanelClosed}`);
-      console.log('Panel stayed open after submission - testing persistence directly');
-      console.log('Fog panel stayed open after submission - testing persistence directly');
-      console.log(`Points rendering panel closed after submission: ${isPanelClosed}`);
-      console.log('Points panel stayed open after submission - testing persistence directly');

Also applies to: 675-675, 679-679, 865-865, 869-869


1432-1439: Performance test might be too lenient

A 15-second timeout for page load might be too generous and could hide performance regressions.

Consider:

  1. Setting a more aggressive target (e.g., 5 seconds)
  2. Adding performance metrics collection
  3. Testing under different network conditions
     test('should load within reasonable time', async () => {
       const startTime = Date.now();
+      const metrics = [];

       await page.goto('/map');
       await page.waitForSelector('.leaflet-container', { timeout: 15000 });

       const loadTime = Date.now() - startTime;
-      expect(loadTime).toBeLessThan(15000); // Should load within 15 seconds
+      expect(loadTime).toBeLessThan(5000); // Should load within 5 seconds
+      
+      // Collect additional metrics
+      const performanceMetrics = await page.evaluate(() => 
+        JSON.stringify(performance.getEntriesByType('navigation')[0])
+      );
+      console.info('Performance metrics:', performanceMetrics);
     });

1667-1667: Remove console.log from error handling test

-      console.log(`Console errors detected: ${consoleErrors.length}`);
+      // Verify errors were detected and handled
+      expect(consoleErrors.length).toBeGreaterThan(0);

1-1671: Consider splitting this large test file for better maintainability

This test file is 1671 lines long, which makes it difficult to maintain and navigate. Consider splitting it into smaller, focused test files.

Split the test file by functionality:

  • e2e/map/core-display.spec.js - Core map display tests
  • e2e/map/navigation.spec.js - Date and time navigation tests
  • e2e/map/layer-controls.spec.js - Layer control tests
  • e2e/map/settings.spec.js - Settings panel tests
  • e2e/map/calendar.spec.js - Calendar panel tests
  • e2e/map/interactive-elements.spec.js - Interactive map elements
  • e2e/map/performance.spec.js - Performance and loading tests
  • e2e/map/accessibility.spec.js - Accessibility tests
  • e2e/map/error-handling.spec.js - Error handling tests

Each file can share common setup through a base configuration or page object model pattern.

app/javascript/controllers/maps_controller.js (2)

122-124: Remove commented-out dead code

These commented lines are duplicates of the active code above and serve no purpose. Dead code should be removed to improve maintainability.

         let distance = parseInt(this.element.dataset.distance) || 0;
         const pointsNumber = this.element.dataset.points_number || '0';
-        // Original stats data loading disabled:
-        // let distance = parseInt(this.element.dataset.distance) || 0;
-        // const pointsNumber = this.element.dataset.points_number || '0';

422-476: Consider refactoring to reduce code duplication

The world borders fetching logic (lines 445-458) duplicates the same logic from setupScratchLayer (lines 372-385). Consider extracting this into a separate method to follow DRY principles.

+  async fetchWorldBordersData() {
+    if (this.worldBordersData) {
+      return this.worldBordersData;
+    }
+
+    const response = await fetch('/api/v1/countries/borders.json', {
+      headers: {
+        'Accept': 'application/geo+json,application/json'
+      }
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    this.worldBordersData = await response.json();
+    return this.worldBordersData;
+  }
+
   async refreshScratchLayer() {
     console.log('Refreshing scratch layer with current data');

     if (!this.scratchLayer) {
       console.log('Scratch layer not initialized, setting up');
       await this.setupScratchLayer(this.countryCodesMap);
       return;
     }

     try {
       // Clear existing data
       this.scratchLayer.clearLayers();

       // Get current visited countries based on current markers
       const visitedCountries = this.getVisitedCountries(this.countryCodesMap);
       console.log('Current visited countries:', visitedCountries);

       if (visitedCountries.length === 0) {
         console.log('No visited countries found');
         return;
       }

-      // Fetch country borders data (reuse if already loaded)
-      if (!this.worldBordersData) {
-        console.log('Loading world borders data');
-        const response = await fetch('/api/v1/countries/borders.json', {
-          headers: {
-            'Accept': 'application/geo+json,application/json'
-          }
-        });
-
-        if (!response.ok) {
-          throw new Error(`HTTP error! status: ${response.status}`);
-        }
-
-        this.worldBordersData = await response.json();
-      }
+      // Fetch country borders data
+      console.log('Loading world borders data');
+      const worldData = await this.fetchWorldBordersData();

       // Filter for visited countries
-      const filteredFeatures = this.worldBordersData.features.filter(feature =>
+      const filteredFeatures = worldData.features.filter(feature =>
         visitedCountries.includes(feature.properties["ISO3166-1-Alpha-2"])
       );

Then update setupScratchLayer to use the same method.

e2e/live-mode.spec.js (2)

407-415: Document network simulation requirements

The network simulation might fail in certain test environments. Consider adding a comment about the requirements or making this test conditional.

       // Test connection resilience by simulating various network conditions
+      // Note: Network simulation requires Playwright to be running with appropriate permissions
+      // and may not work in all CI environments (e.g., Docker containers without network capabilities)
       try {
         // Simulate brief network interruption
         await page.context().setOffline(true);

36-37: Consider parameterizing test dates

The tests use hardcoded future dates (2025-06-04) which will eventually become past dates. Consider using relative dates or parameterizing these values.

+  // Use a date far enough in the future to avoid conflicts with real data
+  const TEST_DATE = new Date();
+  TEST_DATE.setFullYear(TEST_DATE.getFullYear() + 10);
+  const TEST_DATE_STRING = TEST_DATE.toISOString().split('T')[0];
+
   test.beforeEach(async () => {
-    // Navigate to June 4, 2025 where we have test data
-    await page.goto('/map?start_at=2025-06-04T00:00&end_at=2025-06-04T23:59');
+    // Navigate to test date where we have test data
+    await page.goto(`/map?start_at=${TEST_DATE_STRING}T00:00&end_at=${TEST_DATE_STRING}T23:59`);

Also applies to: 226-226, 472-476

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c4e6b0 and 67eb333.

📒 Files selected for processing (18)
  • .app_version (1 hunks)
  • CHANGELOG.md (1 hunks)
  • app/javascript/controllers/maps_controller.js (22 hunks)
  • app/javascript/controllers/trips_controller.js (1 hunks)
  • app/javascript/maps/fog_of_war.js (1 hunks)
  • app/javascript/maps/helpers.js (1 hunks)
  • app/javascript/maps/live_map_handler.js (1 hunks)
  • app/javascript/maps/marker_factory.js (1 hunks)
  • app/javascript/maps/markers.js (3 hunks)
  • app/javascript/maps/photos.js (1 hunks)
  • app/javascript/maps/visits.js (8 hunks)
  • e2e/live-map-handler.spec.js (1 hunks)
  • e2e/live-mode.spec.js (1 hunks)
  • e2e/map.spec.js (1 hunks)
  • e2e/marker-factory.spec.js (1 hunks)
  • e2e/memory-leak-fix.spec.js (1 hunks)
  • playwright.config.js (1 hunks)
  • spec/services/tracks/track_builder_spec.rb (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
app/javascript/maps/fog_of_war.js (1)
app/javascript/controllers/maps_controller.js (4)
  • mapElement (1092-1092)
  • controller (1169-1169)
  • controller (1206-1206)
  • fog (760-760)
e2e/marker-factory.spec.js (1)
app/javascript/maps/marker_factory.js (2)
  • marker (71-81)
  • marker (105-109)
app/javascript/maps/markers.js (1)
app/javascript/maps/marker_factory.js (4)
  • marker (71-81)
  • marker (105-109)
  • createInteractiveMarker (65-90)
  • createSimplifiedMarker (100-128)
🪛 Biome (2.1.2)
app/javascript/maps/photos.js

[error] 105-105: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 108-108: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 111-114: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 115-115: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (23)
playwright.config.js (1)

1-51: Well-structured Playwright configuration with good CI/local environment handling.

The configuration follows Playwright best practices with proper test isolation, artifact collection, and Rails server integration. The CI-specific settings (single worker, retries, forbidOnly) are appropriate for stable test execution.

app/javascript/maps/helpers.js (1)

202-202: Good modularization by moving photo functionality to dedicated module.

The removal of photo-specific functions from generic helpers and their relocation to app/javascript/maps/photos.js improves code organization and maintainability. The remaining utility functions are appropriately scoped for general use.

app/javascript/maps/fog_of_war.js (4)

107-125: Excellent refactoring with improved state management and immediate initialization.

The transition from arrow functions to traditional function expressions enables proper this context binding for storing map and controller references. The immediate fog drawing when markers exist is a good UX improvement.


128-142: Proper event handler management with named function reference.

Storing the resize handler as this._onResize enables controlled cleanup and prevents memory leaks. The conditional redraw logic is appropriate for maintaining fog state across resize events.


145-155: Thorough cleanup in onRemove method.

The explicit removal of the resize event listener prevents memory leaks, which aligns with the PR's goal of eliminating memory leaks in live map functionality.


157-162: Well-designed public API for external fog updates.

The new updateFog method provides a clean interface for external components to trigger fog redraws, supporting the refactored layer management approach mentioned in the AI summary.

.app_version (1)

1-1: Appropriate version increment for this release.

The version bump to 0.30.7 properly reflects the scope of changes including map refactoring, memory leak fixes, and comprehensive E2E test additions.

spec/services/tracks/track_builder_spec.rb (1)

326-326: Good improvement to test reliability with appropriate tolerance.

Changing from exact equality to a 1-second tolerance for duration checks is a sensible improvement for integration tests, accommodating minor timing variations that can occur during test execution without compromising test validity.

CHANGELOG.md (1)

7-22: Changelog entry looks fine

Entry provides clear, granular notes for 0.30.7; no issues spotted.

app/javascript/maps/markers.js (2)

1-1: Good refactoring to use marker factory pattern.

The import and delegation to marker_factory functions centralizes marker creation logic and improves maintainability.


46-48: Excellent use of factory delegation.

The delegation to createSimplifiedMarker with userSettings parameter maintains consistency with the factory pattern and ensures proper marker configuration.

e2e/marker-factory.spec.js (3)

80-85: Excellent marker consistency validation logic.

The CSS property validation ensures markers have consistent styling (border radius, colors, dimensions) which is crucial for UI consistency.


113-129: Good use of performance.memory API for memory monitoring.

The memory monitoring using window.performance.memory provides valuable insights into memory usage patterns, which is important for the memory leak fixes being tested.


150-179: Outstanding documentation of refactoring benefits.

This test case serves as excellent living documentation of the marker factory refactoring benefits, including code reuse, memory safety, maintenance, and performance improvements.

app/javascript/maps/photos.js (4)

5-7: Excellent retry mechanism implementation.

The retry logic with exponential backoff and maximum retry attempts provides robust error handling for network requests.


48-66: Well-structured asynchronous photo loading.

The Promise-based approach for loading photo thumbnails with proper error handling ensures that failed photo loads don't block the entire operation.


20-29: Clean loading control implementation.

The custom Leaflet control for showing loading state provides good user feedback during photo fetching operations.


170-185: Rich popup content with good user experience.

The popup content includes comprehensive photo metadata, hover effects, and proper external links. The styling and interaction design enhances usability.

app/javascript/maps/live_map_handler.js (1)

160-170: Well-implemented memory management

The sliding window approach correctly limits memory usage by removing the oldest points and their corresponding markers.

app/javascript/maps/visits.js (2)

236-238: Good architectural improvement

Decoupling visits data fetching from drawer UI state improves separation of concerns and makes the layer control the single source of truth for visibility.


675-681: Good separation of concerns

Splitting display logic into createMapCircles for map rendering and displayVisits for UI updates improves modularity and testability.

app/javascript/maps/marker_factory.js (2)

37-52: Well-designed for live streaming efficiency

The function correctly creates lightweight markers without heavy features, perfect for high-frequency updates in live mode.


65-90: Excellent implementation of interactive markers

The function properly creates feature-rich markers with all necessary metadata and delegates complex drag handling to a dedicated function, following good separation of concerns.

Comment on lines +17 to +23
constructor(map, layers, options = {}) {
this.map = map;
this.markersLayer = layers.markersLayer;
this.polylinesLayer = layers.polylinesLayer;
this.heatmapLayer = layers.heatmapLayer;
this.fogOverlay = layers.fogOverlay;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add parameter validation for required arguments

The constructor should validate that required parameters are provided to fail fast with clear error messages.

Add validation at the beginning of the constructor:

 constructor(map, layers, options = {}) {
+  if (!map) {
+    throw new Error('LiveMapHandler: map parameter is required');
+  }
+  if (!layers || typeof layers !== 'object') {
+    throw new Error('LiveMapHandler: layers parameter must be an object');
+  }
+  
   this.map = map;
   this.markersLayer = layers.markersLayer;
   this.polylinesLayer = layers.polylinesLayer;
   this.heatmapLayer = layers.heatmapLayer;
   this.fogOverlay = layers.fogOverlay;
📝 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
constructor(map, layers, options = {}) {
this.map = map;
this.markersLayer = layers.markersLayer;
this.polylinesLayer = layers.polylinesLayer;
this.heatmapLayer = layers.heatmapLayer;
this.fogOverlay = layers.fogOverlay;
constructor(map, layers, options = {}) {
if (!map) {
throw new Error('LiveMapHandler: map parameter is required');
}
if (!layers || typeof layers !== 'object') {
throw new Error('LiveMapHandler: layers parameter must be an object');
}
this.map = map;
this.markersLayer = layers.markersLayer;
this.polylinesLayer = layers.polylinesLayer;
this.heatmapLayer = layers.heatmapLayer;
this.fogOverlay = layers.fogOverlay;
🤖 Prompt for AI Agents
In app/javascript/maps/live_map_handler.js around lines 17 to 23, the
constructor lacks validation for required parameters. Add checks at the start of
the constructor to verify that 'map' and 'layers' are provided and not null or
undefined. If any required parameter is missing, throw an error with a clear
message indicating which parameter is missing to ensure the code fails fast and
aids debugging.

Comment on lines +83 to +87
// Parse the received point data
const newPoint = data;

// Add the new point to the markers array
this.markers.push(newPoint);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Validate and clone point data to prevent mutations

The point data is assigned directly without validation or cloning, which could lead to unexpected mutations.

   // Parse the received point data
-  const newPoint = data;
+  if (!Array.isArray(data) || data.length < 2) {
+    console.error('LiveMapHandler: Invalid point data format');
+    return;
+  }
+  // Clone the array to prevent external mutations
+  const newPoint = [...data];
📝 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
// Parse the received point data
const newPoint = data;
// Add the new point to the markers array
this.markers.push(newPoint);
// Parse the received point data
if (!Array.isArray(data) || data.length < 2) {
console.error('LiveMapHandler: Invalid point data format');
return;
}
// Clone the array to prevent external mutations
const newPoint = [...data];
// Add the new point to the markers array
this.markers.push(newPoint);
🤖 Prompt for AI Agents
In app/javascript/maps/live_map_handler.js around lines 83 to 87, the point data
is assigned directly from the input without validation or cloning, risking
unintended mutations. To fix this, validate the structure and content of the
incoming point data to ensure it meets expected criteria, then create a deep
clone of the point object before adding it to the markers array to prevent
external mutations from affecting internal state.

this._updatePolylines(newPoint);

// Pan map to new location
this.map.setView([newPoint[0], newPoint[1]], 16);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make map zoom level configurable

The zoom level is hardcoded to 16, which should be configurable for different use cases.

+    // Configuration options
+    this.maxPoints = options.maxPoints || 1000;
+    this.routeOpacity = options.routeOpacity || 1;
+    this.timezone = options.timezone || 'UTC';
+    this.distanceUnit = options.distanceUnit || 'km';
+    this.userSettings = options.userSettings || {};
+    this.clearFogRadius = options.clearFogRadius || 100;
+    this.fogLineThreshold = options.fogLineThreshold || 10;
+    this.liveViewZoom = options.liveViewZoom || 16;

     // Pan map to new location
-    this.map.setView([newPoint[0], newPoint[1]], 16);
+    this.map.setView([newPoint[0], newPoint[1]], this.liveViewZoom);
📝 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
this.map.setView([newPoint[0], newPoint[1]], 16);
// Configuration options
this.maxPoints = options.maxPoints || 1000;
this.routeOpacity = options.routeOpacity || 1;
this.timezone = options.timezone || 'UTC';
this.distanceUnit = options.distanceUnit || 'km';
this.userSettings = options.userSettings || {};
this.clearFogRadius = options.clearFogRadius || 100;
this.fogLineThreshold = options.fogLineThreshold || 10;
this.liveViewZoom = options.liveViewZoom || 16;
// Pan map to new location
this.map.setView([newPoint[0], newPoint[1]], this.liveViewZoom);
🤖 Prompt for AI Agents
In app/javascript/maps/live_map_handler.js at line 104, the map zoom level is
hardcoded to 16. Modify the code to accept the zoom level as a configurable
parameter or option, allowing different zoom levels to be set dynamically
instead of using the fixed value 16.

Comment on lines +233 to +245
_updateLastMarker() {
// Remove previous last marker
if (this.lastMarkerRef) {
this.map.removeLayer(this.lastMarkerRef);
}

// Add new last marker and store reference
if (this.markers.length > 0) {
const lastPoint = this.markers[this.markers.length - 1];
const lastMarker = L.marker([lastPoint[0], lastPoint[1]]);
this.lastMarkerRef = lastMarker.addTo(this.map);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use marker factory and add distinctive styling for last marker

The last marker should use the marker factory for consistency and have distinctive styling to indicate it's the current position.

   _updateLastMarker() {
     // Remove previous last marker
     if (this.lastMarkerRef) {
       this.map.removeLayer(this.lastMarkerRef);
     }

     // Add new last marker and store reference
-    if (this.markers.length > 0) {
+    if (this.markers && this.markers.length > 0) {
       const lastPoint = this.markers[this.markers.length - 1];
-      const lastMarker = L.marker([lastPoint[0], lastPoint[1]]);
+      // Use marker factory with special styling for last marker
+      const lastMarker = createLiveMarker(lastPoint, {
+        size: 12, // Larger size for visibility
+        className: 'last-marker-icon'
+      });
+      // Add pulsing animation or special styling
+      lastMarker.options.icon.options.className += ' pulse-animation';
       this.lastMarkerRef = lastMarker.addTo(this.map);
     }
   }
📝 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
_updateLastMarker() {
// Remove previous last marker
if (this.lastMarkerRef) {
this.map.removeLayer(this.lastMarkerRef);
}
// Add new last marker and store reference
if (this.markers.length > 0) {
const lastPoint = this.markers[this.markers.length - 1];
const lastMarker = L.marker([lastPoint[0], lastPoint[1]]);
this.lastMarkerRef = lastMarker.addTo(this.map);
}
}
_updateLastMarker() {
// Remove previous last marker
if (this.lastMarkerRef) {
this.map.removeLayer(this.lastMarkerRef);
}
// Add new last marker and store reference
if (this.markers && this.markers.length > 0) {
const lastPoint = this.markers[this.markers.length - 1];
// Use marker factory with special styling for last marker
const lastMarker = createLiveMarker(lastPoint, {
size: 12, // Larger size for visibility
className: 'last-marker-icon'
});
// Add pulsing animation or special styling
lastMarker.options.icon.options.className += ' pulse-animation';
this.lastMarkerRef = lastMarker.addTo(this.map);
}
}
🤖 Prompt for AI Agents
In app/javascript/maps/live_map_handler.js around lines 233 to 245, the
_updateLastMarker method creates the last marker directly instead of using the
marker factory, and it lacks distinctive styling. Refactor the code to create
the last marker using the existing marker factory method and apply a unique
style or icon to visually distinguish it as the current position marker.

Comment on lines 144 to 192
marker.on('drag', function(e) {
const newLatLng = e.target.getLatLng();
const map = e.target._map;
const pointIndex = e.target.options.pointIndex;
const originalLat = e.target.options.originalLat;
const originalLng = e.target.options.originalLng;

// Find polylines by iterating through all map layers
map.eachLayer((layer) => {
// Check if this is a LayerGroup containing polylines
if (layer instanceof L.LayerGroup) {
layer.eachLayer((featureGroup) => {
if (featureGroup instanceof L.FeatureGroup) {
featureGroup.eachLayer((segment) => {
if (segment instanceof L.Polyline) {
const coords = segment.getLatLngs();
const tolerance = 0.0000001;
let updated = false;

// Check and update start point
if (Math.abs(coords[0].lat - originalLat) < tolerance &&
Math.abs(coords[0].lng - originalLng) < tolerance) {
coords[0] = newLatLng;
updated = true;
}

// Check and update end point
if (Math.abs(coords[1].lat - originalLat) < tolerance &&
Math.abs(coords[1].lng - originalLng) < tolerance) {
coords[1] = newLatLng;
updated = true;
}

// Only update if we found a matching endpoint
if (updated) {
segment.setLatLngs(coords);
segment.redraw();
}
}
});
}
});
}
});

// Update the marker's original position for the next drag event
e.target.options.originalLat = newLatLng.lat;
e.target.options.originalLng = newLatLng.lng;
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize polyline update performance and improve tolerance handling

The nested iteration through all map layers could impact performance with many layers, and the tolerance value might be too restrictive.

   marker.on('drag', function(e) {
     const newLatLng = e.target.getLatLng();
     const map = e.target._map;
     const pointIndex = e.target.options.pointIndex;
     const originalLat = e.target.options.originalLat;
     const originalLng = e.target.options.originalLng;
+    
+    // Use a more reasonable tolerance for GPS coordinates
+    const tolerance = 0.000001; // ~0.1 meter precision
+    
+    // Cache polyline layers for better performance
+    if (!map._polylineLayers) {
+      map._polylineLayers = [];
+      map.eachLayer((layer) => {
+        if (layer instanceof L.LayerGroup) {
+          layer.eachLayer((subLayer) => {
+            if (subLayer instanceof L.Polyline || 
+                (subLayer instanceof L.FeatureGroup && subLayer.getLayers().some(l => l instanceof L.Polyline))) {
+              map._polylineLayers.push(subLayer);
+            }
+          });
+        }
+      });
+    }
     
-    // Find polylines by iterating through all map layers
-    map.eachLayer((layer) => {
-      // Check if this is a LayerGroup containing polylines
-      if (layer instanceof L.LayerGroup) {
+    // Update polylines using cached layers
+    map._polylineLayers.forEach((layer) => {
         layer.eachLayer((featureGroup) => {
           if (featureGroup instanceof L.FeatureGroup) {
             featureGroup.eachLayer((segment) => {
               if (segment instanceof L.Polyline) {
                 const coords = segment.getLatLngs();
-                const tolerance = 0.0000001;
+                if (!coords || coords.length < 2) return;
+                
                 let updated = false;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/javascript/maps/marker_factory.js between lines 144 and 192, the current
code iterates deeply through all map layers and feature groups to find and
update polylines, which can degrade performance with many layers. To fix this,
optimize by maintaining a direct reference or index to relevant polylines
instead of iterating all layers on each drag event. Also, increase the tolerance
value to a more reasonable threshold to better handle floating-point precision
issues when comparing coordinates.

Comment on lines +315 to +320
const url = page.url();
// Allow for timezone differences by checking for current date or next day
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
expect(url.includes(today) || url.includes(tomorrow)).toBe(true);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider timezone-aware date comparisons

The date comparison logic might fail in different timezones. Consider using a more robust approach.

-    // Allow for timezone differences by checking for current date or next day
-    const today = new Date().toISOString().split('T')[0];
-    const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
-    expect(url.includes(today) || url.includes(tomorrow)).toBe(true);
+    // Use URL parsing for more reliable date extraction
+    const urlParams = new URLSearchParams(new URL(url).search);
+    const startDate = urlParams.get('start_at');
+    
+    // Verify it's a date from today (considering timezone)
+    const startDateTime = new Date(startDate);
+    const now = new Date();
+    const diffHours = Math.abs(startDateTime - now) / (1000 * 60 * 60);
+    
+    // Should be within 24 hours of current time
+    expect(diffHours).toBeLessThan(24);
📝 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 url = page.url();
// Allow for timezone differences by checking for current date or next day
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
expect(url.includes(today) || url.includes(tomorrow)).toBe(true);
});
const url = page.url();
// Use URL parsing for more reliable date extraction
const urlParams = new URLSearchParams(new URL(url).search);
const startDate = urlParams.get('start_at');
// Verify it's a date from today (considering timezone)
const startDateTime = new Date(startDate);
const now = new Date();
const diffHours = Math.abs(startDateTime - now) / (1000 * 60 * 60);
// Should be within 24 hours of current time
expect(diffHours).toBeLessThan(24);
});
🤖 Prompt for AI Agents
In e2e/map.spec.js around lines 315 to 320, the current date comparison uses
local time and may fail across different timezones. To fix this, use a
timezone-aware date library or convert dates to UTC consistently before
comparison. Replace the current logic with one that explicitly handles timezone
differences to ensure the test passes reliably regardless of the environment
timezone.

Comment on lines +962 to +1002
// Due to double-event issue causing toggling, force panel to be visible via JavaScript
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'block';
localStorage.setItem('mapPanelOpen', 'true');
console.log('Forced panel to be visible via JavaScript');
}
});

// After forcing visibility, panel should be visible
await expect(panel).toBeVisible();

// Verify panel contains dynamically loaded content
await expect(panel.locator('#year-select')).toBeVisible();
await expect(panel.locator('#months-grid')).toBeVisible();

// Test closing functionality - force panel to be hidden due to double-event issue
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'none';
localStorage.setItem('mapPanelOpen', 'false');
console.log('Forced panel to be hidden via JavaScript');
}
});

// Panel should be hidden (but may still exist in DOM for performance)
const finalVisible = await panel.isVisible();
expect(finalVisible).toBe(false);

// Test toggle functionality works both ways - force panel to be visible again
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'block';
localStorage.setItem('mapPanelOpen', 'true');
console.log('Forced panel to be visible again via JavaScript');
}
});
await expect(panel).toBeVisible();
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Consider investigating the root cause of double-event issues

The tests contain multiple workarounds for "double-event issues" that force panel visibility through JavaScript. This approach might mask actual bugs in the application.

Instead of working around the issue, consider:

  1. Investigating why double events are occurring
  2. Fixing the root cause in the application code
  3. Testing the actual user experience without workarounds

If workarounds are necessary for now, add a TODO comment:

+      // TODO: Remove this workaround once double-event issue is fixed in the application
       // Due to double-event issue causing toggling, force panel to be visible via JavaScript

Investigate and remove forced visibility workarounds for the calendar panel
The tests currently force the panel’s display state via JavaScript to work around a “double-event” toggle issue. This approach can mask underlying bugs in the application.

Please address the root cause instead of relying on these workarounds:

  • Track down why two toggle events are firing and fix the event handler logic in the app code.
  • Update your tests to drive and assert the panel’s behavior based on real user interactions (clicks) rather than manually mutating styles.
  • Once the fix is in place, remove the JS‐injected visibility code from the tests.

If you need a temporary note for this workaround, add a TODO above the forced evaluation:

+      // TODO: Remove this workaround once the double-event toggle bug is fixed in the app
       // Due to double-event issue causing toggling, force panel to be visible via JavaScript
       await page.evaluate(() => {
         const panel = document.querySelector('.leaflet-right-panel');
         if (panel) {
           panel.style.display = 'block';
           localStorage.setItem('mapPanelOpen', 'true');
           console.log('Forced panel to be visible via JavaScript');
         }
       });
📝 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
// Due to double-event issue causing toggling, force panel to be visible via JavaScript
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'block';
localStorage.setItem('mapPanelOpen', 'true');
console.log('Forced panel to be visible via JavaScript');
}
});
// After forcing visibility, panel should be visible
await expect(panel).toBeVisible();
// Verify panel contains dynamically loaded content
await expect(panel.locator('#year-select')).toBeVisible();
await expect(panel.locator('#months-grid')).toBeVisible();
// Test closing functionality - force panel to be hidden due to double-event issue
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'none';
localStorage.setItem('mapPanelOpen', 'false');
console.log('Forced panel to be hidden via JavaScript');
}
});
// Panel should be hidden (but may still exist in DOM for performance)
const finalVisible = await panel.isVisible();
expect(finalVisible).toBe(false);
// Test toggle functionality works both ways - force panel to be visible again
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'block';
localStorage.setItem('mapPanelOpen', 'true');
console.log('Forced panel to be visible again via JavaScript');
}
});
await expect(panel).toBeVisible();
// TODO: Remove this workaround once the double-event toggle bug is fixed in the app
// Due to double-event issue causing toggling, force panel to be visible via JavaScript
await page.evaluate(() => {
const panel = document.querySelector('.leaflet-right-panel');
if (panel) {
panel.style.display = 'block';
localStorage.setItem('mapPanelOpen', 'true');
console.log('Forced panel to be visible via JavaScript');
}
});
🤖 Prompt for AI Agents
In e2e/map.spec.js around lines 962 to 1002, the test forcibly sets the calendar
panel's visibility via JavaScript to bypass a double-event toggle issue, which
hides the real problem. Investigate and fix the root cause in the application
code by correcting the event handler logic to prevent duplicate toggle events.
Then, update the tests to simulate actual user interactions like clicks to
toggle the panel and assert its visibility accordingly. After confirming the
fix, remove all forced style manipulations and related JavaScript injections
from the test code. Optionally, add a TODO comment above any temporary forced
visibility code if you keep it during the fix.

await layerControl.click();

// Find and enable Areas layer
const areasCheckbox = page.locator('.leaflet-control-layers-overlays').locator('input').filter({ hasText: /Areas/ }).first();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix deprecated Playwright API usage

The filter method with hasText option is not the recommended approach for text-based filtering in recent Playwright versions.

-      const areasCheckbox = page.locator('.leaflet-control-layers-overlays').locator('input').filter({ hasText: /Areas/ }).first();
+      const areasCheckbox = page.locator('.leaflet-control-layers-overlays label:has-text("Areas") input').first();
📝 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 areasCheckbox = page.locator('.leaflet-control-layers-overlays').locator('input').filter({ hasText: /Areas/ }).first();
const areasCheckbox = page.locator('.leaflet-control-layers-overlays label:has-text("Areas") input').first();
🤖 Prompt for AI Agents
In e2e/map.spec.js at line 1416, the use of the deprecated filter method with
hasText option should be replaced. Instead of using filter({ hasText: /Areas/
}), use the locator method with a text selector or the getByText method to find
the input element containing the text "Areas". Update the code to use the
current recommended Playwright API for text-based filtering.

Comment on lines +74 to +76
expect(codeAnalysis.hasOldClearLayersPattern).toBe(false); // Old inefficient pattern should be gone
expect(codeAnalysis.hasIncrementalMarkerAdd).toBe(true); // New efficient pattern should exist
expect(codeAnalysis.hasBoundedData).toBe(true); // Should have bounded data structures
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Strengthen memory leak verification assertions.

The conditional logic is good, but the assertions could be more comprehensive to catch regressions.

Consider adding more specific memory leak pattern checks:

       // If controller is found, verify the fix
       expect(codeAnalysis.hasOldClearLayersPattern).toBe(false); // Old inefficient pattern should be gone
       expect(codeAnalysis.hasIncrementalMarkerAdd).toBe(true); // New efficient pattern should exist
       expect(codeAnalysis.hasBoundedData).toBe(true); // Should have bounded data structures
+      // Additional checks for memory safety
+      expect(codeAnalysis.methodLength).toBeLessThan(2000); // Method should be reasonably sized
+      expect(codeAnalysis.hasLastMarkerTracking).toBe(true); // Should track markers efficiently
📝 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
expect(codeAnalysis.hasOldClearLayersPattern).toBe(false); // Old inefficient pattern should be gone
expect(codeAnalysis.hasIncrementalMarkerAdd).toBe(true); // New efficient pattern should exist
expect(codeAnalysis.hasBoundedData).toBe(true); // Should have bounded data structures
// If controller is found, verify the fix
expect(codeAnalysis.hasOldClearLayersPattern).toBe(false); // Old inefficient pattern should be gone
expect(codeAnalysis.hasIncrementalMarkerAdd).toBe(true); // New efficient pattern should exist
expect(codeAnalysis.hasBoundedData).toBe(true); // Should have bounded data structures
// Additional checks for memory safety
expect(codeAnalysis.methodLength).toBeLessThan(2000); // Method should be reasonably sized
expect(codeAnalysis.hasLastMarkerTracking).toBe(true); // Should track markers efficiently
🤖 Prompt for AI Agents
In e2e/memory-leak-fix.spec.js around lines 74 to 76, the current assertions
check for some memory leak patterns but are not comprehensive enough to catch
all regressions. Enhance the test by adding more specific assertions that verify
the absence of other known inefficient memory usage patterns and confirm the
presence of all expected efficient patterns. This will strengthen the memory
leak verification and improve test coverage against regressions.

console.log('- Incremental polyline updates');

// Test passes regardless as we've verified the fix is in the source code
expect(true).toBe(true);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Test always passes without meaningful verification.

Similar to the previous file, this test always passes regardless of the actual verification outcome, which diminishes its value.

Implement conditional assertions based on the verification results:

-    // Test passes regardless as we've verified the fix is in the source code
-    expect(true).toBe(true);
+    // Verify that either the efficient patterns are found or old patterns are absent
+    const hasImprovements = hasEfficientImplementation.hasIncrementalAdd || 
+                           hasEfficientImplementation.hasBoundedArrays ||
+                           hasEfficientImplementation.hasEfficientTracking;
+    const lacksOldProblems = !hasEfficientImplementation.hasOldPattern;
+    expect(hasImprovements || lacksOldProblems).toBe(true);
📝 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
expect(true).toBe(true);
// Verify that either the efficient patterns are found or old patterns are absent
const hasImprovements = hasEfficientImplementation.hasIncrementalAdd ||
hasEfficientImplementation.hasBoundedArrays ||
hasEfficientImplementation.hasEfficientTracking;
const lacksOldProblems = !hasEfficientImplementation.hasOldPattern;
expect(hasImprovements || lacksOldProblems).toBe(true);
🤖 Prompt for AI Agents
In e2e/memory-leak-fix.spec.js at line 138, the test currently uses a trivial
assertion that always passes, providing no meaningful verification. Replace the
placeholder expect(true).toBe(true) with assertions that check actual conditions
or results relevant to the test scenario, ensuring the test outcome reflects the
correctness of the code under test.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (7)
app/javascript/maps/visits.js (1)

585-600: Same layer control state checking issue remains unaddressed.

This implementation still accesses the private _layers property and uses fragile string comparisons, which was flagged in previous reviews. The suggested solution was to use map.hasLayer(this.visitCircles) and map.hasLayer(this.confirmedVisitCircles) instead of iterating through internal layer properties.

app/javascript/maps/live_map_handler.js (4)

17-23: Parameter validation still missing.

The constructor lacks validation for required parameters that was suggested in previous reviews. The recommended validation should check that map and layers parameters are provided and valid.


83-84: Point data validation and cloning still missing.

The point data is still assigned directly without validation or cloning, which was flagged in previous reviews. The recommended approach was to validate the data structure and clone the array to prevent mutations.


104-104: Zoom level still hardcoded.

The map zoom level remains hardcoded at 16, which was flagged in previous reviews. The suggested solution was to make this configurable via options.


233-245: Last marker should use factory and distinctive styling.

The _updateLastMarker method still creates markers directly instead of using the marker factory, and lacks distinctive styling to indicate it's the current position, both of which were flagged in previous reviews.

app/javascript/maps/marker_factory.js (2)

162-198: Polyline update performance issue remains unaddressed.

The nested iteration through all map layers during drag events still impacts performance, which was flagged in previous reviews. The suggested optimization was to cache polyline layers and use a more reasonable tolerance value.


267-270: Alert-based error notification still needs improvement.

The use of alert() for error notification still blocks the UI thread, which was flagged in previous reviews. The recommended approach was to use a non-blocking notification system.

🧹 Nitpick comments (1)
app/javascript/maps/visits.js (1)

614-676: Good separation of concerns, but optimize logging and performance.

The extraction of map circle creation is excellent for maintainability. However, consider these improvements:

  1. Conditional logging: Wrap console.log statements in development checks to avoid performance impact in production
  2. Incremental updates: Consider whether clearing and recreating all circles is necessary, or if incremental updates would be more efficient for large datasets
-    console.log('fetchAndDisplayVisits called');
+    if (window.DEBUG || process.env.NODE_ENV === 'development') {
+      console.log('fetchAndDisplayVisits called');
+    }

Apply similar changes to other console.log statements throughout the method.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67eb333 and 5224dae.

📒 Files selected for processing (7)
  • app/javascript/controllers/maps_controller.js (22 hunks)
  • app/javascript/controllers/trips_controller.js (1 hunks)
  • app/javascript/maps/fog_of_war.js (4 hunks)
  • app/javascript/maps/live_map_handler.js (1 hunks)
  • app/javascript/maps/marker_factory.js (1 hunks)
  • app/javascript/maps/markers.js (3 hunks)
  • app/javascript/maps/visits.js (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/javascript/controllers/trips_controller.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/javascript/maps/markers.js
  • app/javascript/maps/fog_of_war.js
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/javascript/maps/marker_factory.js (4)
app/javascript/controllers/maps_controller.js (2)
  • marker (750-750)
  • data (1700-1700)
e2e/live-mode.spec.js (1)
  • apiKey (1164-1167)
app/javascript/maps/markers.js (1)
  • renderer (6-6)
app/javascript/maps/popups.js (1)
  • createPopupContent (3-34)
🔇 Additional comments (28)
app/javascript/maps/visits.js (3)

236-238: LGTM! Good separation of concerns.

This change properly decouples visit data refresh from drawer state, ensuring visits are always updated when selection is cleared. The comment clearly explains the new responsibility division.


492-504: Excellent separation of UI and data concerns.

The drawer now focuses solely on displaying content rather than managing data fetching. The placeholder message effectively guides users to the proper controls for enabling visit layers.


682-801: Excellent refactoring with clear separation of responsibilities.

The split between map circle creation and drawer UI updates is well-executed. The defensive programming with early return when the container doesn't exist prevents potential errors. This design makes the code more maintainable and testable.

app/javascript/maps/live_map_handler.js (1)

156-194: Excellent memory management design.

The bounded data structures and incremental update approach effectively address the memory leak issues. The _enforcePointLimits() method properly maintains array bounds, and the heatmap/polyline updates are efficiently implemented to avoid recreation of entire layers.

app/javascript/maps/marker_factory.js (2)

3-63: Excellent constant definitions and factory design.

The MARKER_DATA_INDICES constants greatly improve code readability and maintainability. The factory functions are well-designed with clear purposes: createLiveMarker optimized for performance, and proper parameterization throughout. The velocity-based color coding is a nice touch.


76-101: Well-architected interactive marker creation.

The function properly separates concerns by delegating popup creation and drag handling. Storing metadata in marker options provides the necessary context for drag operations while maintaining clean separation between creation and behavior.

app/javascript/controllers/maps_controller.js (22)

7-7: LGTM: New LiveMapHandler import supports modularization.

The import of LiveMapHandler aligns with the refactoring to delegate live map functionality to a dedicated class for better memory management and modularity.


34-35: LGTM: Import refactoring improves modularity.

The reduction of helper imports to just showFlashMessage and the addition of fetchAndDisplayPhotos from a dedicated photos module reflects good separation of concerns.


84-84: LGTM: Fog line threshold configuration added.

Adding fogLineThreshold configuration improves customizability of the fog of war feature.


158-158: LGTM: Simplified fog overlay creation.

The fog overlay creation has been streamlined to create a single persistent instance, which aligns with the unified fog overlay management mentioned in the refactoring goals.


238-240: LGTM: Clear documentation for LiveMapHandler initialization.

The comment clearly explains the purpose of the new initialization method.


312-343: LGTM: Comprehensive LiveMapHandler initialization.

The initialization method properly configures the LiveMapHandler with all necessary layers and options. The approach of passing existing data to the handler enables continuity between static and live modes.


345-354: LGTM: Proper delegation to LiveMapHandler with appropriate fallback.

The appendPoint method correctly delegates to the LiveMapHandler when live mode is enabled and provides a helpful warning when the handler is not available or live mode is disabled.


380-381: LGTM: World borders data caching improves performance.

Caching the world borders data prevents unnecessary re-fetching of static geographic data.


419-473: LGTM: Dynamic scratch layer refresh with proper caching.

The new refreshScratchLayer() method provides dynamic updates while efficiently reusing cached world borders data. The error handling and logging are appropriate for debugging.


570-603: LGTM: Enhanced layer event handlers support modular functionality.

The new event handlers properly integrate with the refactored photo, visits, and scratch map modules. The date parameter extraction and delegation to appropriate managers follows good separation of concerns.


622-632: LGTM: Proper cleanup when visits layer is removed.

The handler correctly clears visit circles when the Suggested Visits layer is disabled, ensuring proper memory management and state consistency.


705-705: LGTM: Consistent fog overlay references throughout controller.

The consistent use of this.fogOverlay throughout the controller ensures proper integration with the unified fog overlay management approach.

Also applies to: 1117-1117, 1927-1927


750-754: LGTM: Improved addLastMarker method with proper return handling.

The method now returns the marker reference for tracking and properly handles the null case when no markers exist.


756-761: LGTM: Performance improvement with requestAnimationFrame.

Using requestAnimationFrame for the fog canvas drawing operation ensures optimal timing with the browser's render cycle, improving performance.


1027-1034: LGTM: Proper LiveMapHandler integration with settings.

The settings update correctly enables or disables the LiveMapHandler based on the live_map_enabled setting, ensuring the handler state stays synchronized with user preferences.


1086-1086: LGTM: Local property synchronization with settings.

Updating the local liveMapEnabled property ensures consistency between the controller state and user settings.


1165-1190: LGTM: Fixed scope issue in toggle panel button.

Storing the controller reference and using it in the event handler properly fixes the JavaScript scope issue where this would not refer to the controller instance inside the Leaflet control.


1370-1384: LGTM: Consistent photo initialization based on user settings.

The automatic photo fetching when photos_enabled is true in user settings ensures a consistent experience and matches the pattern used in layer event handlers.


1388-1388: LGTM: Updated fog initialization with new method signature.

The fog initialization correctly uses the updated updateFog method signature with the fogLineThreshold parameter.


1392-1403: LGTM: Enhanced visits initialization with improved logic.

The visits initialization properly checks for enabled confirmed visits layers and includes helpful logging for debugging the initialization process.


1502-1504: LGTM: Improved panel state management.

Setting the panel display to 'block' and updating localStorage ensures proper visibility and remembers user preferences across sessions.


718-718: LGTM: Consistent fog update method usage.

The updateFog call in the deletePoint method correctly uses the updated method signature with the fogLineThreshold parameter.

@Freika Freika merged commit a1bca21 into master Aug 1, 2025
10 checks passed
This was referenced Aug 19, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 3, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 13, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 22, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 30, 2025
@coderabbitai coderabbitai bot mentioned this pull request Nov 7, 2025
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.

2 participants