und3rgr0und_ AboutÀ propos ChangelogNouveautés Open the globeOuvrir le globe
Changelog

Everything that changed, session by session.

From a static placeholder to a live 3D globe with 80+ data layers. Newest first; each entry is tagged by kind: new feature, fix, bug, refactor, data or infrastructure.

—sessions
—changes
—new features
—since the first one
s352026-10-25

Security sweep by four review agents

fixAbuse and flood limits on the API
Four independent reviews: PHP endpoints, server, front-end and a full functional pass. No critical issue and no exploitable script injection were found. The weak spots were floods.
  • Replay dates: only a real date between 2000 and today is honoured. Before, any date-shaped string created a new cache file and a keyed NASA call.
  • Feedback: the site’s own Origin is required, with at most 10 messages per IP per day and a size cap on the inbox.
  • Visitor counter: at most 20 new visitors per IP per hour, a capped session log, and the public feed cached for 10 s.
  • Upstreams: the headline bar rebuilds once at a time and serves the previous version meanwhile. A failing feed is retried after a minute instead of on every request. Caches are written atomically.
  • Other caches: share-preview images are keyed on what they show, so arbitrary ?l= values no longer create files. A place card without a town name is cached for 1 min, not 15.
fixHeaders, links and the curfew layer
/changelog.html now gets the same security headers as every other page. The content-security policy drops unsafe-eval and adds form-action 'self'. The server version is no longer announced. A shared link with out-of-range coordinates (?lat=999) no longer crashes the globe. The rain-radar frame path is validated. A curfew reported by a single outlet still appears, but marked unconfirmed, and only corroborated ones (two outlets or a .gouv.fr source) reach the alert card. The France layer keeps its previous data if fewer than 4 of its 6 press searches answer.
bugFour dead layers found by the functional pass
The pass turned on every layer one by one: 70 of 82 returned data, and the rest were legitimately empty (no aurora, markets closed on Sunday) except four.
  • BGP activity is back. RIPE RIS Live stopped matching the list form of its subscription filter.
  • HoneyDB points at its current endpoint (the old one now returns 404).
  • Malware URLs (URLhaus) is set to “soon”: abuse.ch retired its public downloads and now requires an API key.
  • Wikipedia edits is set to “soon”: Wikipedia replaced IP editors with temporary accounts, so there is nothing left to place on a map.
Also fixed: the satellite layers no longer start a new multi-MB download on every one-second tick while the first one is still running. The service worker no longer keeps a copy of every shared link. A robots.txt was added.
infraDeploy hardening
The site code is now owned by root and read-only for PHP, so a bug in an endpoint can’t rewrite the site. A deploy step that could be abused through a planted link was removed. The service worker gets a fresh cache on every release. The SSH-attack map now reads the real peer address, so a crafted username can’t plant a fake dot, and never shows this server’s own IP. A few internal paths were removed from this changelog.
s342026-10-25

France · mobilisation: live protests, curfews and an alert mode

featProtests and clashes across France, from the press, every 10 minutes
A new France · mobilisation group. The server reads French press headlines (Google News, six searches over the last 48 h). It keeps those about protests, rallies, arrests, looting, bans and curfews. It then places each one on the town it names, using the official list of the 2 264 communes with 5 000+ inhabitants (geo.api.gouv.fr) or the département’s largest town. “Rally in Tours after the attack in Trappes” counts for Tours. Each town pulses, more strongly the more it’s in the news. Clicking it opens the counts (protests, clashes, bans), the sources and the latest headlines with links. The map shows where the press reports something; it does not count crowds.
featCurfews that update and expire on their own
No API publishes curfews: they live in prefectural decrees, as PDFs. So the layer reads them from the press too, with no human in the loop. It extracts hours (“de 21h à 6h”), end dates (“jusqu’au lundi 26 octobre”, “les nuits du 24 et 25”, “ce week-end”, “jusqu’à lundi”) and whether only minors are concerned. A town drops off by itself once its end date has passed, when the press reports the curfew lifted or not renewed, or after 24 h without news. Shown at town level: the exact perimeter is in the decree.
featAlert mode: black and red
The new France · live quick view (key 8) flies to France and turns the whole site black and red. The map is re-toned to red, a vignette breathes at the edges, a hazard stripe runs along the top and the logo glitches. An alert card shows the live figures and the towns under curfew, with one tap to fly to each. Exit alert mode turns it all off and flies back to where you were. It works in both interfaces. Nothing flashes faster than once a second, and all motion stops when the system asks for reduced motion.
fixAlert mode: much less red
The first version re-toned the whole map and interface red, which was too much. The map and the panels now stay their usual dark. Red is kept for the signals: the dots on the map, the accent (live badge, switches, active states), a thin hazard stripe, the logo and the edge of the alert card. The pulsing red vignette and red scanlines are gone.
fixFrance layer: better placement and classification
Checked by hand on a random sample of 45 placed headlines: about 93 % were on the right town. Fixed the misses. The scene of an attack or tragedy is no longer taken as the place of a rally: “rally in Montpellier after the attack in Trappes” counts for Montpellier, and “victims of the attack in the Yvelines” no longer lands on Versailles. “Sans heurts”, “dans le calme” and “aucun incident” no longer count as clashes, and “agressions” now does.
s332026-10-03

Security hardening pass

fixNo more ways to overload the shared PHP pool
The "what's here?" card no longer parks a server worker waiting on the geocoder's 1-request-per-second limit — it just skips the place name when the limiter is busy (weather, air and Wikipedia are unaffected). The proxy only varies its cache on a date or language for the few feeds that use them, so a made-up ?d= can't mint endless cache files. place.php and game.php get a tighter request burst, and the stored-visitor list is capped.
fixSmaller things from a security review
Air-quality values are coerced to numbers before display (defence in depth), and each deploy now keeps only the three most recent rollback copies and backups instead of letting them pile up.
s322026-10-03

Live visitors, rebuilt: 48 h, really live, more precise

featWho is online right now
The page used to send one signal per visitor per day, so someone browsing for an hour still looked like a visit from that morning. It now sends a short ping every minute while the tab is visible: online now means online now. Each visitor also gets a last activity time, pages viewed and an approximate time on site.
feat48 h window, grouped by city
The layer covers the last 48 hours instead of 24. Visitors from the same city share one marker (“3 visitors · 1 online”) instead of stacking into a blob: a bright ring with a line to the server while someone is online, a smaller ring within the hour, then a dot that fades over two days. Click one for the place, visitor count, last activity, pages, time on site and how precise the location is.
featMore precise locations, honest counts
Location now comes from Cloudflare’s own geolocation when it’s available, with the free IP database as a fallback, and region names are added. Robots, link previews and headless browsers are no longer counted as visitors.
infraVisitor data survives reboots
Visits were kept in /tmp, wiped on every server restart and capped at the last 250. They now live in a persistent store, one record per visitor for 8 days and one line per visit for 30 days, with no IP address saved. The privacy section of the About page lists exactly what is kept.
s312026-10-01

About and changelog pages rebuilt

featA new About page, in English and French
Rewritten from scratch in the new interface’s style. A tour of what to try (quick views, pinning, “what’s here?”, replay, Mars and the solar system, satellite view, radio roulette, the guessing game, sharing and embedding) and every keyboard shortcut. The full catalogue of sources is now read live from the globe’s own layer list, with a search box and each layer’s refresh rate. The old page listed 22 layers, frozen in May. The privacy section now says exactly what is stored (anonymous visit ping, feedback, 14-day server logs) and which third parties your browser talks to. EN/FR switch shared with the globe.
featThis changelog: filters, search, permalinks
Filter entries by kind (feature, fix, bug, refactor, data, infra), search every entry, jump straight to a session, and link to one with /changelog.html#s12. Readable on phones; both pages follow the globe’s dark / dusk theme.
fixEnergy layer names: last emoji gone, now in French
Energy layers (hydro, solar, wind…) still carried an emoji in their name, in the panel and in the active-layer chips. They now get the same treatment as every other label: hidden in the new interface, unchanged in the old one. In French, their names (hydroélectrique, solaire, éolien, charbon, gaz) and sources are now translated too.
s302026-10-01

New interface "Phosphor console" · old / new UI switch

featA new interface, on by default
Same terminal soul, calmer chrome. Monospace stays for signals, numbers and labels; layer names and sentences switch to a condensed grotesk so the panel reads at a glance. Stroke icons replace the emoji, which some phones and desktops showed as empty boxes. In the panel, every layer row is now a switch, and the presets become a 4 × 2 grid of quick views. Active layers show up as removable chips under the logo. The footer is a single status bar and zoom is a compact cluster next to the panel. A pinned event becomes a card with its details in a grid and a close button. Text contrast meets WCAG AA.
featPhones: bottom sheets and a layers button
The sources panel opens as a bottom sheet with a grab handle, 44 px touch targets and larger switches. When it's closed, a Sources button floats above the footer with the number of active layers. The headline bar no longer overlaps the logo. Active layers become a row of chips you can swipe, and a pinned event opens as a bottom sheet with its own close button.
featOLD UI / NEW UI switch
Prefer the original look? Use the switch at the end of the footer, press u, or open ?ui=old. Your choice is remembered on this device, and ?ui=new brings the new interface back. The new interface lives in its own stylesheet (ui2.css, scoped to html.ui2), so the old interface is still rendered exactly as before.
s292026-10-01

Satellite view · live rain radar · "Where am I?" game · on this day in history

featSatellite view — the real Earth on the globe
A new Satellite view group paints real NASA imagery onto the globe itself (NASA GIBS, no key): yesterday's true-colour photo of the whole planet (VIIRS — storms, deserts, ice, clouds), the Earth at night (Black Marble city lights) and ocean surface temperature (GHRSST, daily). Borders and place names stay on top; turning one on switches the others off.
featAnimated rain radar, worldwide
RainViewer's composite of national weather radars, looped over the last hour (10-minute frames) with a clock showing which frame you're seeing. The newest frame shows instantly; older frames load in the background and join the loop as they arrive.
feat"Where am I?" — a geo-guessing game
Five rounds, five photos of UNESCO World Heritage sites (Wikipedia photos, checked to be geotagged within 60 km of the site). Click the globe where you think it is: a dashed great-circle line shows how far off you were, score up to 5 000 per round, a country hint for half the points, a final recap map with all your guesses, best score saved, and a share button. Start it from 🎯 play in the footer, the welcome card, or the g key.
featOn this day in history
Events that happened on today's date (Wikipedia "On this day", in your language), pinned at the most precise place mentioned — the building or city rather than the whole country when available — and coloured by age, with photo and link.
fixRadio player buttons weren't clickable
The mini player inherited the click-through style of the decorative HUD, so ⏸ / 🎲 / ✕ only worked from the keyboard. Fixed.
s282026-10-01

New light theme: "dusk atlas"

featLight theme redesigned — the Earth finally stands out
The old light theme put a pale globe on a pale background and the planet dissolved into it. The light theme now floats a cream-and-teal Earth (cream land, clear blue-green oceans, sand-coloured borders) on a deep dusk-blue sky, with an atmosphere halo around the globe. Panels stay light and readable; zoomed in, the map keeps the same atlas palette.
s272026-10-01

Public holidays · air quality & photos in the place card

featPublic holidays layer
Countries on a public holiday today glow as gold rings; the next ones in the coming week show as dimmer dots (name, local name, date, nationwide or regional). Source: Nager.Date, refreshed hourly. Part of the Discover preset.
featPlace card: air quality, UV and a photo of what's nearby
Clicking the map now also shows the European air-quality index (colour-coded), PM2.5 and the UV index (Open-Meteo Air Quality), plus the nearest Wikipedia article that has a photo, with its picture and a two-line summary.
s262026-10-01

Collapsible LIVE feed · launch countdowns · zoom glitch fix

featLIVE feed bar — folds into one line, unfolds into a list
The old scrolling ticker is replaced by a compact LIVE pill that rotates through the latest events one at a time. Tap it to unfold a list filterable by Planet / World / Space; tap an item to fly there (the list folds back on mobile). Polled every 60 s instead of 10 min, every item shows a live "x min ago", and new arrivals flash with a +N badge. New sources: 🚀 rocket launches in the next 48 h with a ticking T-hh:mm:ss countdown (Launch Library 2), 🌊 GDACS red/orange alerts (floods, volcanoes, cyclones worldwide) and the top world headlines of the last 24 h (Google News, in your language). Each source has its own cache (quakes 60 s, news 3 min, fires 10 min).
fixDots and cables glitching while zooming
Overlay layers sat exactly on the globe surface and z-fought MapLibre's globe mesh: submarine cables broke into dashes and dots got sliced during zoom. The overlay is now drawn without depth testing (far-side points were already culled in JS). Also: tapping the news bar no longer opens the "what's here?" card behind it, the welcome card is translated, and a fire cluster with no city name no longer shows as ", Australia".
s252026-10-01

public-apis picks · live radios · wildlife · "what's here?" card

featLive radios + radio roulette
~3000 geotagged stations from Radio Browser (HTTPS streams only). Click a dot, hit ▶ and it plays in a mini player at the bottom of the screen. The 🎲 button — or the r key — tunes into a random station somewhere on Earth and flies the globe there; dead streams are skipped automatically.
feat"What's here?" — click anywhere on the globe
Clicking an empty spot opens a place card: name + flag (OpenStreetMap Nominatim), live weather, local time and sunrise/sunset (Open-Meteo), and the nearest Wikipedia articles. Everything goes through /api/place.php, so your IP never reaches a third party. Esc or ✕ closes it.
featWildlife sightings · 🚲 Bike-share networks · 🎧 Discover preset
The latest iNaturalist observations (with photo in the info panel, coloured by group: birds, insects, plants, fungi…) and the ~800 CityBikes bike-share systems. A new Discover preset (key 7) bundles radios, wildlife, bikes and the day/night line.
s242026-07-26

Wildfire overhaul · FR/EN toggle · time-travel replay · 502 outage fix

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 the local listener 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.
s232026-06-16

UI overhaul: search · presets · legend · shortcuts · accessibility

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.
s222026-05-20

3D solar system · real-time ephemeris · daily refresh cron

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 the visitor log 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.
s212026-05-19

Mega-session: 4 phases · 8 layers · stats · PWA · OG · 20+ bug fixes

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 the visitor log, a server-side file, 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 the image cache 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).
s202026-05-18

Public changelog + feedback modal

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 a server-side file. 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.
s192026-05-18

deck.gl half-redrawn "two-globes" seam fix

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.
s182026-05-18

Drag-to-pan fix on globe projection

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).
s172026-05-18

deck.gl 9.3 — round globe + points that stick

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.
s162026-05-18

Full-codebase bug hunt — 14 fixes

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.
s152026-05-18

MapLibre 5.x upgrade — Mars as a real 3D globe

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.
s142026-05-18

Mars debug via live browser session

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.
s132026-05-18

Time-travel replay + Mars/changelog fixes

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.
s122026-05-18

Bug hunt — 7 issues fixed

bugISS layer crashed on null altitude/velocity
space.js:51 — d.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.js — p.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.
s112026-05-18

Massive data + planet switcher

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.
s102026-05-17

Big coord audit + nuclear expansion

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).
s092026-05-17

Time filters + science + culture + conflicts

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_hour → all_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))
s082026-05-16

UX polish

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 Subsolar → Position.
s072026-05-16

Module split + code review

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.
s062026-05-15

English translation

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.
s052026-05-16

9 new layers (round 2)

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).
s042026-05-15

Click-to-pin + light theme readability

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.
s032026-05-15

Moving dots + lightning animation

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).
s022026-05-15

Initial layer ecosystem (23 sources)

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 kept server-side in a root-owned file readable only by PHP: NASA, NASA FIRMS, OpenAQ, AbuseIPDB, HoneyDB, Cloudflare Radar. Each settable via a dedicated --xxx-key= deploy flag.
s012026-05-15

Initial site replacement

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
A root-owned deploy script, callable without a password only with fixed arguments, so the unprivileged account can redeploy without a shell. Atomic site swap, tarball backup, nginx config rollback on nginx -t failure.