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
+39 -2
View File
@@ -95,6 +95,7 @@ const WMO_DESCRIPTION_EN: Record<number, string> = {
// ── Cache management ────────────────────────────────────────────────────
const weatherCache = new Map<string, { data: WeatherResult; expiresAt: number }>();
const inFlight = new Map<string, Promise<WeatherResult>>();
const CACHE_MAX_ENTRIES = 1000;
const CACHE_PRUNE_TARGET = 500;
const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
@@ -146,7 +147,7 @@ export function estimateCondition(tempAvg: number, precipMm: number): string {
// ── getWeather ──────────────────────────────────────────────────────────
export async function getWeather(
async function _getWeatherImpl(
lat: string,
lng: string,
date: string | undefined,
@@ -281,9 +282,27 @@ export async function getWeather(
return result;
}
export async function getWeather(
lat: string,
lng: string,
date: string | undefined,
lang: string,
): Promise<WeatherResult> {
const ck = cacheKey(lat, lng, date);
const cached = getCached(ck);
if (cached) return cached;
const inFlightKey = `${ck}:${lang}`;
const existing = inFlight.get(inFlightKey);
if (existing) return existing;
const promise = _getWeatherImpl(lat, lng, date, lang);
inFlight.set(inFlightKey, promise);
try { return await promise; } finally { inFlight.delete(inFlightKey); }
}
// ── getDetailedWeather ──────────────────────────────────────────────────
export async function getDetailedWeather(
async function _getDetailedWeatherImpl(
lat: string,
lng: string,
date: string,
@@ -434,6 +453,24 @@ export async function getDetailedWeather(
return result;
}
export async function getDetailedWeather(
lat: string,
lng: string,
date: string,
lang: string,
): Promise<WeatherResult> {
const ck = `detailed_${cacheKey(lat, lng, date)}`;
const cached = getCached(ck);
if (cached) return cached;
const inFlightKey = `${ck}:${lang}`;
const existing = inFlight.get(inFlightKey);
if (existing) return existing;
const promise = _getDetailedWeatherImpl(lat, lng, date, lang);
inFlight.set(inFlightKey, promise);
try { return await promise; } finally { inFlight.delete(inFlightKey); }
}
// ── ApiError ────────────────────────────────────────────────────────────
export class ApiError extends Error {