feat(import): selective GPX/KML element import and performance improvements

Add type-selector UI in the file import modal letting users choose which
GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points,
paths) to import. KML LineString placemarks are now imported as path
places with route_geometry.

Performance improvements:
- Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut
  unnecessary re-renders in PlacesSidebar
- Add weatherQueue to cap concurrent weather fetches at 3
- Replace sequential per-place deletes with a single bulkDelete API call
  (new DELETE /places/bulk endpoint + deletePlacesMany service)
- Memoize atlas/photo/weather service calls to avoid redundant requests
- Add multi-select mode to PlacesSidebar for bulk operations

Add large GPX/KML/KMZ fixtures for integration/perf testing and two
profiler analysis scripts under scripts/.
This commit is contained in:
jubnl
2026-04-18 01:28:37 +02:00
parent 9a31fcac7b
commit 6a718fccea
45 changed files with 22471 additions and 285 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
+2 -2
View File
@@ -771,7 +771,7 @@ describe('KML/KMZ Import', () => {
expect(res.body.summary.totalPlacemarks).toBe(3);
expect(res.body.summary.skippedCount).toBe(1);
expect(Array.isArray(res.body.summary.errors)).toBe(true);
expect(res.body.summary.errors.join(' ')).toContain('missing Point coordinates');
expect(res.body.summary.errors.join(' ')).toContain('unsupported geometry type');
const nested = res.body.places.find((p: any) => p.name === 'Nested Place');
expect(nested).toBeDefined();
@@ -862,7 +862,7 @@ describe('GPX Import — edge cases', () => {
.set('Cookie', authCookie(user.id))
.attach('file', emptyGpx, { filename: 'empty.gpx', contentType: 'application/gpx+xml' });
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/no waypoints/i);
expect(res.body.error).toMatch(/no matching places/i);
});
});
@@ -277,19 +277,24 @@ describe('importGpx', () => {
expect(result.places[1].name).toBe('London');
});
it('PLACE-SVC-022 — falls back to <rte> route points when no <wpt> elements exist', () => {
it('PLACE-SVC-022 — imports <rte> as a single polyline-place with routeGeometry', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const gpx = Buffer.from(`<?xml version="1.0"?><gpx version="1.1">
<rte>
<name>My Route</name>
<rtept lat="48.8566" lon="2.3522"><name>Start</name></rtept>
<rtept lat="51.5074" lon="-0.1278"><name>End</name></rtept>
</rte>
</gpx>`);
const result = importGpx(String(trip.id), gpx) as any;
expect(result.places).toHaveLength(2);
expect(result.places[0].name).toBe('Start');
expect(result.places[1].name).toBe('End');
expect(result.places).toHaveLength(1);
expect(result.places[0].name).toBe('My Route');
expect(result.places[0].lat).toBe(48.8566);
expect(result.places[0].lng).toBe(2.3522);
expect(result.places[0].route_geometry).toBeTruthy();
const coords = JSON.parse(result.places[0].route_geometry);
expect(coords).toHaveLength(2);
});
it('PLACE-SVC-023 — imports <trk> track as a single place with routeGeometry', () => {