Skip to content

LF-5298: Show a 5-day weather forecast with frost-risk warnings on the Home page#4178

Merged
kathyavini merged 44 commits into
integrationfrom
LF-5298-show-a-5-day-weather-forecast-with-frost-risk-warnings-on-the-home-page
Jun 5, 2026
Merged

LF-5298: Show a 5-day weather forecast with frost-risk warnings on the Home page#4178
kathyavini merged 44 commits into
integrationfrom
LF-5298-show-a-5-day-weather-forecast-with-frost-risk-warnings-on-the-home-page

Conversation

@litefarm-pr-bot
Copy link
Copy Markdown
Collaborator

@litefarm-pr-bot litefarm-pr-bot commented May 21, 2026

Description

Implement the Home page weather widget.

  • Update the weather API
  • WeatherForecast container (container) — API call, state, data formatting (utils.ts)
  • PureWeatherForecast (stories)
  • TimeStrip (reusable) (stories)

Data Flow

  1. API returns flat list of slots: { slots: [{ dt, temp, ... }], city: { timezone... } }
  2. Frontend groups slots into days (for the day pill selection):
    { localYmd: "2026-05-27", slotIndices: [0..7], isFrost: false }
  3. Selecting a day passes only that day's slots to TimeStrip with relative indices
    e.g. day "2026-05-28" → slots[8..15], selectedSlotIndex 8 → relative index 0
Original description by Claude The Home page weather widget has been a single-snapshot read of OpenWeather's current-conditions endpoint since the original implementation, and the design system has since moved on to a 5-day forecast model that includes frost-risk surfacing. The legacy code coupled the displayed measurement system (metric/imperial) into the OpenWeather request itself, which forced the frost threshold to be expressed twice (once for each unit) and prevented a single, authoritative metric trigger. The widget also derived all date labels from the browser timezone, so a farm in India viewed from a North-American browser saw days mislabelled.

This change replaces the legacy WeatherBoard with a new WeatherForecast widget under containers/WeatherForecast/ (smart, RTK-Query backed) and components/WeatherForecast/ (presentational, composed of DayPillRow, FrostBanner, DayWeatherSummary, TimeStrip, and a PureWeatherForecast shell). The backend endpoint constant moves from /data/2.5/weather to /data/2.5/forecast, the request hard-codes units=metric, and the controller stops reading row.units.measurement. The frontend converts to display units client-side via pure helpers in containers/WeatherForecast/selectors.ts. The WeatherData shared type is replaced by WeatherForecast (with slots[] and city.timezoneOffsetSeconds), and the RTK Query endpoint is renamed useGetWeatherQueryuseGetWeatherForecastQuery. Day grouping, day-pill labels and time-chip labels use the offset returned by OpenWeather (city.timezone), never the browser timezone.

Pattern to carry forward: when a backend response shape changes in a way that affects an RTK Query hook's name or argument, the breaking edit and every consumer must ship together — the previous one-call API made it tempting to split the rewrite across PRs, but the type rename and Home wiring are inseparable in practice and would leave the legacy widget broken in an interim merge.

Jira: https://lite-farm.atlassian.net/browse/LF-5298

How Has This Been Tested?

  • Passes test case
  • UI components visually reviewed on desktop view
  • UI components visually reviewed on mobile view
  • Other (please explain)

Type checking (tsc --noEmit) and ESLint were run against the worktree; both reported only pre-existing errors in unrelated files (e.g. stories/Pages/Intro/SetCertificationSummary/CertificationSummary.test.js, stories/Tile/SensorKPITiles.stories.tsx). No new errors were introduced. A non-printable-byte scan (LC_ALL=C perl -ne ...) confirmed all new files contain only the precomposed U+00B0 DEGREE SIGN for the °C/°F glyphs, with no combining diacritics or NUL bytes. No local frontend or backend was started; visual review on Home and Storybook, golden-path slot navigation, balloon-tail anchoring on day-pill rollover, mobile (320–375px) horizontal time-strip overflow, metric-to-imperial unit switching, and locale switching (Hindi, Punjabi) for date string localisation are deferred to manual QA.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The precommit and linting ran successfully
  • I have added or updated language tags for text that's part of the UI
  • I have ordered translation keys alphabetically (optional: run pnpm i18n to help with this)
  • I have added the GNU General Public License to all new files

Swap the legacy WeatherBoard for a new WeatherForecast widget driven by
OpenWeather's /data/2.5/forecast endpoint. The widget shows a 5-day /
3-hour forecast with five day pills, a horizontal time strip of 3-hour
slots, a shared rollover selection (one slot index drives a derived day
pill highlight), and a red frost-risk banner whenever the day's minimum
forecast temperature is below 2°C.

Backend always requests metric units; the frontend converts on display
using the farm's measurement setting. All local-day grouping, day pill
labels and time chip labels use the farm timezone from city.timezone in
the OpenWeather response rather than the browser timezone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@litefarm-pr-bot litefarm-pr-bot requested review from a team as code owners May 21, 2026 19:41
@litefarm-pr-bot litefarm-pr-bot requested review from kathyavini and removed request for a team May 21, 2026 19:41
@SayakaOno SayakaOno marked this pull request as draft May 22, 2026 21:37
@SayakaOno SayakaOno added enhancement New feature or request new translations New translations to be sent to CrowdIn are present labels May 26, 2026
@SayakaOno SayakaOno marked this pull request as ready for review June 2, 2026 20:51
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fantastic, and with so many difficult date/time details sorted out 🙇❤️ I also appreciate the extra accessibility added to this one with aria-pressed and the labels; it feels much better than our normal default!

I went down a bit of a rabbit hole with the local storage litefarm_lang (comment below) which was totally inconclusive -- maybe you could set me straight 😅 The last time I did a lot of localization testing I swear it was very easy to produce dates in Japanese throughout app but that seems to have changed??


Also on localization, just for future reference

As of today it's appears still completely broken in Punjabi. But it's 100% desktop Chrome browser and not the component, since it works perfectly on mobile:

Desktop:
Image

Phone:
Image

}

export function localTimeOfDay(utcMs: number, offsetSeconds: number): number {
const localMs = (utcMs + offsetSeconds) * 1000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay seeing these side-by-side really threw me for a loop for a moment given the matching variable name utcMs, but different parentheses 😅

I think the math is all correct, but the variable name for this second function should be utcSeconds as it is multiplied by 1000 here. The top one is actually in ms, though.

};

export function formatTimeChipLabel(utcMs: number, offsetSeconds: number, locale: string): string {
const localMs = (utcMs + offsetSeconds) * 1000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this utcMs should also be utcSeconds

slots={visibleSlots}
offsetSeconds={offsetSeconds}
system={system}
locale={i18n.language}
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an interesting choice by Claude to move us onto i18n.language. I think this will be the first feature that uses this instead of getLanguageFromLocalStorage(). I was assuming that those can be different because I used to see this a lot when testing back in the day (i.e. Japanese dates on home page):

Screenshot 2026-06-04 at 11 45 42 AM

I can still see this for an account with ja as my user table language_preference if I submit an unrelated change on the "My Info" form, but it doesn't survive reload and it doesn't show this when I first log in. I'm quite sure it used to 🤔 Do you know if this behaviour has changed, maybe with the i18n changes we pushed for offline? Are you still able to see this for some accounts?

Only if so, I'm worried it will feel like a regression -- the dates will have appeared in native language on the old Weather Board, but will be strictly in English now -- and maybe we should check with Loïc? But my concern might be out-of-date if i18n is updating local storage differently now.

Edit: Another example: dates like this in transactions... this I remember seeing commonly as well:

スクリーンショット 2026-06-04 午後1 14 46

(As a side note I wanted to check what we had picked for the date internationalization in Farm Notes, only to realize we didn't internationalize that date string at all! 😱 Oops! I will make a Jira ticket)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate you testing this and finding that bug!!
I'll replace i18n.language with getLanguageFromLocalStorage for consistency!


export const weatherService = {
async getWeather({ lat, lon, units = 'metric' }: WeatherParams): Promise<WeatherData> {
async getWeather({ lat, lon }: WeatherParams): Promise<WeatherForecast> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not from this PR at all and it's just a minor preference, but what do you think of naming this weatherService.getWeather (which is called from weatherController.getWeather) something distinct?

It just made IDE code search / click-follow a hair more disorienting than it had to be that the two functions had the exact same name. Could the service method be called something like fetchForecast or fetchWeatherForecast?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

});
let label = formatter.format(date);
if (date.getUTCMinutes() === 0) {
label = label.replace(/:00/, '');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it got the go-ahead from Loïc as not looking weird, but I'm with you on this one -- the plain numbers 2 5 8 in the time strip throw me off when in es or fr! I don't feel like I'm looking at times at all.

Maybe this is something we can double-check with at least one Spanish speaker in bug bash to make sure this is universally understood 😉

Alternatively / maybe simpler... would it be hard to affix the :00 back on ONLY when not in am/pm time? That might be another option that I think Loïc would like if he saw, probably even better than the plain numbers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've update the function to keep :00 conditionally. I like it much better 😁

Copy link
Copy Markdown
Collaborator

@SayakaOno SayakaOno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you Joyce for reviewing! I'm sorry that I confused you with the seconds/milliseconds mix-up 🙏 I think I've addressed all your comments.

For the Punjabi issue (https://issuetracker.google.com/issues/40679433), I don't see any updates from the past year, and it looks like only 10 people have voted, so I don't think we can get our hopes up 😞
I don't think we had any Punjabi users last time we checked, but it looks like there's one currently. I wonder if we should fall back to English when the user is on Chrome desktop and their language preference is Punjabi.
There seems to be some time before the release, so maybe we could ship it?

const value = convert(rainMm + snowMm)
.from('mm')
.to(unit);
const displayValue = system === 'metric' ? value : Math.round(value * 1000) / 1000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • metric: display the data as-is (2 decimal places)
  • imperial: round to 3 decimal places

It looks like I'd forgotten to submit this...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I complete forgot! 😳

And my testing farm did have one of those crazy rain amounts that needed all three decimal places 🙏

Screenshot 2026-06-05 at 3 38 10 PM

slots={visibleSlots}
offsetSeconds={offsetSeconds}
system={system}
locale={i18n.language}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate you testing this and finding that bug!!
I'll replace i18n.language with getLanguageFromLocalStorage for consistency!


export const weatherService = {
async getWeather({ lat, lon, units = 'metric' }: WeatherParams): Promise<WeatherData> {
async getWeather({ lat, lon }: WeatherParams): Promise<WeatherForecast> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

});
let label = formatter.format(date);
if (date.getUTCMinutes() === 0) {
label = label.replace(/:00/, '');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've update the function to keep :00 conditionally. I like it much better 😁

Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh that :00 change really does make such a nice difference 😍

For the Punjabi issue (https://issuetracker.google.com/issues/40679433), I don't see any updates from the past year, and it looks like only 10 people have voted, so I don't think we can get our hopes up 😞

Thank you for the link, I have upvoted it! 🤣

I wonder if we should fall back to English when the user is on Chrome desktop and their language preference is Punjabi.
There seems to be some time before the release, so maybe we could ship it?

I would really like to do this, yes!!! But then I get concerned about ruining the working mobile? I think there is a pre-existing ticket so I'll add it into the epic as a reminder to figure something out if there is time.

const value = convert(rainMm + snowMm)
.from('mm')
.to(unit);
const displayValue = system === 'metric' ? value : Math.round(value * 1000) / 1000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I complete forgot! 😳

And my testing farm did have one of those crazy rain amounts that needed all three decimal places 🙏

Screenshot 2026-06-05 at 3 38 10 PM

@kathyavini kathyavini added this pull request to the merge queue Jun 5, 2026
Merged via the queue into integration with commit 4a897e3 Jun 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new translations New translations to be sent to CrowdIn are present

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants