fix(stats): apply moderation to the stats, like the map already does
A spot the community has hidden as 'not a skate spot' no longer inflates the community numbers. - useGrowthStats calls useAdminModeration() and drops hidden content inside dedup(), so it's excluded from totalSpots, the region breakdown and the trusted counts (kind-agnostic: the coord is built from each event's own kind). - The hidden-set sizes are part of the query key, so the stats recompute once the labels load. - Region footnote says 'community relay only, with moderation-hidden content excluded'. This narrows the map<->stats gap. The remaining deltas are legitimate and documented in AGENTS.md (untagged pins; auto-hidden spots).
This commit is contained in:
parent
9fe219aac6
commit
6d2492d847
3 changed files with 21 additions and 3 deletions
|
|
@ -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**;
|
||||
|
|
|
|||
|
|
@ -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<GrowthStats>({
|
||||
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<string, typeof events[0]>();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -443,7 +443,8 @@ export default function CommunityStatsPage() {
|
|||
</div>
|
||||
<p className="mt-3 text-[11px] text-gray-600">
|
||||
Counts <strong className="text-gray-500">spots & shops</strong>, 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 && (
|
||||
<> <strong className="text-gray-500">{uncategorised} of {locationsTotal}</strong> location{locationsTotal === 1 ? '' : 's'} have no location tag and show as “Uncategorised”.</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue