Skip to content

feat(streamer): weather subscription channel — obs snapshot + windowed tick + on-demand forecasts (#184)#197

Merged
robbiebyrd merged 5 commits into
mainfrom
feat/weather-channel
Jul 13, 2026
Merged

feat(streamer): weather subscription channel — obs snapshot + windowed tick + on-demand forecasts (#184)#197
robbiebyrd merged 5 commits into
mainfrom
feat/weather-channel

Conversation

@robbiebyrd

Copy link
Copy Markdown
Collaborator

Phase 3 of the Weather app (#184): the streamer's weather subscription channel. Clock-synced conditions and forecasts now flow over the WebSocket — live-verified end-to-end against production data (all 8 checks, including KJFK's 9/11 morning at 21.1 °C / FEW / 16 km and the real OKX zone forecast text).

What's in here

  • model/weather.go + 4 Postgres queries — pointer-nullable scanning throughout (no COALESCE-to-zero: 0 °C is a real temperature), DISTINCT ON latest-per-station snapshot, NULL-zone guard on the tick query.
  • Session wiringweather channel const, 600 s window, usenet-shaped Postgres tick branch (lock discipline: plan under mu, query and send outside), one combined weather frame (windowed obs + newly-issued forecast segments), suppressed when empty.
  • Handler — subscribe/init/seek snapshot (latest obs per station ≤ clock, no age limit); on-demand {"type":"weather_forecast","zone","id"} request with ^[A-Z]{2}Z\d{3}$ validation (also blocks LIKE-wildcard injection), silent drop on invalid/unsubscribed, explicit empty reply when no product exists. Also fixes a latent test omission: flights (and weather) added to the all-channels subscribe test.
  • Protocol doc — full wire contract, accuracy-audited claim-by-claim against the code; radar/almanac "not on the wire" callout (static Wasabi + index.json).

⚠️ Two deviations from the design doc (plans/weather-app-design.md) — flagging for review

  1. Postgres-only, no Redis hour buckets. The data is ~34k immutable rows (vs flights' 3.5M); the repo's own usenet precedent (backend CLAUDE.md hard-rule-4 exception) covers exactly this case, and it eliminates the warm/rewarm-after-reload operational surface entirely.
  2. Forecasts are on-demand per zone, not a latest-per-zone snapshot broadcast (~800 KB saved per subscribe; the app shows one station's forecast at a time; newly-issued products still stream forward on the tick, ~2/min).

Verification

go test ./..., -race on the weather tests, vet, gofmt all clean; final review traced mid-tick-seek ordering, Close-during-query, and horizon races (duplicates-only-never-gaps, matching existing channels). Existing frontend untouched — additive-only wire changes (omitempty on both new fields), confirmed no new frames reach clients that don't subscribe.

Phase-4 client contract notes (carried to the frontend PR)

Clear weather state on seek and keep max-start_date per station (late stale windows carry fresh ids); start request ids at 1 (id:0 is omitted by the envelope); pre-init requests silently drop (existing convention).

🤖 Generated with Claude Code

https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c

Robbie Byrd and others added 5 commits July 13, 2026 02:15
Models + Postgres query layer for the new streamer weather channel
(weather_observations/weather_forecasts, populated by
packages/tools/weather-recon). Nullable numerics scan as *float64/*int
straight into the model (no COALESCE, matching item.go's
CalcDuration/Sort precedent); nullable text uses the existing
derefStr pattern. No db-layer tests, per repo convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the flights pattern for the const/window/horizon plumbing and the
usenet pattern for the tick's Postgres read: weather observations and
forecasts are sparse enough (usenet/news window scale) to read Postgres
directly on the tick rather than warming a Redis cache. SendWeather
suppresses empty batches; SendWeatherForecast always sends (even with a
nil forecast) so the client sees an explicit "no forecast for this zone"
rather than ambiguous silence.

Renamed usenetTickTimeout -> pgTickTimeout since weather now shares it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W179bqEPFP92a3JrsPkD5c
Wires the weather channel into the ws.go handler: knownChannel accepts
"weather"; sendWeatherSnapshot (CurrentWeatherObs) is wired into both
sendChannelSnapshot (subscribe) and sendSubscribedSnapshots (init/seek);
a new weather_forecast message validates the zone against the UGC
zone shape before it reaches a SQL LIKE, silently drops when not
subscribed or the zone is invalid (mirroring flights_history's
gating), and otherwise replies via SendWeatherForecast. Also fixes a
known test oversight by adding the previously-missing "flights"
channel (alongside "weather") to
TestWSHandlerAllChannelsSubscribeIndependently.

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

Copy link
Copy Markdown

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

Full report

@robbiebyrd robbiebyrd merged commit 1827ac2 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.

1 participant