fix(frontend): keep the /cell prefetch seeding the dated grade URL
All checks were successful
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / build-frontend (pull_request) Successful in 29s
PR build (required check) / gate (pull_request) Successful in 3s

This commit is contained in:
emi 2026-07-26 06:18:37 +00:00
parent 44b02d985a
commit ab4ba54575

View file

@ -207,7 +207,7 @@ function seedCache(url, data, etag) {
// lives here next to the bundle fetch — not knowledge of any one page. // lives here next to the bundle fetch — not knowledge of any one page.
function viewFetches(q, today) { function viewFetches(q, today) {
return { return {
grade: [uv(`grade?${q}`), TTL.grade], grade: [uv(`grade?${q}&date=${today}`), TTL.grade],
forecast: [uv(`forecast?${q}`), TTL.forecast], forecast: [uv(`forecast?${q}`), TTL.forecast],
calendar: [uv(`calendar?${q}&months=24`), TTL.calendar], calendar: [uv(`calendar?${q}&months=24`), TTL.calendar],
day: [uv(`day?${q}&date=${today}`), TTL.day], day: [uv(`day?${q}&date=${today}`), TTL.day],
@ -232,6 +232,12 @@ export function prefetchViews(lat, lon, ownViews) {
if (_prefetched === tag) return; if (_prefetched === tag) return;
_prefetched = tag; _prefetched = tag;
const q = `lat=${lat}&lon=${lon}`; 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); const own = new Set(ownViews);
// Yield first so the landing view finishes rendering before we fetch the rest. // Yield first so the landing view finishes rendering before we fetch the rest.
setTimeout(async () => { setTimeout(async () => {
@ -239,17 +245,20 @@ export function prefetchViews(lat, lon, ownViews) {
const ek = "tg-cell-etag:" + q; const ek = "tg-cell-etag:" + q;
let prev = null; let prev = null;
try { prev = sessionStorage.getItem(ek); } catch (e) {} 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 } } : {}) }); { credentials: "include", ...(prev ? { headers: { "If-None-Match": prev } } : {}) });
if (res.ok && res.status !== 304) { if (res.ok && res.status !== 304) {
const bundle = await res.json(); const bundle = await res.json();
try { sessionStorage.setItem(ek, res.headers.get("ETag") || ""); } catch (e) {} 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 || {})) { for (const [name, slice] of Object.entries(bundle.slices || {})) {
if (own.has(name) || !slice || !slice.data) continue; if (own.has(name) || !slice || !slice.data) continue;
const url = name === "day" const url = (fetches[name] || [])[0];
? uv(`day?${q}&date=${bundle.today}`) // the day slice targets today
: (fetches[name] || [])[0];
if (url) seedCache(url, slice.data, slice.etag); if (url) seedCache(url, slice.data, slice.etag);
} }
} }