Skip to content

feat(frontend): Weather app — radar map, live conditions, archived forecasts & almanac (#184)#198

Merged
robbiebyrd merged 7 commits into
mainfrom
feat/weather-app
Jul 13, 2026
Merged

feat(frontend): Weather app — radar map, live conditions, archived forecasts & almanac (#184)#198
robbiebyrd merged 7 commits into
mainfrom
feat/weather-app

Conversation

@robbiebyrd

Copy link
Copy Markdown
Collaborator

Phase 4 of the Weather app (#184) — the app itself, and the final phase. A Mac OS 8-style Weather desktop app: North America map with the real NEXRAD radar mosaic advancing with the virtual clock, 188 temperature-colored station pins, and a side panel with the selected city's current conditions, the actual archived NWS forecast text, and the 2001 almanac.

What's in here

  • weather channel in MediaStreamProvider — flights-shaped: ref-counted subscribe, reveal-buffered delivery, latest-per-station merge (keep max-start_date; late stale windows can't regress a station), id-echoed on-demand forecast requests, reconnect + seek re-request (the seek path was added after browser verification caught a real anachronism: a 9/11 forecast staying on screen at a 9/9 clock — now cleared and re-requested on every threshold seek, with stale-id replies dropped).
  • WeatherMap — FlightTracker's PMTiles basemap stack reused; radar as a MapLibre image source (bounds from the live index.json), frame-swapped only on 5-minute stamp changes, inserted below borders; pins colored by °F with selection ring.
  • Weather app — conditions in imperial (°F/mph/mi/inHg) with absent-reading dashes (0 °C renders 32 °F, tested), "as of" times in each station's own timezone, forecast three-state UI (retrieving / no product at this hour / full ALL-CAPS product text), almanac records & normals for the current replay date, CA/MX + KHSV no-forecast handling. Icon + desktop registration.

Verification

  • 571/571 vitest, build + lint clean; suite fully network-free.
  • Browser-verified twice against the production streamer: Hurricane Erin visible on the radar at boot, KJFK's clear 9/11 morning (70 °F, FEW, "as of 8:51 AM"), station switching, and the TimeMachine seek to 9/9 now showing the genuine "1004 AM EDT SUN SEP 9 2001" product. Screenshots in the review records.
  • Final review traced provider↔app contract paths (three-state forecast semantics, seek interplay, snapshot races) — zero Critical/Important findings.

Deferred / follow-ups

Radar trailing-hour loop control (design doc's phase-5 polish item); almanac day keyed to UTC date (evening hours show next day's block — derive in display tz); hide radar layer when seeking outside frame coverage; forecasts issued mid-playback appear on reselect/seek only (deliberate v1 cut). Also noted for a future pass: the isRunning gate used by older apps is map-membership set at boot (never reverts) — all channels effectively subscribe at boot; gating on real window-open state would be a cross-app optimization.

Closes #184's app milestone: with PRs #191/#193/#194/#195/#196/#197, the Weather feature is complete end-to-end — curated stations → real archived data → streamer channel → desktop app.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c

Robbie Byrd and others added 7 commits July 13, 2026 03:55
Wires the weather channel (observations + on-demand forecast lookup) into
MediaStreamProvider end-to-end, mirroring the flights channel's plumbing:
ref-counted subscribe/unsubscribe, a reveal buffer for forward-windowed
observations merged latest-per-station, and a generation-id-guarded
forecast request/reply round-trip with an explicit null for "confirmed no
product" vs undefined for "not yet requested/pending".

mergeLatestPerStation compares start_date (not id) per station so a
late/reordered window frame can't regress a station to older data. The
subscribe/init/seek snapshot is treated as an outright per-station
replacement rather than going through that same-or-newer merge rule --
found via TDD that a backward seek's snapshot is often *older* than
what's already held, which the keep-newest rule would otherwise reject,
leaving stale data on screen after a rewind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
…ure pins

stations.json (188 entries, generated from weather-recon's stations.csv),
weatherUnits (cToF/ktToMph/kmToMiles/degToCompass/hpaToInHg), and
weatherRadar (stampForUtcMs/frameUrlFor against the Wasabi radar index
manifest) back a new WeatherMap component copied from FlightTracker's
FlightMap blueprint (protocol registration, init/teardown, resize, live
re-theme) with flight-specific sources/animation stripped and a radar image
overlay + temp-colored station pins added instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
…comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Weather.tsx wires WeatherMap (Task 1) into a ClassicyApp shell: default
station KJFK, subscribes to the weather channel for the app's mounted
lifetime, requests a forecast once per selected station's nws_zone (skipped
for the 36 null-zone CA/MX/KHSV stations), and shows normals/records for the
current virtual MM-DD only inside the 09-09..09-12 almanac window. useAlmanac
follows the useFlightTrack fetch-hook shape (AbortController + per-station
cache, never throws) against the static Wasabi almanac manifest.

tsconfig.json gains resolveJsonModule so Weather.tsx can statically import
stations.json (Task 1 already documented this as the expected consumer).
app.png is a minimal placeholder pixel-art icon so the build/registration
gate passes now — Task 3 (icon, registration, polish) owns generating the
final artwork and wiring <Weather> into app.tsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
…ment

The subscribe-effect comment claimed FlightTracker has a "closed but
mounted" gate — it doesn't (isRunning is map-membership set at boot by
ClassicyAppLoad and never removed on close), so the comment now states the
real architecture: unconditional subscribe-on-mount, like RadioScanner.

New test pins the 0-is-falsy trap: temp_c 0 / wind 0kt @ 0° must render
32°F and "N 0 mph", never the absent-value dash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
Wires <Weather /> into ClassicyDesktop (src/app.tsx) and replaces the
Task 2 PIL placeholder with a proper 32x32 Mac OS 8-flavored icon: a
sun peeking from behind a shaded, outlined puffy cloud.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
A TimeMachine seek past SEEK_THRESHOLD_MS left weatherForecastByZone
holding the pre-seek product — a 9/11 forecast stayed on screen at a 9/9
clock. Unlike per-station observations (replaced by the post-seek weather
snapshot), forecasts are request/reply only, so nothing ever invalidated
them.

The seek effect now clears weatherForecastByZone and re-fires the pending
forecast request for the last-requested zone (no-op when none), mirroring
the reconnect path in ws.onopen. The generation bump orphans any in-flight
pre-seek reply via the existing id-echo guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
@github-actions

Copy link
Copy Markdown

Playwright E2E — 1 passed · 0 failed · 0 flaky · 0 skipped

Full report

@robbiebyrd robbiebyrd merged commit b8f0fdb into main Jul 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add weather radar app and forecasts/almanac data

1 participant