- New src/lib/api/moon.js: fetches daily moon_phase from Open-Meteo
(independent of the NWS/Open-Meteo weather source, since NWS has no moon
data), plus pure helpers: moonName (8 phases), illumination fraction, and
nextMoonPhase, which unwraps the ~29.5-day cycle and interpolates the next
quarter boundary (new/first quarter/full/last quarter), returning its name,
date, and days until it.
- New MoonPhaseCard.svelte: renders an SVG moon (two-circle terminator that
correctly shows crescent/quarter/gibbous shapes), the current phase name,
percent illuminated, and the next phase with date. Caches per-location via
an effect; degrades gracefully when unavailable.
- Wired into WeatherDetail between Sun and UV and Temperature Range.
- 11 unit tests for moonName/illumination/interpolation/nextMoonPhase.
- Verified in headless Chromium: card renders, moon shape correct, no errors.
- manifest.webmanifest + apple/maskable PNG icons; relative paths so the
app works both at dev root and under /weather
- plain-JS service worker (sw.js) with notificationclick/push handlers and
an SW-mediated showNotification() path for native Android notifications
- src/lib/pwa.js bootstraps registration and routes alerts to the SW
- Every alert kind (storm/precip/heat/cold/uv/wind) is now a per-type
preference toggle in Settings; removed the old alertThresholds.thunderstorm
boolean in favor of notificationTypes.storm
- Store dedups native notifications per forecast day and fires best-effort
native notifications for newly-appeared alerts
- inline-assets keeps manifest/sw/icons in dist; deploy.yml uploads + verifies
all PWA files via WebDAV
- New src/lib/router.js: parseHash/ viewToHash for the 5 tab views, with a
default fallback for empty/unknown hashes.
- App.svelte: initialize viewMode from location.hash, setView() updates both
state and the URL hash, and a guarded hashchange listener syncs back/forward
and manual URL edits. All tab onclicks route through setView().
- Tests for parse/viewToHash round-trips and fallback. 76 total pass.
- Lightning layer on PrecipitationRadar: toggleable ⚡ button (persisted via
showLightning setting). When on, fetches recent strikes from the planned CORS
relay and renders them as markers that pan/zoom with the slippy map.
- New src/lib/api/lightning.js: fetchLightningStrikes() reads
https://cors.thecookiejar.me/lightning?lat&lon&radiusKm&limit (relay NOT
deployed yet) with an 8s abort timeout and graceful failure - never blocks or
crashes the map, shows an unavailable banner instead. Exports pure
projectStrikeToLayer() for Mercator tile-layer marker projection.
- showLightning setting added to DEFAULT_SETTINGS + app store; toggle persists.
- Tests: fetch URL/parsing/error handling + layer projection. 71 total pass.