diff --git a/AGENTS.md b/AGENTS.md index 1706545..f0af24f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -829,6 +829,15 @@ session with **no other memory of this project**. So: `hidden lg:contents`, so children stay real grid items) with a "Show all 13 crafts" toggle; container widened `max-w-6xl` → `max-w-7xl`; `StatCard` gained a `compact` prop (`+compactNumber`) so big sats don't overflow `text-4xl`. + - ⚠️ **Adding a field to a PERSISTED query is a crash risk (2026-10-01).** + `growth-stats` is in the `queryPersister` allow-list, so a cache saved before + a new field existed lacks it — and `stats.topCountries.length` throws + (`Cannot read properties of undefined`), slamming `/stats` for anyone with an + old cache. Fix: **bump `PERSIST_BUSTER`** (`src/lib/queryPersister.ts`, wired + in `App.tsx`) **AND** read defensively (`stats?.x ?? []`). ⚠️ **The smoke test + cannot catch this** — it boots a clean profile with no persisted cache, so it + passed while real users crashed. Any new field on an allow-listed query needs + both. - **NIP-05 names — `you@skate-spots.org` (2026-09-28):** SkateSpots hands out free handles so a new skater has a human name from minute one. The registry is the Netlify Function **`netlify/functions/nip05.mjs`** diff --git a/src/lib/queryPersister.ts b/src/lib/queryPersister.ts index eda6b59..af93f39 100644 --- a/src/lib/queryPersister.ts +++ b/src/lib/queryPersister.ts @@ -40,7 +40,7 @@ import type { Query } from '@tanstack/react-query'; const IDB_KEY = 'skatespots:react-query-cache'; /** Bump when the persisted data shape changes to invalidate old caches. */ -export const PERSIST_BUSTER = 'v1'; +export const PERSIST_BUSTER = 'v2'; /** Discard any persisted cache older than this (ms). */ export const PERSIST_MAX_AGE = 24 * 60 * 60 * 1000; // 24h diff --git a/src/pages/CommunityStatsPage.tsx b/src/pages/CommunityStatsPage.tsx index 55bf234..8a91ed2 100644 --- a/src/pages/CommunityStatsPage.tsx +++ b/src/pages/CommunityStatsPage.tsx @@ -206,6 +206,13 @@ export default function CommunityStatsPage() { // Total across ALL Lab creator/service crafts (matches what /lab lists) const labTotal = useMemo(() => (stats ? labCraftTotal(stats) : 0), [stats]); + // Defensive reads: a persisted cache saved before these fields existed would + // lack them, and `stats.topCountries.length` would throw. (The persist buster + // was bumped too, but never trust a cache blindly.) + const tradesByMethod = stats?.tradesByMethod ?? []; + const topCountries = stats?.topCountries ?? []; + const topCities = stats?.topCities ?? []; + return ( <>
@@ -333,9 +340,9 @@ export default function CommunityStatsPage() {
- {stats && stats.tradesByMethod.length > 0 && ( + {tradesByMethod.length > 0 && (
- {stats.tradesByMethod.map((m) => ( + {tradesByMethod.map((m) => ( {/* ── Section 1e: Regions ── */} - {stats && (stats.topCountries.length > 0 || stats.topCities.length > 0) && ( + {(topCountries.length > 0 || topCities.length > 0) && (

Where Skaters Are

- {stats.topCountries.length > 0 && ( + {topCountries.length > 0 && ( Top Countries - {stats.topCountries.map((c) => ( + {topCountries.map((c) => (
{c.name}
{c.spots} @@ -380,19 +387,19 @@ export default function CommunityStatsPage() { )} - {stats.topCities.length > 0 && ( + {topCities.length > 0 && ( Top Cities - {stats.topCities.map((c) => ( + {topCities.map((c) => (
{c.name}
{c.spots}