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
|
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
|
map draws every pin. The number comparable to the map is the hero
|
||||||
**"Skate Spots"** card (`totalSpots`), not the region rows.
|
**"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).**
|
- **Reverse geocoding hits Nominatim — disclosed (2026-10-01).**
|
||||||
`src/lib/geocodeRegions.ts` (`reverseGeocodeRegions` / `pushRegionTags`) is the
|
`src/lib/geocodeRegions.ts` (`reverseGeocodeRegions` / `pushRegionTags`) is the
|
||||||
**single source of truth**, used by **spot create, shop create and shop edit**;
|
**single source of truth**, used by **spot create, shop create and shop edit**;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
MARKETPLACE_ITEM_KIND,
|
MARKETPLACE_ITEM_KIND,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { TRADE_ATTESTATION_KIND } from '@/hooks/useTradeAttestations';
|
import { TRADE_ATTESTATION_KIND } from '@/hooks/useTradeAttestations';
|
||||||
|
import { useAdminModeration } from '@/hooks/useAdminModeration';
|
||||||
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
|
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -163,9 +164,14 @@ export function useGrowthStats() {
|
||||||
// so the numbers always match the authoritative community dataset — not a
|
// so the numbers always match the authoritative community dataset — not a
|
||||||
// blend of the user's personal relays that would drift per-device.
|
// blend of the user's personal relays that would drift per-device.
|
||||||
const { queryCanonical } = useSkateRelays();
|
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>({
|
return useQuery<GrowthStats>({
|
||||||
queryKey: ['growth-stats', 'canonical'],
|
queryKey: ['growth-stats', 'canonical', hiddenSig],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(15000);
|
const signal = AbortSignal.timeout(15000);
|
||||||
|
|
||||||
|
|
@ -227,11 +233,14 @@ export function useGrowthStats() {
|
||||||
querySkateContent([{ kinds: [TRADE_ATTESTATION_KIND], limit: 1000 }], { signal }).catch(() => []),
|
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) {
|
function dedup(events: typeof spotEvents) {
|
||||||
const map = new Map<string, typeof events[0]>();
|
const map = new Map<string, typeof events[0]>();
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
const d = e.tags.find(t => t[0] === 'd')?.[1] ?? e.id;
|
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 key = `${e.pubkey}:${e.kind}:${d}`;
|
||||||
const existing = map.get(key);
|
const existing = map.get(key);
|
||||||
if (!existing || e.created_at > existing.created_at) map.set(key, e);
|
if (!existing || e.created_at > existing.created_at) map.set(key, e);
|
||||||
|
|
|
||||||
|
|
@ -443,7 +443,8 @@ export default function CommunityStatsPage() {
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-[11px] text-gray-600">
|
<p className="mt-3 text-[11px] text-gray-600">
|
||||||
Counts <strong className="text-gray-500">spots & shops</strong>, not skaters — from each
|
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 && (
|
{locationsTotal > 0 && (
|
||||||
<> <strong className="text-gray-500">{uncategorised} of {locationsTotal}</strong> location{locationsTotal === 1 ? '' : 's'} have no location tag and show as “Uncategorised”.</>
|
<> <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