Relay outages no longer look like 'no spots'. querySkateContent swallowed every
relay error and returned [], so a dead relay would blank the map AND overwrite
good cached data with an empty list (then persist it). Now:
- querySkateContent/queryCanonical take an opt-in 'requireAny' that throws when
NO relay answered. Only the map's own feeds use it, so the blast radius is
small (79 other call sites keep today's behaviour).
- useSkateSpots/useSkateSpot fall back to the local IndexedDB cache on failure,
so the map still works at the park; they only error when nothing is saved.
- The persister refuses to persist an empty array, so an outage can't wipe the
last-known map on the next cold start.
- MapPage shows an honest 'Offline · showing saved spots' banner (reusing the
existing relayProbe/useRelayStatus) and a 'Can't reach the relays' card
instead of falsely claiming 'No spots mapped yet'.
Also seeds skater@skate-spots.org for the second admin (caba497f…) and reserves
the name so nobody else can claim it.
(a) Review queue covers spots/shops/events/tours/gear: added { includeHidden }
(admin-tooling only) to useSkateShops/useSkateEvents/useTours/
useMarketplaceItems; generalised the flags to contentFlags() in spotQuality.ts
(trust/status/photo signals stay spot-only); one component per kind so only the
OPEN tab queries. Reports + trust are kind-agnostic; the label mechanism never
cared about kind.
(b) Photo gate softened: removed the hard block (it punished an honest mapper with
no photo). Publish without a photo is allowed; the spot stays Unverified, the
success toast says so, and the queue's 'no photo' flag lets a human look.
(c) Empty-map nudge: MapPage now distinguishes a FILTER empty ('No spots match')
from a genuinely EMPTY map ('No spots mapped yet — be the first to map your
park', with Add-spot + Near-me). Note: must key off allLocations, not mapSpots
(which derives from filteredSpots).
Verified in the browser: the empty-map nudge renders with its CTAs when no spots
load; the review queue renders 15 spot rows and the tabs switch (Gear shows its
own empty state). tsc/eslint clean, 101 tests, smoke PASS.
The two SkateSpots admin pubkeys (already in src/lib/admins.ts) can now remove
any listing, not just their own. Because a NIP-09 deletion signed by a
non-author isn't honored by most relays, admin takedowns are made reliable with
a client-side moderation layer:
- useDeleteEntity: owners still publish a plain NIP-09 kind:5. Admins acting on
someone else's content publish the kind:5 AND a NIP-32 kind:1985 `hidden`
label referencing the target coordinate/id, authored by the admin.
- useAdminModeration (new): fetches admin-authored `hidden` labels app-wide and
exposes a fast lookup + filterAdminHidden() helper. Only labels whose author
is in the hardcoded ADMIN_PUBKEYS are honored (author = trust boundary,
re-verified client-side; never derived from a d-tag).
- Wired the hide filter into the list + detail hooks for spots, crews (incl.
by-focus), tours, events, and shops via React Query `select`, so removed
content disappears from lists and 404s on its detail page.
UI:
- DeleteEntityDialog now renders for owner OR admin, with distinct
"Remove (admin)" framing and copy (used on Crew, Coach, Tour, Event pages).
- SpotDetailPage & ShopDetailPage: routed their inline delete through
useDeleteEntity, show the delete button for admins, and relabel the confirm
dialog for admin takedowns.
- Added the delete control to Tour and Event detail pages (previously none).
SECURITY.md: documented the admin moderation trust boundary and extended the
auditor file map. Verified build + no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- AppRouter: lazy load all 40+ pages (only Index is eager)
→ initial JS bundle no longer includes code for pages never visited
- MapPage: lazy load SpotMap/Leaflet (~150KB) behind Suspense
→ Leaflet only downloads when user navigates to /map
- useSkateSpots/useShops/useMarketplace/useTours:
→ timeout: 30s → 8s (fail fast, show error sooner)
→ staleTime: 30s → 5min (trust cache, fewer relay round-trips)
→ gcTime: 5min → 30min (keep data in memory longer)
→ refetchOnMount: 'always' → true (use cache when data is fresh)
- Remove console.log/warn/debug spam from production hooks
- vite.config: manual chunk splitting for leaflet, nostr-tools, bitcoin
libs, radix-ui, react-router — each chunk cached independently
by browser, only re-downloaded when that library updates
- vite.config: esbuild drops console.log/warn/debug in prod build
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.
Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)
Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>