diff --git a/frontend/src/lib/components/PlacesMap.svelte b/frontend/src/lib/components/PlacesMap.svelte index aa358c38..3002879f 100644 --- a/frontend/src/lib/components/PlacesMap.svelte +++ b/frontend/src/lib/components/PlacesMap.svelte @@ -51,7 +51,12 @@ if (hasBasemap !== null) return hasBasemap; try { const res = await fetch(BASEMAP_URL, { headers: { Range: 'bytes=0-0' } }); - hasBasemap = res.ok; // 200/206 = present, 404 = absent + // The SPA fallback serves index.html (HTTP 200, text/html) for any + // missing path, so `res.ok` alone can't distinguish "basemap present" + // from "absent". A real .pmtiles is binary (octet-stream); the shell + // is HTML — treat an HTML body as "no basemap" and fall back cleanly. + const type = res.headers.get('Content-Type') ?? ''; + hasBasemap = res.ok && !type.toLowerCase().includes('text/html'); } catch { hasBasemap = false; } diff --git a/src/interfaces/web/mod.rs b/src/interfaces/web/mod.rs index d16c09f3..a2668038 100644 --- a/src/interfaces/web/mod.rs +++ b/src/interfaces/web/mod.rs @@ -97,6 +97,9 @@ pub fn create_web_routes() -> Router> { /// (`element.style.*`) for UI state — impractical to migrate to classes. /// - `frame-src` lists `blob:` explicitly (`*` only matches network schemes) for /// inline PDF/document viewers; `media-src` lists `blob:` for blob video/audio. +/// - `worker-src` lists `blob:` because MapLibre GL (the Places map) spawns its +/// web worker from a blob URL; `'self'` covers same-origin workers like the +/// delta-upload worker. pub fn content_security_policy(config: &AppConfig) -> String { let static_path = resolve_static_path(config); let hashes = inline_script_csp_hashes(&static_path); @@ -118,7 +121,7 @@ pub fn content_security_policy(config: &AppConfig) -> String { format!( "default-src 'self'; \ {script_src}; \ - worker-src 'self'; \ + worker-src 'self' blob:; \ style-src 'self' 'unsafe-inline'; \ img-src 'self' data: blob: https:; \ media-src 'self' blob:; \