Skip to content

Widgets: weather and location from the profile coordinates (stacked on #153) - #156

Merged
Adron merged 2 commits into
mainfrom
issue-119-weather-location
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-119-weather-location

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Stacked PR. This targets issue-118-widgets (#153), not main — it adds two cards to the widget rail that PR introduces and reuses its collapsible / refreshable / quiet-placeholder machinery. Review or merge #153 first; the diff here is only the weather and location work.

Live probe evidence (2026-09-16, real API, test account)

Both endpoints are bearer-reachable and need nothing but a coordinate pair.

Request Status Body
GET /api/weather (bare) 400 {"error":"Latitude and longitude are required","code":"bad_request"}
GET /api/weather?latitude=47.6062 (one of the pair) 400 same — one coordinate counts as missing
GET /api/weather?latitude=abc&longitude=def 400 {"error":"Invalid latitude or longitude","code":"bad_request"}
GET /api/weather?latitude=47.6062&longitude=-122.3321 200 see below
GET /api/location (bare) 400 {"error":"Latitude and longitude are required","code":"bad_request"}
GET /api/location?latitude=47.6062&longitude=-122.3321 200 see below

Query-parameter names — from the live OpenAPI spec, not guessed

/api/weather   -> latitude, longitude, extended, refresh
/api/location  -> latitude, longitude

These are the long spellings — which matters, because the sibling /api/widgets/bike-share in #153 uses lat/lon instead. The two families genuinely disagree and neither can be inferred from the other; don't normalise them onto one spelling without re-probing.

Real payload shapes (key union across every row of every response captured)

GET /api/weather?latitude=47.6062&longitude=-122.3321
{"location":"Seattle","temperature":72,"condition":"Sunny","conditionIcon":"bx-sun",
 "high":72,"low":72,"humidity":null,"windSpeed":8,"timeZone":"America/Los_Angeles"}

GET /api/weather?...&extended=true    adds:
 hourly[25] {startTime, temperature, probabilityOfPrecipitation, shortForecast}
 weekly[14] {name, startTime, isDaytime, temperature, probabilityOfPrecipitation, shortForecast, icon}

GET /api/location?latitude=47.6062&longitude=-122.3321
{"city":"Seattle","state":"WA","country":"United States",
 "coordinates":{"latitude":47.6062,"longitude":-122.3321},"timezone":"America/Los_Angeles"}

Endpoints that returned errors or empty data

  • Both are United States only. This was the main thing worth pinning down, since the brief warned some endpoints might be region-gated to Seattle. They are not Seattle-gated — they are NWS-coverage-gated:

    Point /api/weather /api/location
    Seattle 47.6062,-122.3321 200 Seattle 200 Seattle, WA
    NYC 40.7128,-74.0060 200 New York, 78° 200 New York, NY
    Chicago 41.8781,-87.6298 200 Chicago, 69° 200 Chicago, IL
    Anchorage 61.2181,-149.9003 200 Anchorage, 58° 200 Anchorage, AK
    Honolulu 21.3069,-157.8583 200 Urban Honolulu, 88° 200 Urban Honolulu, HI
    Toronto 43.6532,-79.3832 404 404
    Tokyo 35.6762,139.6503 404 404
    0,0 (mid-ocean) 404 404
    91,200 (out of range) — 404

    404 bodies are {"error":"Failed to fetch weather data","code":"not_found"} / {"error":"Failed to fetch location data","code":"not_found"}. Both ViewModels catch 404 specifically and report "No weather data for your location." / "Your location could not be resolved." rather than letting it fall into the generic failure placeholder — it's an ordinary empty state, not an outage.

  • humidity is present in the envelope but was null in all seven observed responses, across five cities. Modelled and rendered if it ever appears, documented as not expected.

  • extended=true inflates the payload ~37× (166 → 6178 bytes) for forecast arrays a one-line rail card doesn't render, so it isn't requested. Note weekly has 14 rows, not 14 days — NWS alternates day and night periods, so that's ~7 days.

  • refresh=true is accepted (200) but the widget's refresh button deliberately does not send it: the whole point of the 30-minute server cache is to shield the NWS upstream, and a user tapping refresh shouldn't punch through it.

  • Casing inconsistency upstream: weather returns timeZone, location returns timezone. Absorbed only because the client's JsonSerializerDefaults.Web options are case-insensitive — flagged in both models so nobody "fixes" one to match the other.

Models proven to deserialize, not eyeballed

The same throwaway net10.0 harness outside the repo from #153, extended with the new types — <Compile Include>s the model files by absolute path and runs the captured payloads through new JsonSerializerOptions(JsonSerializerDefaults.Web):

== WeatherConditions ==
  weather_coords   loc=Seattle        temp=72 cond=Sunny                      H/L=72/72 wind=8  humidity=<null> tz=America/Los_Angeles  hourly=0 weekly=0
  wx_refresh       loc=Seattle        temp=72 cond=Sunny                      H/L=72/72 wind=8  humidity=<null> tz=America/Los_Angeles  hourly=0 weekly=0
  wx_nyc           loc=New York       temp=78 cond=Mostly Sunny               H/L=78/71 wind=8  humidity=<null> tz=America/New_York     hourly=0 weekly=0
  wx_chicago       loc=Chicago        temp=69 cond=Scattered Rain Showers     H/L=69/66 wind=10 humidity=<null> tz=America/Chicago      hourly=0 weekly=0
  wx_anchorage     loc=Anchorage      temp=58 cond=Slight Chance Rain Showers H/L=58/58 wind=15 humidity=<null> tz=America/Anchorage    hourly=0 weekly=0
  wx_honolulu      loc=Urban Honolulu temp=88 cond=Scattered Rain Showers     H/L=88/88 wind=15 humidity=<null> tz=Pacific/Honolulu     hourly=0 weekly=0
== WeatherConditions (extended=true) ==
  hourly=25  weekly=14
    hourly[0] start=2026-09-16 20:00:00Z temp=69 pop=0 fc=Sunny
    hourly non-null: start=25 temp=25 pop=25 fc=25
    weekly[0] name=This Afternoon day=True temp=72 pop=0 fc=Sunny
    weekly non-null: name=14 isDaytime=14 icon=14 start=14
== LocationInfo ==
  location_coords  city=Seattle        state=WA country=United States  tz=America/Los_Angeles  coords=47.6062,-122.3321
  loc_nyc          city=New York       state=NY country=United States  tz=America/New_York     coords=40.7128,-74.006
  loc_chicago      city=Chicago        state=IL country=United States  tz=America/Chicago      coords=41.8781,-87.6298
  loc_anchorage    city=Anchorage      state=AK country=United States  tz=America/Anchorage    coords=61.2181,-149.9003
  loc_honolulu     city=Urban Honolulu state=HI country=United States  tz=Pacific/Honolulu     coords=21.3069,-157.8583

ALL MODELS DESERIALIZED OK

Every startTime in both forecast arrays round-tripped into DateTimeOffset (25/25 and 14/14 non-null), so the offset-bearing ISO strings parse cleanly.

Acceptance criteria

  • Both service methods take lat/long from the profile. They take them as explicit parameters — the preferred option in the brief. GetWeatherAsync(GeoPoint at, bool extended, bool refresh, ct) and GetLocationAsync(GeoPoint at, ct). The UI sources them from ProfileLocationProvider, which reads latitude/longitude off the raw GET /api/user envelope.
  • With no profile location set, the widgets prompt to set one in Settings instead of showing an error: "Set your location in Settings to see the weather." / "…to see it here." Neither issues a request that would 400.
  • Refresh no more often than the 30-minute server cache warrants. WeatherWidgetViewModel.CacheFor is exactly 30 minutes, matching the documented server-side cache; nav switches, expand/collapse and repeated load-alls inside the window are free. Location caches for 6 hours — a geocode of a fixed point doesn't move.
  • Dependency on the profile-location work avoided rather than waited on. See below.

Notes

  • Models/CurrentUser.cs is untouched. Open PR fix(models): reconcile Message and CurrentUser against live payloads #131 adds Latitude/Longitude there; this branch does not need it and does not conflict with it. ProfileLocationProvider → GetProfileLocationAsync reads the two fields off the raw GET /api/user JSON, and the XML doc notes it can collapse into CurrentUser once those properties land.
  • No OS geolocation, per the product docs — the web's browser-permission flow has no desktop equivalent, so the stored profile location is the parity-appropriate source. Documented on GeoPoint and ProfileLocationProvider so the decision survives.
  • Shares one coordinate lookup. ProfileLocationProvider is semaphore-gated and cached, so these two cards plus transit and bike-share cost a single GET /api/user between them, not four.
  • The Location card also shows the current local time in the resolved IANA zone. TimeZoneInfo.FindSystemTimeZoneById is guarded — .NET on Windows understands IANA ids from .NET 6 on, but a machine with stale ICU data can still miss one, and a widget must not throw over a timezone name.
  • Temperatures render as a bare 72°: the API proxies the U.S. National Weather Service and reports Fahrenheit, but the payload never states a unit, so the UI doesn't claim one.
  • conditionIcon (bx-sun, bx-cloud-rain) is a web BoxIcons class the desktop app doesn't ship — retained on the model for fidelity, not rendered; condition text is what's shown.
  • Strata tokens only; 4px corners, 4pt grid. Nothing blocks the dispatcher.

Build

Clean in both configurations, 0 warnings / 0 errors, with --no-incremental:

dotnet build InterlinedList/InterlinedList.csproj -c Debug   --no-incremental   ->  Build succeeded. 0 Warning(s) 0 Error(s)
dotnet build InterlinedList/InterlinedList.csproj -c Release --no-incremental   ->  Build succeeded. 0 Warning(s) 0 Error(s)

Closes #119

🤖 Generated with Claude Code

Two more cards on the widget rail from #118, using the same collapsible /
refreshable / quiet-placeholder machinery.

Coordinates are explicit method parameters on the service rather than being
read out of a CurrentUser field inside it. That keeps the service testable and,
deliberately, keeps this change independent of the in-flight work adding
Latitude/Longitude to Models/CurrentUser.cs — which this branch does not touch.
The UI sources them from ProfileLocationProvider (one semaphore-gated cached
GET /api/user shared with the transit and bike-share cards, so the two new
widgets cost no extra request).

No OS geolocation, per the product docs: the web's browser-permission flow has
no desktop equivalent, so the stored profile location is the parity-appropriate
source. With no coordinates set, both cards point the user at Settings instead
of erroring.

Live findings from probing on 2026-09-16:

- Parameter names are the LONG spellings, latitude/longitude, from the live
  OpenAPI spec — unlike the bike-share widget's lat/lon. The two families
  genuinely differ; neither can be inferred from the other. Weather also takes
  `extended` and `refresh`.
- Both endpoints are UNITED STATES ONLY. Seattle, NYC, Chicago, Anchorage and
  Honolulu all returned 200; Toronto, Tokyo, lat/lon 0,0 and out-of-range
  coordinates all returned 404 "Failed to fetch weather/location data". That is
  the National Weather Service's coverage showing through the proxy, so a 404
  is caught specifically and reported as "no data for your location" rather
  than falling into the generic failure placeholder.
- Only one of the pair, or non-numeric values, is a 400 as documented.
- `extended=true` adds hourly[25] and weekly[14] (NWS alternates day/night
  periods, so 14 rows is ~7 days) and inflates the payload ~37x, 166 -> 6178
  bytes. The rail shows one line, so it is not requested.
- `humidity` was null in all seven observed responses across five cities. It is
  modelled and rendered if it ever appears, but is not expected.
- Weather returns `timeZone`, location returns `timezone` — absorbed only
  because the client's JsonSerializerDefaults.Web options are case-insensitive.

Weather is cached server-side for 30 minutes, so the widget's cache window
matches it exactly and the refresh button intentionally does NOT send
refresh=true — a user tapping refresh should not punch through to the upstream.
Location is a geocode of a fixed point, so it caches for six hours.

Closes #119

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Adron
Adron changed the base branch from issue-118-widgets to main September 24, 2026 06:12
@Adron
Adron merged commit f9c9536 into main Sep 24, 2026
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.

Widgets: weather and location from the profile coordinates

1 participant