← back to globe

change log_

// und3rgr0und.com — history of implementations & fixes

Project rebuild from a static placeholder to a live 3D vector globe with real-time data layers. Sessions are listed newest-first. Each entry shows the type of change (feat / fix / refactor / data / infra).

[s24] Wildfire overhaul · FR/EN toggle · time-travel replay · 502 outage fix 2026-07-26
feat"À la une" live headline bar
A dismissible bar at the top of the globe surfaces the most notable events happening right now — big fires (FIRMS clusters named by the nearest city), strong earthquakes (USGS M4.5+) and active cyclones (NOAA NHC), aggregated server-side by /api/headlines.php. It scrolls like a TV news ticker (pauses on hover), and each headline links to a real news article (Google News) — the Gironde fire → France 24. Click the card to fly the map there and switch on the matching layer; click the ↗ to open the article.
featReal-time ephemeris: true planet positions + rotation
The 3D solar view now places every planet at its actual heliocentric position right now — a full JPL Keplerian ephemeris (eccentricity + inclination, not just mean longitude), verified against the Sun's real ecliptic longitude to 0.1° — and spins each on its axis with the real IAU rotation rate and direction (Venus & Uranus retrograde). It's genuine real-time, so — like reality — the spin is slow (~15°/h for Earth).
featCosmic scale on the solar-system map
Click a planet (or the Sun) in the 3D solar view and the detail panel now shows how tiny it is against the rest of the observable universe: its diameter versus the Sun (plus "how many fit inside"), a red supergiant (Betelgeuse), the largest known star (Stephenson 2-18), the Milky Way, and the observable universe — e.g. Earth is 6.9 × 10¹⁹ times smaller than the observable universe. Bilingual (FR/EN). The unused flat 2D solar fallback layer was removed at the same time.
featWildfires = EONET events + FIRMS ≥4 MW
The Wildfires layer used only NASA EONET, whose curated feed is Americas-only and slow — so European fires like Gironde never appeared. It now also re-badges every FIRMS thermal hotspot above 4 MW (a tunable "this is a real fire" cut) as a wildfire, so active fire fronts show worldwide. The raw thermal-hotspot layer is unchanged.
featFR / EN interface toggle
A new language button in the SOURCES header switches the whole UI between English and French — header, panel, all 75 layer names + descriptions, presets, group titles, legend, footer, keyboard help and the feedback modal. The choice is remembered, and French is auto-selected for French-language browsers.
featFIRMS replay reaches back in time again
Historical fire replay used the MODIS Standard Product, which lags real time by ~3 months — so "yesterday" came back empty. Recent dates now pull VIIRS NRT (crisp, near-real-time) while older dates keep the MODIS archive back to 2000. Yesterday / last week / last month now show fires.
bugFIRMS hotspots ballooned into blobs when zoomed in
Thermal detections were drawn as metre-radius circles (1.8–9 km) that overlapped into orange blobs smearing over the basemap at city zoom. Now rendered as small crisp dots (~1 VIIRS pixel), legible at every zoom without disappearing.
bugSite-wide 502 after a deploy overwrote the tunnel origin
A deploy re-applied a stale nginx.conf that dropped listen 127.0.0.1:3300 — the loopback origin the Cloudflare tunnel connects to — so Cloudflare returned 502 for all dynamic content (cached static assets still served). Restored the listener and fixed it in the source config so future deploys can't drop it again.
[s23] UI overhaul: search · presets · legend · shortcuts · accessibility 2026-06-16
featUI overhaul: search · presets · legend · shortcuts · onboarding
Panel gains a /-focusable search box over all 75 streams; six one-click curated presets (Disasters / Air traffic / Maritime / Space / Internet / Threats) + clear. A floating legend (top-left) lists active layers with colour + live count and lets you click to remove one. Keyboard shortcuts (/ 1-6 0 h t p +/- ?) with a help overlay. First-visit onboarding nudge. Day/Night terminator now on by default. Returning visitors get a fast-forwarded boot.
bugPanel scroll-jump on every data tick
paintPanel() rebuilt the whole 75-row DOM on every refresh (lightning every 8 s), yanking the user's scroll back to the top mid-read. Split into a structural paint + a lightweight refreshLayerRow() that only rewrites a single row's count/active state — scroll position is now preserved. The live count also flashes when a layer updates.
featAccessibility + mobile details sheet
Dim text colour lightened to clear WCAG-AA contrast on the dark surface; inactive layer swatches bumped to 50% opacity so their colour reads before activation. On phones, tapping a point now opens the rich detail panel as a bottom sheet instead of being hidden entirely.
[s22] 3D solar system · real-time ephemeris · daily refresh cron 2026-05-20
featNavigable 3D solar system (Three.js)
The solar mode is no longer a flat sky chart — it's a real WebGL scene (Three.js r160, self-hosted in /vendor). Nine textured sphere meshes (NASA / solarsystemscope equirectangular maps), OrbitControls (drag to orbit, scroll to zoom, click a body to focus), a 4 000-star field, Saturn's rings, faint orbit paths, and a PointLight at the Sun giving every planet a real day/night terminator. Lazy-loaded via dynamic import() so visitors who never open solar pay zero bytes for it; MapLibre is hidden while the scene is active.
featLive ephemeris — real positions + rotation
Every frame recomputes each planet's heliocentric angle from the current wall-clock using real J2000 mean longitudes + orbital periods, so what's on screen is where the planet actually is right now (no sped-up simulation). Axial spin uses each body's true sidereal-day length keyed to the Unix epoch, so rotation phase is consistent across reloads — Jupiter visibly turns (9.93 h day), the inner planets creep.
dataDaily refresh cron (USGS M8+ auto-append + health probes)
A cron at 03:30 UTC pulls every M8.0+ quake from USGS, dedups against great-quakes.json with a ±2° / same-year tolerance, appends the new ones, and auto-redeploys only if the file changed. It also probes the upstream feeds (USGS, NOAA SWPC, OpenSky, CelesTrak, Onionoo) and logs latency. Keeps the historical-quake layer current with no manual edits.
fixDeploy no longer wipes the visitor log
The deploy script rm -f'd /tmp/und3r-visitors.jsonl on every run, resetting the stats dashboard's visitor counter to zero on each ship. The append-only beacon log now survives deploys (stats.php only reads the last 7 days anyway). Also widened the live-visitors globe layer window from 30 min to 24 h.
[s21] Mega-session: 4 phases · 8 layers · stats · PWA · OG · 20+ bug fixes 2026-05-19
feat8 new layers + Solar-system planet mode
Mars: mars-quakes (NASA InSight SEIS catalog), mars-features (named craters + Tharsis volcanoes). Earth: great-quakes (deadliest M7+ since 1556), cable-landings (37 strategic submarine-cable terminuses). Space: space-debris (Cosmos-1408 fragments via Celestrak TLE + SGP4). New planet mode solar — Sun + 8 planets at their live heliocentric positions from J2000 Keplerian elements; flat sky chart on a black background.
bugGlobe rendering: zoom bug, bleed-through, drag, two-globes
Four globe bugs caught + patched live: (1) points stuck at pre-zoom positions when a URL with deep zoom loaded — fixed via self-jumpTo + zoomend repaint trigger; (2) zoom slider/label desynced from actual zoom on URL boot — synced in _runOnceMapReady; (3) pulse rings + halos dominated the view at city zoom — added zoom-dependent radiusMaxPixels caps; (4) far-side bleed-through of thousands of satellites — added great-circle culling against camera center for points + rings, skipped for arcs/paths.
featMobile UX: panel collapsed on phones + sized chrome
Detect matchMedia('(max-width: 720px)') at boot and add .hidden to the panel so the user lands on the globe, not the layer list. Bigger tap targets on layer rows, taller feedback textarea, bottom-anchored panel sheet stops at 60vh, HUD logo scaled 0.7×, footer wrapped + smaller font, panel-reopen tab moved to top-right where it doesn't fight the about/donate row.
feat/api/stats.php — owner-only live dashboard
IP-whitelisted PHP page (both nginx-level + in-script) that reads /tmp/und3r-visitors.jsonl, /var/lib/und3r-feedback/feedback.jsonl, and the last 256 KB of nginx access.log. Counts: total visits, 24h, 7d, top countries, HTTP 2xx/3xx/4xx/5xx, top paths, full feedback list newest-first. Terminal-styled to match the site. Auth uses inet_pton round-trip so IPv6 normalization can't lock the owner out.
featPWA — manifest + service worker app-shell cache
/manifest.json declares the site installable (standalone display, theme color #00ffaa, SVG icon). /sw.js precaches the shell (/, /style.css, /manifest.json) via Promise.allSettled so a transient miss doesn't abort install. Runtime cache: same-origin assets + unpkg vendor libs (cache-first). Bypass: /api/* + all tile hosts (always live). Navigation: network-first with per-URL fallback cache (so /about doesn't overwrite /'s offline copy).
feat/api/og.php — dynamic Open Graph image (GD)
Server-rendered 1200×630 PNG via PHP GD. Reflects URL state: the ?p=mars/?l=... query is hashed and used as the cache key in /tmp/und3r-cache/og-{hash}.png with 10-minute TTL + probabilistic 1% eviction sweep so the dir can't fill from adversarial probing. Replaces the static og-image.png in the index meta tags.
bugproxy.php LOCK_NB — fixed PHP-FPM worker deadlock
Critical fix. The blocking flock(LOCK_EX) added to proxy.php in [s16] caused every FPM worker to queue behind a leader fetching upstream (up to 25 s). With 5 workers + the thundering-herd-prone cables endpoint, the entire pool froze and every PHP endpoint — feedback.php, visitors.php, stats.php, ALL of them — started returning 504 because no worker was free to accept the next connection. Switched to LOCK_EX | LOCK_NB: contender either serves stale cache or 503's with Retry-After instead of waiting.
fixCode-review patches from final audit
Round-up of smaller fixes caught by the post-phase audits: (a) auto-rotate skipped in solar mode (it was sliding the heliocentric coords every frame); (b) _auToLat rebalanced so Sun + inner planets share a band instead of stacking; (c) planet-aware constructor defaults for center/zoom so ?p=solar doesn't boot at Earth's Europe-framed view; (d) space-debris refreshMs 2000→1000 to match the other TLE layers (no more 2-second teleport hops); (e) _animTimer + _viewportRefetchTimer cleared on pagehide; (f) feedback.php's flock return value checked (was silently writing if locking failed); (g) og.php cache hit re-validates filesize > 0 (truncated cache files were serving empty PNGs).
[s20] Public changelog + feedback modal 2026-05-18
featPublic changelog + footer link
Dropped the IP-whitelist on /changelog.html — the page is now public and linked from the footer next to "about" and "donate". Removed the "private endpoint" line from the changelog footer; the page still ships with Cache-Control: no-store + CDN-Cache-Control: no-store so every visit fetches the latest version (it gets a new commit per deploy). The previous allow / deny / @notfound nginx block is gone — the route is now just a plain try_files on the file.
featFeedback modal — bug/feature submission from the HUD
Footer now has a feedback button that opens a terminal- styled modal with three fields: type (bug / feature / other), message (10–2000 chars), and optional contact. The modal closes on ✕ click, backdrop click, or ESC. Submits post-form-urlencoded to /api/feedback.php; success auto-closes after 1.5 s.
infra/api/feedback.php — append-only JSONL log
New PHP endpoint appends each submission as a single JSON line to /var/lib/und3r-feedback/feedback.jsonl. Operator reads with sudo tail -f. Defences: nginx rate-limit (10 req/s burst 25), Origin header check (only https://und3rgr0und.com), invisible honeypot field (silent 200 on fill so bots don't learn), client-stamped timestamp token rejected if > 24 h old, length bounds, control- char strip. Endpoint added to the nginx PHP whitelist; everything else under /api/*.php still 404s. Log dir is mode 0770 root:www-data — PHP-FPM (www-data) can write, world can't read.
[s19] deck.gl half-redrawn "two-globes" seam fix 2026-05-18
bugTwo-globes visual after drag — half-redrawn deck overlay
User shipped a screenshot showing a second, offset sphere of points floating above the main Earth globe. Reproduced in browser: with interleaved: true on a globe-projected map, the deck.gl overlay can finish a drag in a half-redrawn state — one hemisphere shows the pre-drag pointcloud, the other shows the post-drag one, with a visible vertical seam down the middle of the sphere. With dense layers active (satellites + Starlink = ~7 700 points), the seam looks like two separate globes overlapping.

Root cause: deck.gl's MapboxOverlay caches projection state per gesture frame; when MapLibre's transform settles at moveend, deck doesn't always know to invalidate its previous frame. Forcing map.triggerRepaint() on moveend drops the stale frame and re-projects deck.gl against MapLibre's final camera. Cheap (one repaint per gesture end) and doesn't fight the in-flight drag the way the [s18]-reverted styledata-time setProjection did.
[s18] Drag-to-pan fix on globe projection 2026-05-18
bugGlobe didn't follow mouse drag — points stayed pinned to screen
After the [s17] interleaved-overlay fix, every mouse drag on the globe barely moved the camera — a 540 px horizontal pan produced a ~0.4° longitude change instead of the expected ~137°. The deck.gl layers (earthquakes etc.) sat motionless on screen even though the basemap should have rotated underneath them.

Root cause: the map.on('styledata', …) handler was re-asserting map.setProjection({ type: 'globe' }) on every styledata event. MapLibre 5 fires styledata frequently during camera-driven re-renders (panning, zooming, tile-load progress), and each setProjection call inside the gesture resets the projection matrix mid-drag — the cumulative pan delta gets clobbered and the camera barely moves.

Fix: removed the setProjection call from the styledata handler. The projection is set once at the constructor and re-asserted once per style.load (after setStyle() for the planet/theme swap) — that's sufficient. Verified by dragging from (740, 400) to (200, 400): lng goes from 0 to 136.8, globe rotates to show Asia/Oceania, and every earthquake ring stays glued to its real geographic position (Japan, Philippines, Indonesia, PNG, Vanuatu, New Zealand all on the Ring of Fire).
[s17] deck.gl 9.3 — round globe + points that stick 2026-05-18
featdeck.gl points now stick to the globe surface
After the MapLibre 5 globe upgrade in [s15], deck.gl's points stayed glued to mercator coordinates while the basemap was sphere-projected — earthquakes/asteroids/ISS rendered in a flat horizontal band that stretched well past the globe's rim. [s15] flagged this as a known limitation; [s16] half-tried interleaved: true and back-tracked when MapLibre's load event stopped firing. Root cause for the stall: deck.gl 9.0.36 in interleaved mode registered a custom layer that kept the map's tile-loaded check permanently false on globe maps, deadlocking map.on('load'). Upgraded to deck.gl 9.3.2 (latest), which ships proper MapLibre 5 globe integration. Verified on the Pacific Ring of Fire: 217 earthquakes all pin to their exact lat/lng on the globe — California cluster on the San Andreas, Alaska/Aleutian arc, Vanuatu/Tonga subduction, Andes belt. Same fix carries Mars: 12 rovers now sit on the actual Mars surface inside Jezero / Gale.
infradeck.gl 9.0.36 → 9.3.2 + interleaved overlay
Bumped deck.gl from 9.0.36 to 9.3.2 in index.html with new SRI digest sha384-AZDnlCTAkFvSh/+Cf/M6f7gaXokQypLzR6WElZxMcHLxGp+6BXRftDtJDH42vG/U. Switched MapboxOverlay from default non-interleaved mode to interleaved: true so deck layers share MapLibre's WebGL context and view-projection matrix — the only way to get globe vertex transforms applied to scatter / ring / arc layers. Verified deck.gl 9.3 lifts the 9.0.x load-event deadlock.
fixBoot panel stuck — map.on('load') unreliable in interleaved mode
Even on deck.gl 9.3.2, MapLibre's loaded() probe can return false indefinitely when an interleaved deck layer is sitting in the style — so the load event stalls and the boot crossfade never triggers. Replaced the single map.on('load', _runOnceMapReady) with a triple-binding against load, idle, and style.load; _runOnceMapReady is idempotent (gated by _mapLoaded) so the first trigger to fire wins. style.load fires reliably as soon as the style is parsed — well before tiles are in — which is what the boot panel and URL-state replay actually need.
refactorReverted the mercator fallback from earlier today
Earlier in this session I considered switching the whole map to mercator as a pragmatic workaround. Removed that path — both planets are back on globe projection. STYLE_MARS gets its projection: { type: 'globe' } back, the constructor stays at globe, and every setProjection() call asserts globe rather than mercator.
[s16] Full-codebase bug hunt — 14 fixes 2026-05-18
bugXSS via upstream API error messages in panel
app.js:670 wrote L._error directly into the layer row's innerHTML. That field comes from e.message on a caught exception (sliced to 24 chars), which ultimately originates from upstream API responses. A misbehaving proxy returning HTML-laden errors could inject script into the panel. All panel-row interpolations now go through esc() (label, desc, count, err message).
bugPlanet swap lock never released on style error
_planetSwapInFlight was cleared in map.once('style.load') only — if MapLibre rejected the style (malformed JSON, network error on tile source), the 'error' event fired first and left the flag stuck at true, locking out every future Earth/Mars swap until a page reload. Now also reset in the error handler.
bugstartLayer double-call leaked setInterval handles
startLayer() had no idempotency guard. When a layer was both in the URL state and in defaultOn, or when setReplayMode() re-entered an already-running layer, the second call called setInterval again and overwrote the Map entry. The first interval kept firing forever with no way to stop it — slow leak measurable over hours. Added early-return when state.enabled.has(L.id).
bugURL params with NaN crashed MapLibre 5 constructor
readURLState() used parseFloat(p.get('z')) with ?? null fallback. parseFloat('abc') returns NaN, and NaN ?? null stays NaN. MapLibre 5's stricter validator throws "invalid LngLat" on NaN center/zoom and the map fails to construct. A shared link like ?z=foo&lat=bar would 502 the whole page. Now filtered via Number.isFinite().
bug_boostLightLabels feedback loop in light mode
map.on('styledata') called _boostLightLabels() which called setPaintProperty on every text-bearing layer. In MapLibre 5, setPaintProperty re-fires styledata — so each boost spawned another boost. Bounded in practice (idempotent values) but burned CPU on every theme/planet swap. Added a _boostedThisStyle flag, cleared on style.load.
fixSecurity feeds crashed on rows missing lat/lng
sec.js AbuseIPDB / HoneyDB / Feodo / URLhaus filters checked timestamp fields but never lat/lng. When the proxy's geoip lookup failed on a row, row.lat.toFixed(2) threw TypeError mid-map, killing the whole fetch and showing err: row.lat is undefined in the panel. Added Number.isFinite(row.lat) && Number.isFinite(row.lng) to every sec.js feed.
fixISS rendered at (NaN, NaN) on proxy failure
space.js ISS layer used d.latitude/ d.longitude without validation. A failed proxy fetch resolves to {} (or a cached stale-null), producing two ring objects with NaN coordinates. Added an isFinite() guard at the top of the fetch.
fixLaunch pads at equator/prime meridian silently dropped
L.pad?.latitude && L.pad?.longitude filter relied on JavaScript's truthiness — numeric 0 is falsy, so any pad on the equator (lat=0) or prime meridian (lng=0) was excluded. No real-world spaceport sits exactly on 0,0 yet, but Sea Launch's Odyssey platform routinely sat at lng≈154 lat=0. Changed to explicit != null checks.
fixLaunch countdown displayed in local time despite T-
new Date(L.net).toLocaleString('en-US') with no timeZone option rendered the launch NET in the user's local zone — "T-5/19/2026 1:00 AM" with no timezone marker. Pads and missions universally schedule in UTC. Added { timeZone: 'UTC', timeZoneName: 'short' } matching the earthquake-replay fix in [s14].
fixSatellites/Starlink blanked after one failed refresh
Both TLE-driven layers had if (_satState.records) return []; on a failed refresh — discarding every subsequent tick rather than propagating from the still-valid cached records. After the first Celestrak rate-limit hit (1×/2 h per IP), the layer stayed empty until a page reload. Now keeps using cached TLEs and backs off the refresh clock.
fixLightning rings pinned forever on upstream error
earth.js lightning fetch's early-return-on-error path returned Array.from(_strikeRings.values()) without running the 25 s purge loop. A single bad response from /api/lightning.php froze the last batch of strikes on the map indefinitely. Now purges first, then returns what's left.
fixBGP layer never rendered any points
RIPE RIS Live's WebSocket emits data.host as the FQDN of the route collector ("rrc21.ripe.net"), but the BGP layer's RRC_GEO coordinate table is keyed by short ID ("rrc21"). Every lookup in net.js missed and the layer stayed empty since the day it was added. Now .split('.')[0] normalizes the key before incrementing counts.
fixDay/Night terminator off by up to 24 h declination
_subsolarPoint() built new Date(year, 0, 0)local time Dec 31 — then subtracted from UTC now to derive day-of-year. The mix of timezones shifted the declination term up to a full day either side of UTC, drifting the terminator band visibly near equinoxes (where the sun's lat moves fastest). Switched to Date.UTC(year, 0, 0).
infradeploy.sh — chown after sed, cache-bust regex widened
Two deploy-time bugs: (1) chown -R www-data:www-data ran before the sed -i cache-bust pass. sed -i writes a temp file as the calling user (root) and renames it over the original, dropping the previous owner — so every cache-busted file ended up root:root. Re-ordered so chown is last. (2) The regex [a-zA-Z_/-]+ for module names skipped anything with digits or dots (geo2.js, iss-1.js) — they were never cache-busted and stayed pinned at the CF edge after each deploy. Widened to [a-zA-Z0-9_./-]+.
infraproxy.php — flock against thundering herd + Vary: Origin
nginx's burst=25 nodelay rate limit allows up to 25 concurrent requests through. When the cache file for an expensive endpoint (cables, satellites — each up to 20 MB) was missing or stale, all 25 fired concurrent curls in parallel — DoS-amplifying our own server. Added an flock(LOCK_EX) per-endpoint: only one worker fetches upstream, the rest wait and re-check the cache (now fresh) on release. Also added Vary: Origin to prevent any future multi-origin CORS cache poisoning.
[s15] MapLibre 5.x upgrade — Mars as a real 3D globe 2026-05-18
featMars now renders as a 3D globe (was flat)
Even with projection: { type: 'globe' } declared in STYLE_MARS, switching to Mars produced a flat equirectangular map. Live-debugged with the Chrome MCP and found two roots: map.setProjection didn't exist as a method, and MapLibre 4.7's setStyle() silently stripped the projection field from the style object passed in (getStyle().projection === undefined on every reload). Both are MapLibre 4.x limitations — globe projection is constructor-only in that branch and can't survive a style swap. Upgraded to [email protected] where map.setProjection() is a first-class runtime API and style spec carries projection through setStyle(). Mars now ships as an actual sphere with Arabia Terra, Margaritifer Terra, Terra Sabaea, Acidalia Planitia, Syrtis Major all visible.
infraMapLibre GL JS 4.7.1 → 5.6.0 + SRI hashes
Bumped both the <script> and <link rel=stylesheet> in index.html to [email protected]. Recomputed the SHA-384 SRI digests against the bytes served by unpkg: sha384-GfxBM9x46BaAFxtCq39Fxir8fNZ4VDnwgfi6Kzi5/F1tAFsm0amuuV8kd+Pxzuf/ for the JS, sha384-Nq6PQ+9vJPvw7U/VfDELyrWoGQMsy0gi6QShhaSrGzkpF5KkM40csg2leky+YMTd for the CSS. Verified in-browser that [email protected]'s MapboxOverlay still attaches cleanly to the 5.x map (it does — all Earth layers still render).
refactorDrop noisy setProjection warnings
Removed the console.warn('[planet] setProjection:', e) that fired on every Earth/Mars swap in 4.x. setProjection() is supported in 5.x and the try/catch around it is now belt-and-braces only; no need to surface a warning when the path is well-trodden. Updated the boot-log line from "linking maplibre 4.7" to "5.6" to match what's actually loaded.
bugdeck.gl points flying off the globe in a flat band
After the MapLibre 5 / globe upgrade, deck.gl layers (earthquakes, ISS, asteroids, visitors, lightning) rendered as a flat horizontal stripe that stretched across the entire viewport — points stayed glued to mercator screen coordinates while the basemap had moved to a sphere. Caught from a user screenshot. Root cause: MapboxOverlay defaults to interleaved: false, which rasterizes deck.gl on its own 2-D canvas using the previous mercator projection matrix and composites it on top. deck.gl docs are explicit: globe projection requires interleaved mode so the deck layers share MapLibre's WebGL context and projection. Flipped to interleaved: true. Points now stick to the sphere correctly — verified on the Pacific Ring of Fire (India/Nepal/Philippines/Vanuatu earthquakes all land on the right continents).
fixKnown limitation — no z-occlusion on far side of globe
With interleaved: true, deck.gl shares MapLibre's depth buffer but the globe sphere itself isn't a real 3-D mesh — it's a projected raster surface. Points on the hemisphere facing away from the camera still render and bleed through the visible side as faint ghosts past the rim. Tolerated for now: alternative would be a pre-pass that clips points by angle-from-camera (dot product between camera-to-point and camera-to-center vectors). Flagged for a future session; not a blocker since the bleed-through is faint at z ≥ 1.5.
[s14] Mars debug via live browser session 2026-05-18
bugMars basemap blocked by CSP
Connected to the live site via the Chrome MCP extension and watched what MapLibre actually did. 29× TypeError: Failed to fetch in the console: the OpenPlanetary tile CDN is hosted at cartocdn-gusc.global.ssl.fastly.net (Fastly, not *.cartocdn.com), so the CSP's img-src and connect-src rejected every tile. Added the host to both directives → Mars surface now renders (Olympia Planum, Hellas Planitia, Acidalia, Tempe Terra all show correctly).
fixMars rovers invisible at default zoom
Verified in-browser: pts-mars-rovers deck layer was built with 12 data points, visible: true, and projected to the correct screen coords — but radiusUnits: 'meters' with r: 0.18 meant the dots were sub-pixel at z < 5. Visible only when zoomed in to z ≥ 7. Converted to type: 'ring' with pulse: true like the Earth landmark layers (volcanoes, cyclones), so they're visible at any zoom regardless of the planet's apparent size.
fixEarthquake replay-date timezone mismatch
Picking "01/05/2026" returned events that showed "5/2/2026 12:17 AM" in the panel — earthquake actually at 22:17 UTC on May 1, displayed in local time (Paris UTC+2). Switched the format to toLocaleString('en-US', { timeZone: 'UTC', timeZoneName: 'short' }) so the date matches the picker. Same fix applied to BTC blocks.
infraDeeper cache-busting on per-module imports
The existing cache-bust stamped ?v=$TS on app.js, style.css, and layers.js, but the per-group sub-modules (layers/earth.js, _helpers.js, etc.) inherited browser caches indefinitely. JS changes in those files didn't propagate even after a deploy. Extended deploy.sh to sed-stamp every from './…js' import across the module tree.
[s13] Time-travel replay + Mars/changelog fixes 2026-05-18
featReplay mode — historical data at any date
New LIVE / REPLAY toggle at the top of the SOURCES panel. In replay mode a date picker appears; non-historical layers are grayed out with a "live-only" tag. Currently 4 layers replay:
  • 🌍 Earthquakes — USGS FDSNWS query by date range, magnitude ≥ 2.5
  • 🔥 FIRMS thermal hotspots — switches from VIIRS NRT (live, 60-day window) to MODIS SP (archive back to 2000) for replay
  • 🛰️ Near-Earth asteroids — NASA NeoWs feed by date
  • 🦠 WHO outbreaks — filters the 100 most recent ±60 days around the picked date
Test result on 2015-08-15: ~5000 fires, on 2015-03-22: 59 earthquakes M≥2.5, 7 NEOs — all geographically resolved.
infraDate-aware proxy + per-date cache files
proxy.php now accepts an optional ?d=YYYY-MM-DD that flows into the upstream URL builder (via the url_fn callable). Cache filename includes the date so each replay-day keeps its own snapshot indefinitely (live = current-day with normal TTL).
bugMars basemap was invisible
NASA Mars Trek tiles are served WMTS-style (/z/y/x.jpg, row-major) which doesn't align with MapLibre's XYZ scheme past zoom 0. Switched to OpenPlanetary's Mars basemap (cartocdn-gusc.global.ssl.fastly.net/...) which uses true XYZ and CORS-friendly headers. Mars planet view now renders.
bugChangelog page returned 404 for whitelisted IPs
Two issues: (1) the location = block didn't have an explicit try_files directive so file lookup was ambiguous, and (2) Cloudflare was caching the 403→404 response from before the whitelist was in place. Fix: added try_files /changelog.html =404; and Cache-Control: no-store + CDN-Cache-Control: no-store headers so Cloudflare bypasses the edge cache.
[s12] Bug hunt — 7 issues fixed 2026-05-18
bugISS layer crashed on null altitude/velocity
space.js:51d.altitude.toFixed(0) threw when the wheretheiss.at API returned a null field (rare but observed). The whole ISS layer stopped rendering. Fix: (d.altitude ?? 0).toFixed(0) on all 4 fields (altitude, velocity, latitude, longitude).
bugPower plants + Nuclear: crash on null capacity
energy.js and infra.jsp.mw.toFixed(0) would throw if WRI shipped a null capacity. Pre-coerce to mw = p.mw || 0 at the top of the map function so the layer keeps rendering even on missing values.
bugMars rovers fetched on Earth from URL state restore
Mars rovers layer (planet: 'mars') is hidden in rebuildDeckLayers when on Earth, but the URL-restore loop would still call startLayer() + fetch(). The fetch did nothing useful but populated state. Two fixes:
  • URL restore now skips layers whose planet doesn't match the active planet
  • fetch() bails early if state.planet !== 'mars'
fix_rgba() lost alpha on 8-digit hex (#RRGGBBAA)
amb.js:13 uses color: '#44aaff88' for submarine cables to render them semi-transparent. _rgba() only sliced the first 6 chars, dropping 88 and using the caller's default alpha 220 instead. Fix: detect 8-digit hex and parse the embedded alpha. Cables now render at ~0.5 opacity as originally intended.
fixMars view → URL share now keeps Mars context
scheduleURLWrite() serialized layers + viewport but NOT the active planet. Sharing a Mars URL with someone else loaded Earth basemap and they had to manually toggle. Added ?p=mars to the URL and parsed it on boot (URL takes precedence over localStorage so shared links always reproduce the sender's view).
fixLongitude drift in URL state
Auto-rotation could push center.lng past ±180 (e.g. 195°). scheduleURLWrite writes the raw value. Normalize with ((lng + 540) % 360) - 180 so shared URLs always have [-180, 180].
fixsetPlanet race when clicked rapidly
Earth → Mars → Earth in < 200ms triggered two setStyle calls before style.load fired, leaving the map in an inconsistent state. Added a _planetSwapInFlight guard that ignores re-entry until the basemap finishes loading.
fixDuplicate Etna volcano removed
ACTIVE_VOLCANOES in _data.js had both "Etna" and "Etna (NE)" at nearly identical coords (37.748 / 37.940, both Sicily). The map rendered two overlapping pulse rings. Removed the duplicate.
[s11] Massive data + planet switcher 2026-05-18
featMars planet switcher (architecture for multi-planet)
Added a "🪐 PLANET" selector below the SOURCES panel (collapsed by default). Switching to Mars swaps the basemap to NASA Mars Trek Viking MDIM v2.1 raster tiles (trek.nasa.gov), hides all Earth layers via a planet: field, and shows 12 rover/lander positions (Curiosity, Perseverance, Zhurong, InSight, Spirit, Opportunity, Sojourner, Phoenix, Viking 1&2, Mars 3, Beagle 2). Persisted to localStorage. Theme toggle is no-op while on Mars (basemap is fixed). Architecture extensible — adding Venus / Moon = add a new style + set planet: on the layer.
dataPower plants — WRI Global Power Plant DB v1.3
Extracted all ~30 000 power plants from the WRI Global Power Plant Database, bucketed by primary fuel into 5 separate layers (cache-on-activation, gzipped):
Hydroelectric7 156 plants · 630 KB
Solar10 665 plants · 988 KB
Wind5 344 plants · 500 KB
Coal2 330 plants · 221 KB
Gas3 998 plants · 391 KB
data15 curated datasets added (~700 dots)
  • Mountain peaks (~100) — 14 eight-thousanders + Seven Summits + famous Alps/Andes/Asia/Africa/N.America/Oceania
  • F1 circuits (38) — 24 active + 14 historical (Monaco, Spa, Suzuka, Silverstone, Paul Ricard…)
  • Major stadiums (100) — Rungrado May Day, Maracanã, Camp Nou, Stade de France, Vélodrome…
  • Archaeological sites (~85) — Machu Picchu, Pyramids, Petra, Lascaux, Pompeii, Göbekli Tepe, Stonehenge…
  • Historical launch sites (~40) — Cap Canaveral, Baikonur, Kourou, Tanegashima, Vandenberg, Plesetsk…
  • Skyscrapers (~38) — Burj Khalifa, Merdeka 118, Shanghai Tower, Tour First Paris…
  • Strategic mines (~60) — Li/REE/Cu/Au/U/Fe/diamond. Incl. EMILI Beauvoir (FR).
  • Oil refineries (~100) — Jamnagar, Ras Tanura, Gonfreville Le Havre, Donges, Feyzin…
  • Offshore oil platforms (~100) — Brent, Ekofisk, Mars, Thunder Horse, P-50→P-77, Bonga, Sakhalin…
  • Gas storage (~50) — Rough, Rehden, Chémery, Lussagnet, Manosque, Bogorodchany…
feat4 new layer groups
ENERGY (8 layers: 5 fuels + oil rigs + gas storage + refineries), 💎 MINING (1 layer with 7 mineral categories), 🎯 SPORTS (F1 + stadiums), 🏥 HEALTH (WHO outbreaks, last 12 months filter). Total now: 16 groups · 69 active layers + 4 disabled stubs.
featWHO Disease Outbreak News layer
Fetches the WHO DON feed via proxy.php?t=who-don (6 h cache), filters to last 365 days, parses country name from the title, maps to ~200 country centroids. Ring size scales with recency (≥30 days = small, <30 days = large red). Tooltip + details panel show full title, publication date, age, and summary excerpt.
[s10] Big coord audit + nuclear expansion 2026-05-17
fix17 wrong coordinates corrected
Audit revealed many hand-curated entries had wrong coords. Highlights:
  • Borexino was at (42.45, -71.07) = Boston! → fixed to Gran Sasso (42.45, 13.57)
  • Virgo was in Switzerland → fixed to Cascina, IT (43.63, 10.50)
  • DESY was at Darmstadt → fixed to Hamburg (53.58, 9.89)
  • European XFEL was in Denmark → Hamburg
  • Australian Synchrotron was in Perth → Clayton, Melbourne
  • Palomar was in LA, Pic du Midi was in the Atlantic, CHIME was in southern Africa
  • Universities: UC Berkeley at UCLA's coords, Cornell in Québec, Tsinghua in Shanghai, HKU in Shenzhen, UBC in WA state
  • Datacenters: GCP us-east4 in Philadelphia, GCP us-west3 in Portland, Azure East US 2 in NY state, Azure Mexico Central in Phoenix
  • Seaports: Anchorage AK in southern California, Veracruz at Mexico City
  • Parks: Tunkinsky, Putorana, Wadi El Gemal, Songshan, Khustain, Bali Barat — all 100–700 km off
  • DNS root B-root: lat/lng swapped (lat -117 outside [-90,90])
dataNuclear: 40 → 208 plants (WRI + decom)
Replaced the hand-curated 40 reactors with WRI's full power plant DB filtered to primary_fuel=Nuclear (195 sites). Added 13 sites WRI excludes (decommissioned / research): Chernobyl, Three Mile Island, Ignalina, Caorso, Sellafield, Trawsfynydd, Krško, Gösgen, Mühleberg (shutdown), Fukushima Daini, Onagawa II/III, Point Lepreau, Wolf Creek. Coverage approaches reactormap.com (which shows reactor-unit level).
dataDatacenters: 232 → 318 (+86 entries)
Added missing operators: AWS GovCloud (us-gov-west/east-1), Vultr (26 cities), Linode/Akamai (19), Fly.io (35 regions), Backblaze (4). Beyond the existing AWS / GCP / Azure / Equinix / Digital Realty / NTT / Alibaba / OVH / Scaleway / Hetzner / DigitalOcean / OCI / Tencent.
fixUniversities — duplicates removed
Removed "Princeton (ext)" (duplicate of Princeton) and merged "Carnegie Mellon" (which had Stanford's coords!) with "Carnegie Mellon Pittsburgh". 150 → 148 clean entries.
fixSynchrotron category added to Big physics
User caught: the Big physics layer claimed "accelerators + grav waves + neutrinos" but had zero synchrotrons. Added a 4th category (20 synchrotrons: ESRF Grenoble, SOLEIL Saint-Aubin, Diamond, SLS, PETRA III, ALS, APS, NSLS-II, Spring-8, Sirius, Elettra, ALBA, MAX IV, SOLARIS, SESAME, Indus-2, Australian Synchrotron, TPS, SSRF, BSRF). Cyan color to distinguish from violet (accelerators).
[s09] Time filters + science + culture + conflicts 2026-05-17
feat<24h filter on event layers
User wanted all event-based layers to show only the last 24h or live data. Audit found 5 layers needed tweaking:
  • Earthquakes: switched USGS feed all_hourall_day (6 → 170 events)
  • Tsunamis: filter to last 24h on the week-long significant feed
  • Wildfires (EONET): cutoff 6h → 24h
  • URLhaus: filter by dateadded < 24h
  • AbuseIPDB: filter by lastReportedAt < 24h
All other layers were already live or <24h by design.
feat3 new groups (SCIENCE, NATURE, CULTURE, KNOWLEDGE, CONFLICTS)
  • 🔬 SCIENCE: observatories (~55), polar stations (~55 Antarctic+Arctic), big physics (43)
  • 🌱 NATURE: national parks (156), rainforests (14), coral reefs (30)
  • 🎨 CULTURE: UNESCO World Heritage (1245 sites from official XML)
  • 🎓 KNOWLEDGE: top universities (150 QS), major libraries (30)
  • ⚔️ CONFLICTS: refugee camps (~40, ring size = log(pop))
[s08] UX polish 2026-05-16
featCollapsible category groups
Click a group title (🌍 EARTH, etc.) to collapse/expand. Chevron rotates to -90° when collapsed. Counter shows active/total per group (excluding disabled stubs). Persisted to localStorage so preferences stick across visits.
featBoot screen with streaming log
Replaced the single "initializing uplink_" text with an 8-line log streaming in over ~1.8s ("> linking maplibre / fetching carto / opensky handshake / blitzortung ws / wikimedia eventstream / celestrak tle / maxmind geoip / tor onionoo / >>> uplink stable"). Map fades in only after both the log finishes AND map.load fires, so the planet eases in instead of popping.
fixMobile touch picking — dots are tappable now
deck.gl's gesture detector rejects taps with >5px jitter as "drag" — common with fingers — so onClick never fired. Replaced with a unified pointerup capture-phase handler that calls overlay.pickObject({x,y,radius:28}) directly. Drag threshold relaxed to 14px on touch.
refactor_details key normalization across all layers
Canonical schema: every layer with details now has Position (lat/lng formatted), Source (upstream name), and Source IP (not IP) for IP-based items. Volcanoes Latitude+Longitude collapsed to Position. Solar SubsolarPosition.
[s07] Module split + code review 2026-05-16
refactorlayers.js split into per-group modules
Monolithic layers.js (1483 lines) split into a barrel + 10 modules: _helpers.js, _data.js, _state.js, earth.js, space.js, flights.js, net.js, sec.js, infra.js, amb.js. Each file ≤370 lines.
refactorfetchJSON / getViewportBbox / _stringHash helpers
22 layers had near-identical fetch boilerplate (await fetch + r.ok check + r.json + return []) → replaced with one helper. Zoom-aware bbox logic duplicated between aircraft and ships → extracted. hash() defined twice (Tor + Tor-exits) → deduped. POOL_GEO (26 entries) was rebuilt inside fetch() every 20s → hoisted to module scope.
infraDead code removal + CSP migration
Deleted: globe.pointOfView shim (only referenced by deleted .bak files), tsunamis proxy endpoint (the layer fetches USGS direct), orphan /etc/nginx/snippets/und3r-security.conf (never included). Migrated Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy headers (which were stranded in the dead snippet) into the active nginx config.
[s06] English translation 2026-05-15
featFull UI translation FR → EN
Group titles (TERRE → EARTH, ESPACE → SPACE, SÉCURITÉ → SECURITY), all layer labels (Séismes → Earthquakes, Aurores → Aurorae, Avions → Aircraft, Navires → Ships, Visiteurs live → Live visitors, etc.), all _details object keys, tooltips with French interpolated text (SÉISME, ATTAQUE SSH, Foudre, Soleil au zénith…), volcano country/region names (Sicile → Sicily, Japon → Japan, Antarctique → Antarctica…). toLocaleString('fr-FR')'en-US' everywhere. lang="fr"lang="en" on both pages.
[s05] 9 new layers (round 2) 2026-05-16
featNASA FIRMS thermal hotspots + EONET wildfires
FIRMS via NASA_FIRMS_KEY (separate from regular NASA key). Returns ~5000 satellite thermal detections per 24h. Server-parses CSV → JSON, filters to nominal+high confidence, colors by FRP (fire radiative power). EONET wildfires as a complementary "notable events" layer with a 24h cutoff.
featabuse.ch security feeds (Feodo + URLhaus)
Feodo Tracker C2 servers (online filter only) and URLhaus malicious URLs. Both geo-located server-side via local MaxMind GeoIP, no auth required.
featRIPE RIS BGP live (browser WebSocket)
Connects to wss://ris-live.ripe.net from the browser (CSP allow-listed). Subscribes to UPDATE messages, aggregates counts per RRC (Route Collector), renders 23 RRC locations with rings whose maxRadius is proportional to log(count). Counts decay 50% every 5s for a "fading activity" feel.
featBitcoin blocks (mempool.space + pool geolocation)
Last 6 blocks rendered as orange rings at the mining pool's HQ location (Foundry US, F2Pool Beijing, AntPool, MARA Pool, Binance Pool, etc.). Newest block = biggest ring. Pool slug → coords via a hoisted table.
featStatic infrastructure layers (datacenters / nuclear / seaports / airports)
Populates the previously empty 🗺️ INFRASTRUCTURE group:
  • Datacenters: 34 → 232 (cloud regions + Equinix + Digital Realty + OVH + Scaleway + Hetzner + DigitalOcean + Alibaba + Tencent + OCI)
  • Nuclear: 40 plants (later expanded to 208)
  • Seaports: 198 (incl. 24 French ports — Marseille-Fos, Le Havre, Dunkerque…)
  • Airports: 6072 with IATA code, parsed from OpenFlights public dataset
featAircraft + ships viewport-aware loading
Server's /api/ships.php accepts ?bbox=south,west,north,east&max=N. Client passes the current viewport when zoom ≥ 2 → server returns only ships in view (up to 5000). At low zoom the global sample is downsampled to 800 to avoid blobbing the planet. Cap raised from 600 → 5000. moveend event re-fetches viewport-aware layers (debounced 350ms).
[s04] Click-to-pin + light theme readability 2026-05-15
featClick-to-pin info panel
Bottom-left info panel persists when you click a dot (instead of fading on mouse-out). Click empty space → unpin. Implementation: pointer-up handler in capture phase + suppressClick flag to coordinate with deck.gl. Larger picking radius (11px). Hover still works (transient preview) unless something is pinned.
fixLight theme map labels readable
Switched basemap from Carto Positron to Voyager (darker labels) for the light theme. Added a _boostLightLabels() pass that adjusts label paint properties after style swap so place names, roads, and admin boundaries stay legible. Dark theme untouched.
[s03] Moving dots + lightning animation 2026-05-15
featDead-reckoning for aircraft / ships / satellites
Dots slide smoothly between API refreshes instead of teleporting. Layer items carry _vel (m/s), _hdg (degrees), and _baseTime. _extrapolatePos() projects the position forward based on elapsed time. updateTriggers bound to a pulse tick so deck.gl re-renders.
featLightning animation (white + 8s refresh)
Blitzortung WebSocket strikes render as animated white rings that fade over ~25s. Stable Map keyed by strike id so rings keep their pulse phase between 8s refreshes (preventing flicker). pulseFade: true flag distinguishes lightning (decay) from layers that loop forever (volcanoes, cyclones).
[s02] Initial layer ecosystem (23 sources) 2026-05-15
featReal-time data layers
Wired the first round of public-feed layers:
  • Earthquakes (USGS), Aurorae (NOAA OVATION), Lightning (Blitzortung WebSocket)
  • Volcanoes (Smithsonian static), Cyclones (NOAA NHC), Tornadoes (NWS)
  • ISS (wheretheiss.at, 5s), Satellites (Celestrak TLE + satellite.js, recompute 1s)
  • Launches (Launch Library 2), Near-Earth asteroids (NASA NeoWs)
  • Aircraft (OpenSky, 15s), Ships (AISStream WebSocket → Python service)
  • Tor relays (Onionoo), Wikipedia anonymous edits (Wikimedia SSE)
  • SSH attacks (server's auth.log tailed by Python service + MaxMind geo)
  • Live visitors (own beacon, 8s), Submarine cables (TeleGeography)
  • AbuseIPDB blacklist, HoneyDB threats, Tor exit nodes
  • Day/Night terminator (subsolar point), Markets open (UTC trading hours)
infra3 systemd Python services (SSH / AIS / Lightning)
Long-running tailers / WebSocket clients write JSONL files that the frontend polls via thin PHP endpoints. Auto-restart, log rotation, throttled writes. AIS uses a free AISStream.io token (managed via --ais-token= deploy flag).
infraPHP-FPM 8.3 + nginx + Carto vector tiles
proxy.php with file-cache + flock for endpoints that need CORS / auth / IP geolocation. Per-endpoint TTLs. Stale-while-error. 6 API keys managed via /etc/und3r/keys.conf (root:www-data 0640): NASA, NASA FIRMS, OpenAQ, AbuseIPDB, HoneyDB, Cloudflare Radar. Each settable via a dedicated --xxx-key= deploy flag.
[s01] Initial site replacement 2026-05-15
feat3D vector globe (MapLibre 4.7 + deck.gl 9)
Replaced the previous static placeholder site. MapLibre in globe projection morphs smoothly into a 2D Mercator map past zoom 5 (no manual toggle). Carto dark-matter / voyager vector tiles. deck.gl MapboxOverlay for the 5000+ GPU-instanced dots and animated pulses. Globe-to-zoom is bounded to z=18 so users can zoom from continent to individual buildings.
featURL state + shareable views + embed mode
Active layers, zoom, and center serialized to the URL query string (?l=&z=&lat=&lng=) and restored on load. Share button (↗) copies the current state URL. ?embed=1 drops the HUD chrome so the page can be iframed on other sites.
infraAtomic deploy pipeline with rollback
Root-owned /usr/local/sbin/und3r-deploy writes a sudoers whitelist so the unprivileged deploy user can redeploy and manage services without a password but cannot inject arbitrary commands. Atomic site swap, tarball backup, nginx config rollback on nginx -t failure.