3.10REMOVEDCOVERAGE and NEON layers
- COVERAGE shaded where radar coverage actually exists, to tell “no rain here” apart from “no radar here”. A real distinction, but not one worth a permanent button on a dashboard pointed at one place.
- NEON hue-rotated reflectivity 250° to magenta and violet. It looked the part and it broke the one thing radar colour is for: green-yellow-red is a standardised intensity scale, and rotating it makes every reading wrong. The app warned about this on every toggle, which is a sign the option should not have existed.
- Removed the buttons, the coverage tile source, the per-frame hue rotation, the toggle branch and both config defaults. The radar frames no longer carry a
raster-hue-rotate paint property at all.
- Nine layer buttons became seven. Nothing else moved.
3.00UIRain overlay off by default
- The screen-wide rain effect is now a setting, and it is off. It was original behaviour from 1.0092, not a regression — the rain code is byte-identical to the build as received, apart from
#fx moving to position:fixed during the 2.00 viewport fix.
- Why it read as broken: it keys off precipitation at your saved location, not at whatever the map is showing. Pan somewhere dry while it is raining at home and you get streaks over a still map — noise, not information.
- Turning it off applies immediately rather than waiting for the next conditions refresh.
2.90OPSLoose ends
- The changelog link in the topbar was rendering as a default browser link — blue and underlined, the one thing on the page that looked like 1996. Now cyan with a hairline and a glow on hover, matching the HUD. Same treatment in the sources sheet.
- Added a favicon. The app never declared one, so browsers fell back to a 0-byte placeholder and showed a blank tab. It is a radar sweep with a magenta strike.
- Removed a dead
input[type=password] rule left behind when the pollen key field was cut at 1.10.
- Retired
hashes.py, superseded by deploy.py at 1.80, plus two scratch files.
- Corrected the bundled docs: they still described pollen, the unpkg CDN, Google Fonts and the simulated lightning — all gone since 1.10, 1.20 and 2.80.
2.80NEWLightning is real now
- The LIGHTNING layer was synthetic. It rolled random points inside active NWS severe-storm polygons every 850 ms — reactive to real warnings, but the strike positions were invented.
- Replaced with actual detections: NOAA nowCOAST strike density from the US National Lightning Detection Network and GLD360, 8 km cells over a rolling 15-minute window.
- Usable because NOAA processes the vendor feed into a derived product its own metadata calls appropriate for public distribution. Raw strike feeds are commercial; Blitzortung's terms forbid third-party client access and any storm-warning use.
- It follows the scrubber — page back an hour and the lightning frame goes with it, so radar and lightning never show different times.
- Density, not bolts: where lightning is happening and how hard. Less cinematic than the fake version, and true.
- Removed
tickLightning, startLightning, stopLightning, pointInRing, ringsOf and the severePolys cache. One host added to the CSP; still no key and no server code.
2.70FIXModals fit the screen
- The settings modal grew until SAVE fell off the bottom of the phone. It is now a flex column: fixed header, scrolling body, pinned footer.
- Sized against
100dvh, so mobile browser chrome cannot crop it.
min-height:0 on the body — without it a flex child refuses to scroll no matter what overflow says.
- Same treatment applied to the sources sheet. 44 px tap targets, safe-area padding at the foot.
2.60NEWForecast source switch & uncertainty band
- New setting: AUTO uses your local NWS office for the 24-hour forecast in the US, OPEN-METEO forces the old behaviour. Rain probabilities differ sharply — 27% vs 70% at +12h when first compared.
- New setting: a shaded 31-member GFS ensemble spread behind the temperature line.
- Both default off. Both fail soft — a dead NWS feed falls back and the chart says
OPEN-METEO · NWS UNAVAILABLE rather than blanking.
- The chart now always names the source that drew it.
- Fixed: the existing settings handler was bound to every button in that row, so the new toggles would have set units to
undefined on save.
- CSP gained one host,
ensemble-api.open-meteo.com. No keys, no server code.
2.50UIPanel order
- 7-DAY moved above AIR QUALITY in the control panel.
2.40NEWAir quality graded by pollutant
- The headline US AQI is the worst single pollutant, not an average — so a lone number hides what is driving it. Every pollutant now gets its own sub-index, coloured by its own EPA band, sorted worst first.
- A
DRIVEN BY … line names the culprit; that row is marked DRIVING.
- Added a colour scale strip so the bands are self-teaching.
- Costs nothing — Open-Meteo returned the sub-indices all along, the app was not asking for them.
- Caught before shipping: a stray
mark('conditions') would have reported the conditions feed fresh whenever air quality succeeded.
2.30FIXNext 24h rebuilt
- The chart was
preserveAspectRatio="none" on a fixed 300×74 viewBox stretched to full width — it distorted the curve and the text. The viewBox is now built from the element's real pixel width, one unit per CSS pixel.
- Added a readout strip: NOW, HIGH, LOW and PEAK RAIN, each with the hour it happens.
- High and low are marked and labelled on the curve instead of clipped against the left edge.
- Precipitation moved into its own band with a real 0–100% scale.
- Hour labels moved inside the SVG so they align with the data. Taller on mobile.
2.20FIXA way back from a hidden HUD
- Zen mode hides everything with
class="hud" — including the topbar holding the only button that turns it back on. On desktop H saved you; a phone has no H.
- Added a SHOW HUD button in the upper right, deliberately outside
.hud.
- The bottom hint is a real button now and relabels itself to TAP TO RESTORE HUD on touch.
2.10UIControl panel
- The pull-down is now CONTROL PANEL, with an LED that glows green when deployed and goes dark when stowed.
- The layer buttons (RADAR → NEON) moved inside it as the first row, scrolling sideways, freeing the top of the map.
- Panel order set: layers, alerts, keyframes, next 24h, current, air quality, disclaimer.
2.00MILESTONERenders correctly on a phone
- The map was cropped whenever browser chrome was showing. Two causes:
height:100% measures the layout viewport, which does not shrink for the URL bar; and nothing anywhere called map.resize() except on fullscreen change.
- Switched to
100dvh and pinned the map and CRT overlay to the visible viewport.
- Added real viewport tracking —
resize, orientationchange, pageshow, fullscreen, and crucially visualViewport, the only event that fires reliably as mobile chrome slides.
- This is the first version that looked right on a phone in and out of fullscreen.
1.90NEWMobile layout
- Below 900 px the two side columns, the timeline and the legend fold into one scrollable sheet. Previously the right column was simply hidden, silently costing alerts, the 24-hour chart and the 7-day forecast.
- Card dragging disabled on touch — the handle fought scrolling, and coordinates saved on a desktop put cards off-screen. Saved positions are ignored on mobile and restored untouched on a wide screen.
- Fluid type and spacing scale, each
clamp() keeping a rem term so browser zoom still works.
- Layer rail became a horizontal strip; the timeline lost a 420 px minimum that was wider than the phone.
1.80OPSFingerprinted assets
- Assets were cached a year as
immutable under names that never changed — so an edit could never reach a returning visitor. Each asset now carries a content hash in its filename.
- One-command deploy that fingerprints, rewrites references, recomputes the CSP hashes from the final file, and sweeps superseded copies.
1.70FIXSource map 404
- Stripped MapLibre's trailing
sourceMappingURL rather than shipping another megabyte.
1.60FIXPanels render again
- The CSP hash-pinned
<style> blocks but said nothing about style attributes, which the app sets throughout — so gauge widths, bar colours and card positions were all blocked.
- Added
style-src-attr 'unsafe-inline'. A real if small loosening: CSS only, cannot execute script, and script-src stays hash-pinned.
1.50FIXFonts load
- Font URLs were written relative to the wrong directory —
fonts/… from a stylesheet in assets/css/ resolved one level too deep. Corrected to ../fonts/.
1.40FIXBasemap appears
- The map was black and unlabelled. The CSP allowed
basemaps.cartocdn.com, but that URL only returns a style document — tiles, glyphs and sprites come from tiles.basemaps.cartocdn.com and tiles-a…tiles-d, all blocked.
- Lesson recorded: a policy built by grepping source for hostnames misses hosts discovered at runtime from a fetched config.
1.30SECPublished, hardened
- Live on its own subdomain with a strict CSP:
default-src 'none', script-src pinned by sha256 rather than 'unsafe-inline' — possible because the app has no inline handlers, no eval, no javascript: URLs.
worker-src blob:, without which MapLibre renders nothing.
- HSTS, nosniff, DENY framing, 18 powerful features disabled, HTTPS forced, dotfiles and docs blocked.
1.20SECNo third-party code
- MapLibre was loading from
unpkg.com at a floating @5 — whatever npm served at request time, running with full page privileges. Pinned to 5.24.0 and self-hosted. (npm's latest was already 6.3.0, a major version ahead.)
- Orbitron and Share Tech Mono self-hosted, so Google no longer sees every visitor.
- Zero external subresources remain.
1.10REMOVEDPollen
- Removed entirely — card, layer button, heatmap tiles, fetch, refresh timer, settings field, CSS and error handling. 22 edits, every one asserted.
- That deleted the only API key, the only billable dependency and the only reason to run a server-side proxy. The site stays a pure static file.
1.0092BASELINEAs received
- Fullscreen cyberpunk weather dashboard in one HTML file. Animated radar, storm alerts, current conditions with station-vs-model cross-check, AQI, sparkline, 7-day, IL alerts ticker.
- 119-check smoke suite included. It has not run since — there is no npm on this host.