From a425d6366059a7358c6f07d96f43ba3cb7c8189e Mon Sep 17 00:00:00 2001 From: fraxle Date: Wed, 13 May 2026 16:02:47 +0100 Subject: [PATCH] Fix timezones --- assets/js/app.js | 57 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index e9f46f2..90a7aac 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -358,6 +358,19 @@ export function UTCIForecast() { // Take the raw API arrays and stitch them into one object per hour, // calculating UTCI + soak-factor for each row. This is what gets // displayed in the table. + // Open-Meteo with timezone=auto returns local wall-clock strings like + // "2026-05-13T14:00" — no Z, no offset suffix. We need two things: + // 1. The wall-clock hour for display & day grouping (just slice the string) + // 2. The true UTC instant for solarElevationDeg (which uses .getUTC* internally) + // Strategy: treat the ISO string as UTC (append Z), which gives a Date whose + // UTC hours equal the local wall-clock hour. Then ADD the utc_offset_seconds + // to shift it to the real UTC instant. e.g. Brisbane UTC+10: "14:00" local + // → parse as UTC 14:00 → add 10h → UTC 00:00 next day? No — subtract. + // Brisbane local 14:00 = UTC 04:00, offset = +10h, so UTC = local - offset. + // Date.parse("2026-05-13T14:00Z") = ms for UTC 14:00 + // Subtract offset (+10h = 36000000ms) → UTC 04:00. Correct. + const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000; + const hourlyRows = forecast ? forecast.hourly.time.map((iso, i) => { const h = forecast.hourly; const Ta = h.temperature_2m[i]; @@ -379,8 +392,15 @@ export function UTCIForecast() { const soilT0 = h.soil_temperature_0cm ? h.soil_temperature_0cm[i] : null; const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null; const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null; - const dt = new Date(iso); - const elev = solarElevationDeg(location.lat, location.lon, dt); + // iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z). + // For display we slice the string directly — no Date object needed. + // For solarElevationDeg (which uses .getUTC* internally) we need the + // true UTC instant: treat the local time as UTC then subtract the offset. + // e.g. Brisbane UTC+10: local 14:00 → parse as UTC 14:00 → subtract 10h → UTC 04:00 ✓ + const dtUTC = new Date(Date.parse(iso + 'Z') - utcOffsetMs); + // dt kept for SkyScope / backward compat — same as dtUTC. + const dt = dtUTC; + const elev = solarElevationDeg(location.lat, location.lon, dtUTC); const eh = vaporPressureHpa(Ta, RH); const Tmrt = calcTmrt(Ta, dir, dif, glob, elev); const utci = utciApprox(Ta, Tmrt, va, eh); @@ -404,17 +424,19 @@ export function UTCIForecast() { hourlyRows.forEach(row => { const key = row.iso.slice(0, 10); let day = days.find(d => d.key === key); - if (!day) { day = { key, date: new Date(row.iso), rows: [] }; days.push(day); } + if (!day) { day = { key, rows: [] }; days.push(day); } day.rows.push(row); }); const visible = days[selectedDay]?.rows || []; const now = new Date(); + // nowLocalISO: current moment in location-local time as "YYYY-MM-DDTHH" + // Used to match against r.iso (which is already a local wall-clock string). + const nowLocalISO = new Date(now.getTime() + utcOffsetMs) + .toISOString().slice(0, 13); // "YYYY-MM-DDTHH" const currentRow = hourlyRows.length > 0 - ? (hourlyRows.find(row => - now.toDateString() === row.dt.toDateString() && - now.getHours() === row.dt.getHours() - ) ?? hourlyRows.reduce((best, row) => + ? (hourlyRows.find(row => row.iso.slice(0, 13) === nowLocalISO) + ?? hourlyRows.reduce((best, row) => Math.abs(row.dt - now) < Math.abs(best.dt - now) ? row : best)) : null; const currentCat = currentRow @@ -541,9 +563,12 @@ export function UTCIForecast() { const band = confidenceBand(i); const locked = !isPro && i >= FREE_DAYS; const isActive = i === selectedDay; + // d.key is "YYYY-MM-DD" in location-local time — parse as UTC so + // toLocaleDateString with timeZone:'UTC' reads the correct weekday/date. + const dDate = new Date(d.key + 'T00:00Z'); const dayName = i === 0 ? 'Today' : i === 1 ? 'Tomorrow' - : d.date.toLocaleDateString('en-GB', { weekday: 'short' }); + : dDate.toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' }); return html` `; })} @@ -591,9 +616,9 @@ export function UTCIForecast() { form when you have one. --> ${proPromptDay !== null && days[proPromptDay] && (() => { - const promptDate = days[proPromptDay].date; - const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long' }); - const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short' }); + const promptDate = new Date(days[proPromptDay].key + 'T00:00Z'); + const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' }); + const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short', timeZone: 'UTC' }); return html`
{ const cat = utciCategory(r.utci); const isNight = r.elev < 0; - const isNow = - now.toDateString() === r.dt.toDateString() && - now.getHours() === r.dt.getHours(); + const isNow = r.iso.slice(0, 13) === nowLocalISO; const delta = r.utci - r.Ta; const adjCat = utciCategory(r.utciAdj); + // r.iso is the local wall-clock string from the API — slice it directly. + const localHHMM = r.iso.slice(11, 16); return html` <${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} /> - ${r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })} + ${localHHMM} ${visibleCols.air && html`${r.Ta.toFixed(1)}`}