Fix timezones

This commit is contained in:
fraxle
2026-05-13 16:02:47 +01:00
parent e18e128a34
commit a425d63660
+41 -16
View File
@@ -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`
<button
key=${d.key}
@@ -576,7 +601,7 @@ export function UTCIForecast() {
<span style=${{ position: 'absolute', top: '3px', right: '5px', fontSize: '10px', opacity: 0.75 }}>🔒</span>`}
${dayName}
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
${d.date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })}
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
</span>
</button>`;
})}
@@ -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`
<div style=${{
margin: '12px 0',
@@ -857,18 +882,18 @@ export function UTCIForecast() {
${visible.map((r) => {
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`
<tr key=${r.iso}
class=${`${isNight ? 'is-night' : ''} ${isNow ? 'is-now' : ''}`.trim()}>
<td class="utci-time">
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
<${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} />
<span>${r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}</span>
<span>${localHHMM}</span>
</span>
</td>
${visibleCols.air && html`<td>${r.Ta.toFixed(1)}</td>`}