Skip to content

Stop asking for a postal code on every check-in - #899

Draft
CoryMCodes wants to merge 1 commit into
rubyforgood:masterfrom
CoryMCodes:573-weather-location-issue
Draft

Stop asking for a postal code on every check-in#899
CoryMCodes wants to merge 1 commit into
rubyforgood:masterfrom
CoryMCodes:573-weather-location-issue

Conversation

@CoryMCodes

Copy link
Copy Markdown
Collaborator

Weather is meant to be entered once: Checkin::Creator copies the previous
check-in's position forward and re-fetches the forecast for the new day, and
the weather step has a branch that renders the stored location as a tappable
element to change it. In practice users were re-typing their postal code
day after day, and re-typing it did not make the prompt go away.

Two independent defects, one on each side.

WeatherRetriever asked for the wrong day and then cached the answer under a
key it would never look up again. The Dark Sky call this replaced in #689
passed time: for the requested date, resolved in the position's own zone;
tomorrowio_rb's forecast(location, timesteps, units) has no date parameter
at all, so the argument became decorative: the request always returned the
timeline starting at today, and the record was stored under whatever date the
response led with. Weather.find_by(date:, position_id:) therefore missed on
every subsequent call for that date, the re-fetch tripped the date/position
uniqueness validation, and because the write went through Weather.create
the failure was silent and handed back an unsaved record. Checkin::Creator
stores ...get(date, postal_code)&.id, so nil id became "this check-in has
no weather". Rows are keyed by (date, position) and shared by every user with
that postal code, so one fetch of today was enough to poison any request for
a different date — which is every back-filled check-in, and every check-in by
a user whose local day is not the UTC day.

The retriever now selects the day it was asked for out of the timeline,
comparing dates in the position's time zone (NearestTimeZone, as before the
migration) so eastern-hemisphere positions stop landing a day early, and
stores it under the requested date so the cache is reachable. A losing race
returns the record that won rather than an unsaved one. A date outside the
window logs and returns nothing: the forecast endpoint has no history, and
filing today's forecast under a back-filled day is worse for a tracker that
charts weather against symptoms than having no row. Back-filled days
consequently show no weather now, and say so.

The frontend asked for a location whenever there was no weather, which is not
the same question. willRender forced inputVisible true on every render
pass while hasWeather was false, so a check-in with a stored location was
still prompted, and the set(..., false) after a successful save was undone
by the next pass — the input could not be dismissed. Nothing else in the app
displays locationName, so it also looked like the save had not happened,
and a submission that geocoded fine but had no forecast reported "We couldn't
find that location".

inputVisible is now derived from hasLocation and whether the user opened
the input, so it is no longer re-decided during render. A weather query that
resolves empty or rejects no longer stops the location from being saved. The
two failure modes are told apart by comparing the postal code the API echoes
back — it only returns one once it has geocoded it into a position — and a
day with a location but no forecast shows the location plus a note.

Also in scope by necessity: get_icon_legacy read string keys out of a body
parsed with symbolize_names: true, so every icon was "default" and every
summary read "General conditions are default, with an average temperature of
X". Specs asserting the intended forecast fields could not have been written
around that. And the component now declares store: service() instead of
leaning on the app-wide component/store injection, which does not exist in
component tests.

The creator spec stubbed WeatherRetriever.get and asserted it was called,
which is why none of this showed up in CI; it now goes through the retriever
against the cassette, and separately pins that the location carries forward
even when the forecast does not. Backend 334 examples, frontend 460 tests,
standardrb, erblint and eslint all clean. Reverting each fix in isolation
fails 7 of the 18 retriever examples and 6 of the 9 component tests.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

  Weather is meant to be entered once: Checkin::Creator copies the previous
  check-in's position forward and re-fetches the forecast for the new day, and
  the weather step has a branch that renders the stored location as a tappable
  element to change it. In practice users were re-typing their postal code
  day after day, and re-typing it did not make the prompt go away.

  Two independent defects, one on each side.

  WeatherRetriever asked for the wrong day and then cached the answer under a
  key it would never look up again. The Dark Sky call this replaced in rubyforgood#689
  passed `time:` for the requested date, resolved in the position's own zone;
  tomorrowio_rb's `forecast(location, timesteps, units)` has no date parameter
  at all, so the argument became decorative: the request always returned the
  timeline starting at today, and the record was stored under whatever date the
  response led with. `Weather.find_by(date:, position_id:)` therefore missed on
  every subsequent call for that date, the re-fetch tripped the date/position
  uniqueness validation, and because the write went through `Weather.create`
  the failure was silent and handed back an unsaved record. Checkin::Creator
  stores `...get(date, postal_code)&.id`, so nil id became "this check-in has
  no weather". Rows are keyed by (date, position) and shared by every user with
  that postal code, so one fetch of today was enough to poison any request for
  a different date — which is every back-filled check-in, and every check-in by
  a user whose local day is not the UTC day.

  The retriever now selects the day it was asked for out of the timeline,
  comparing dates in the position's time zone (NearestTimeZone, as before the
  migration) so eastern-hemisphere positions stop landing a day early, and
  stores it under the requested date so the cache is reachable. A losing race
  returns the record that won rather than an unsaved one. A date outside the
  window logs and returns nothing: the forecast endpoint has no history, and
  filing today's forecast under a back-filled day is worse for a tracker that
  charts weather against symptoms than having no row. Back-filled days
  consequently show no weather now, and say so.

  The frontend asked for a location whenever there was no weather, which is not
  the same question. `willRender` forced `inputVisible` true on every render
  pass while `hasWeather` was false, so a check-in with a stored location was
  still prompted, and the `set(..., false)` after a successful save was undone
  by the next pass — the input could not be dismissed. Nothing else in the app
  displays `locationName`, so it also looked like the save had not happened,
  and a submission that geocoded fine but had no forecast reported "We couldn't
  find that location".

  `inputVisible` is now derived from `hasLocation` and whether the user opened
  the input, so it is no longer re-decided during render. A weather query that
  resolves empty or rejects no longer stops the location from being saved. The
  two failure modes are told apart by comparing the postal code the API echoes
  back — it only returns one once it has geocoded it into a position — and a
  day with a location but no forecast shows the location plus a note.

  Also in scope by necessity: get_icon_legacy read string keys out of a body
  parsed with `symbolize_names: true`, so every icon was "default" and every
  summary read "General conditions are default, with an average temperature of
  X". Specs asserting the intended forecast fields could not have been written
  around that. And the component now declares `store: service()` instead of
  leaning on the app-wide component/store injection, which does not exist in
  component tests.

  The creator spec stubbed WeatherRetriever.get and asserted it was called,
  which is why none of this showed up in CI; it now goes through the retriever
  against the cassette, and separately pins that the location carries forward
  even when the forecast does not. Backend 334 examples, frontend 460 tests,
  standardrb, erblint and eslint all clean. Reverting each fix in isolation
  fails 7 of the 18 retriever examples and 6 of the 9 component tests.

  Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CoryMCodes
CoryMCodes marked this pull request as draft August 31, 2026 17:56
@suttondemlong

suttondemlong commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Sean mentioned over the weekend at Ruby for Good that we can just push directly instead of from forked repos since we are contributors. It should help with the feedback loop a bit

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.

2 participants