diff --git a/frontend/static/cache.js b/frontend/static/cache.js index 33fe40c..ee04bf5 100644 --- a/frontend/static/cache.js +++ b/frontend/static/cache.js @@ -207,7 +207,7 @@ function seedCache(url, data, etag) { // lives here next to the bundle fetch — not knowledge of any one page. function viewFetches(q, today) { return { - grade: [uv(`grade?${q}`), TTL.grade], + grade: [uv(`grade?${q}&date=${today}`), TTL.grade], forecast: [uv(`forecast?${q}`), TTL.forecast], calendar: [uv(`calendar?${q}&months=24`), TTL.calendar], day: [uv(`day?${q}&date=${today}`), TTL.day], @@ -232,6 +232,12 @@ export function prefetchViews(lat, lon, ownViews) { if (_prefetched === tag) return; _prefetched = tag; const q = `lat=${lat}&lon=${lon}`; + // The client's local "today" — sent explicitly so the server never has to guess + // it from its own (UTC) clock. Getting this wrong is exactly what poisoned the + // Weekly view's cache for UTC+ visitors between their local midnight and UTC + // midnight: the bundle used to be built against the server's date while the + // per-view request stated the client's, so the two silently disagreed. + const today = localDay(); const own = new Set(ownViews); // Yield first so the landing view finishes rendering before we fetch the rest. setTimeout(async () => { @@ -239,17 +245,20 @@ export function prefetchViews(lat, lon, ownViews) { const ek = "tg-cell-etag:" + q; let prev = null; try { prev = sessionStorage.getItem(ek); } catch (e) {} - const res = await fetch(uv(`cell?${q}&neighbors=1`), + const res = await fetch(uv(`cell?${q}&date=${today}&neighbors=1`), { credentials: "include", ...(prev ? { headers: { "If-None-Match": prev } } : {}) }); if (res.ok && res.status !== 304) { const bundle = await res.json(); try { sessionStorage.setItem(ek, res.headers.get("ETag") || ""); } catch (e) {} - const fetches = viewFetches(q, localDay()); + // Seed under bundle.today (the date the server actually resolved — normally + // just an echo of the date= we sent) rather than our own `today`, so the + // seeded key stays byte-identical to what a per-view request for that same + // resolved date will use even in the rare case the two disagree (e.g. a + // request that straddles local midnight). + const fetches = viewFetches(q, bundle.today); for (const [name, slice] of Object.entries(bundle.slices || {})) { if (own.has(name) || !slice || !slice.data) continue; - const url = name === "day" - ? uv(`day?${q}&date=${bundle.today}`) // the day slice targets today - : (fetches[name] || [])[0]; + const url = (fetches[name] || [])[0]; if (url) seedCache(url, slice.data, slice.etag); } }