diff --git a/AGENTS.md b/AGENTS.md index 4081c52..3a8b694 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -857,6 +857,14 @@ session with **no other memory of this project**. So: and the region card only counts locations **with a location tag** while the map draws every pin. The number comparable to the map is the hero **"Skate Spots"** card (`totalSpots`), not the region rows. + - **Moderation now applies to the stats too (2026-10-01):** `useGrowthStats` + calls `useAdminModeration()` and drops hidden content inside `dedup()`, so a + spot the community removed no longer inflates `totalSpots`, the regions, or + the trusted counts. The hidden-set **sizes are part of the query key** so the + stats recompute once the labels load. Remaining map↔stats deltas (both + legitimate): **untagged pins** (the map draws them, the region rows only count + tagged ones) and **auto-hidden** spots (the map drops them at `MapPage:847`; + the stats can't know about reports without querying them). - **Reverse geocoding hits Nominatim — disclosed (2026-10-01).** `src/lib/geocodeRegions.ts` (`reverseGeocodeRegions` / `pushRegionTags`) is the **single source of truth**, used by **spot create, shop create and shop edit**; diff --git a/src/hooks/useGrowthStats.ts b/src/hooks/useGrowthStats.ts index d00cd0d..a19f7ad 100644 --- a/src/hooks/useGrowthStats.ts +++ b/src/hooks/useGrowthStats.ts @@ -22,6 +22,7 @@ import { MARKETPLACE_ITEM_KIND, } from '@/lib/types'; import { TRADE_ATTESTATION_KIND } from '@/hooks/useTradeAttestations'; +import { useAdminModeration } from '@/hooks/useAdminModeration'; import { getSatoshisFromBolt11 } from '@/lib/bolt11'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -163,9 +164,14 @@ export function useGrowthStats() { // so the numbers always match the authoritative community dataset — not a // blend of the user's personal relays that would drift per-device. const { queryCanonical } = useSkateRelays(); + // Admin-hidden content is excluded from the stats EXACTLY as the map excludes + // it, so a spot the community removed can't inflate the numbers. The moderation + // set is part of the query key so the stats recompute once the labels load. + const hidden = useAdminModeration(); + const hiddenSig = `${hidden.coords.size}:${hidden.ids.size}`; return useQuery({ - queryKey: ['growth-stats', 'canonical'], + queryKey: ['growth-stats', 'canonical', hiddenSig], queryFn: async () => { const signal = AbortSignal.timeout(15000); @@ -227,11 +233,14 @@ export function useGrowthStats() { querySkateContent([{ kinds: [TRADE_ATTESTATION_KIND], limit: 1000 }], { signal }).catch(() => []), ]); - // Deduplicate addressable events (keep newest per pubkey+kind+d) + // Deduplicate addressable events (keep newest per pubkey+kind+d), and drop + // anything an admin has hidden — moderation applies here exactly as it does + // to the map, so removed/spam content doesn't inflate the numbers. function dedup(events: typeof spotEvents) { const map = new Map(); for (const e of events) { const d = e.tags.find(t => t[0] === 'd')?.[1] ?? e.id; + if (hidden.has({ coord: `${e.kind}:${e.pubkey}:${d}`, id: e.id })) continue; const key = `${e.pubkey}:${e.kind}:${d}`; const existing = map.get(key); if (!existing || e.created_at > existing.created_at) map.set(key, e); diff --git a/src/pages/CommunityStatsPage.tsx b/src/pages/CommunityStatsPage.tsx index 4121468..2039797 100644 --- a/src/pages/CommunityStatsPage.tsx +++ b/src/pages/CommunityStatsPage.tsx @@ -443,7 +443,8 @@ export default function CommunityStatsPage() {

Counts spots & shops, not skaters — from each - one's own city/country tag (countries grouped by ISO code when present). + one's own city/country tag (countries grouped by ISO code when present), + community relay only, with moderation-hidden content excluded. {locationsTotal > 0 && ( <> {uncategorised} of {locationsTotal} location{locationsTotal === 1 ? '' : 's'} have no location tag and show as “Uncategorised”. )}