LF-5298: Show a 5-day weather forecast with frost-risk warnings on the Home page#4178
Conversation
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>
* display spinner while loading
…t when returning 'Today'
* scrollIntoView when slots are updated * update formatTimeChipLabel function
There was a problem hiding this comment.
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:
| } | ||
|
|
||
| export function localTimeOfDay(utcMs: number, offsetSeconds: number): number { | ||
| const localMs = (utcMs + offsetSeconds) * 1000; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
I think this utcMs should also be utcSeconds
| slots={visibleSlots} | ||
| offsetSeconds={offsetSeconds} | ||
| system={system} | ||
| locale={i18n.language} |
There was a problem hiding this comment.
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):
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:
(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)
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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?
| }); | ||
| let label = formatter.format(date); | ||
| if (date.getUTCMinutes() === 0) { | ||
| label = label.replace(/:00/, ''); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I've update the function to keep :00 conditionally. I like it much better 😁
SayakaOno
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
- metric: display the data as-is (2 decimal places)
- imperial: round to 3 decimal places
It looks like I'd forgotten to submit this...
| slots={visibleSlots} | ||
| offsetSeconds={offsetSeconds} | ||
| system={system} | ||
| locale={i18n.language} |
There was a problem hiding this comment.
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> { |
| }); | ||
| let label = formatter.format(date); | ||
| if (date.getUTCMinutes() === 0) { | ||
| label = label.replace(/:00/, ''); |
There was a problem hiding this comment.
I've update the function to keep :00 conditionally. I like it much better 😁
There was a problem hiding this comment.
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; |


Description
Implement the Home page weather widget.
Data Flow
{ slots: [{ dt, temp, ... }], city: { timezone... } }{ localYmd: "2026-05-27", slotIndices: [0..7], isFrost: false }TimeStripwith relative indicese.g. day "2026-05-28" → slots[8..15],
selectedSlotIndex8 → relative index 0Original 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
WeatherBoardwith a newWeatherForecastwidget undercontainers/WeatherForecast/(smart, RTK-Query backed) andcomponents/WeatherForecast/(presentational, composed ofDayPillRow,FrostBanner,DayWeatherSummary,TimeStrip, and aPureWeatherForecastshell). The backend endpoint constant moves from/data/2.5/weatherto/data/2.5/forecast, the request hard-codesunits=metric, and the controller stops readingrow.units.measurement. The frontend converts to display units client-side via pure helpers incontainers/WeatherForecast/selectors.ts. TheWeatherDatashared type is replaced byWeatherForecast(withslots[]andcity.timezoneOffsetSeconds), and the RTK Query endpoint is renameduseGetWeatherQuery→useGetWeatherForecastQuery. 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?
Type of change
Checklist:
pnpm i18nto help with this)