feat(map): self-hosted MapLibre GL + PMTiles vector map

Replace Leaflet + OSM raster tiles with MapLibre GL JS + PMTiles:
- Full continental US vector tiles (8GB PMTiles, zoom 0-14 with overzoom)
- Dark theme via @protomaps/basemaps (official supported path)
- Clustered device markers with status colors (green/yellow/red)
- Popup cards show CPU, memory, wireless client count + avg signal
- Font glyphs proxied through nginx, sprites served locally
- Zero third-party requests from the browser
- Fleet summary SQL now includes wireless client count and avg signal
  via LEFT JOIN LATERAL on wireless_links

Also removes alert toast spam and fixes map container height.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-19 20:16:07 -05:00
parent f0ddd98b93
commit 79899840ca
18 changed files with 462 additions and 198 deletions

View File

@@ -23,7 +23,7 @@ server {
# CSP for React SPA with Tailwind CSS and Leaflet maps
# worker-src required for SRP key derivation Web Worker (Safari won't fall back to script-src)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' ws: wss:; worker-src 'self' blob:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' ws: wss:; worker-src 'self' blob:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
# Proxy API requests to the backend service
# The api container is reachable via Docker internal DNS as "api" on port 8000
@@ -91,7 +91,7 @@ server {
add_header X-Content-Type-Options "nosniff" always;
}
# Serve PMTiles with byte range support (protomaps-leaflet reads via HTTP Range)
# Serve PMTiles with byte range support
location /tiles/ {
add_header Access-Control-Allow-Origin "*" always;
add_header X-Content-Type-Options "nosniff" always;
@@ -101,6 +101,17 @@ server {
try_files $uri =404;
}
# Proxy map font glyphs (MapLibre requests many glyph ranges dynamically)
location ~ ^/map-fonts/(.+)$ {
resolver 127.0.0.11 8.8.8.8 valid=300s ipv6=off;
set $font_path $1;
proxy_pass https://protomaps.github.io/basemaps-assets/fonts/$font_path;
proxy_ssl_server_name on;
proxy_set_header Host protomaps.github.io;
add_header X-Content-Type-Options "nosniff" always;
expires 30d;
}
# Serve static assets with long cache headers
# Note: add_header in a location block clears parent-block headers,
# so we re-add the essential security header for static assets.