From 5ccceb463a328f04f3df02cb29d891004b707b91 Mon Sep 17 00:00:00 2001 From: DioCrafts Date: Fri, 19 Jun 2026 21:04:56 +0200 Subject: [PATCH] fix(places): unblock MapLibre worker and fix false-positive basemap probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues kept the Photos → Places map blank once the SPA could boot: - CSP `worker-src 'self'` blocked MapLibre GL, which spawns its web worker from a blob: URL, so the map never constructed. Allow `worker-src 'self' blob:` ('self' still covers same-origin workers like delta-upload). - `checkBasemap()` trusted `res.ok`, but the SPA fallback serves index.html (HTTP 200, text/html) for any missing path — so a missing basemap.pmtiles read as "present", and pmtiles.js then choked on HTML ("Wrong magic number for PMTiles archive"). Reject text/html responses so an absent basemap falls back cleanly to the themed blank style. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/components/PlacesMap.svelte | 7 ++++++- src/interfaces/web/mod.rs | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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:; \