Commit graph

5 commits

Author SHA1 Message Date
SkateSpots
e44f8b81a7 feat(moderation): reversible verdicts (undo) + community photos as a signal
Undo (latest verdict wins):
- MODERATION_LABEL_CLEARED = 'cleared' — the neutral undo verdict.
- useAdminModeration no longer UNIONS labels (which made a hide permanent in the
  UI); it keeps the NEWEST verdict per target (created_at, with device-local
  verdicts overlaid by timestamp), so 'cleared' supersedes an earlier
  'hidden'/'verified'. useAdminClear() publishes it.
- Local mirror is now a verdict map (skatespots_local_admin_verdicts_v1: key ->
  {verdict, at}) with migration from the legacy hidden/confirmed arrays.
- useSkateSpots(limit, { includeHidden: true }) — ADMIN TOOLING ONLY: without it
  a hidden spot leaves the list, so a verdict could never be undone. The review
  queue uses it, keeps actioned rows (Hidden/Confirmed badge + Undo), and sorts
  un-actioned first.

Community photos as a signal:
- useBatchSpotPhotos: ONE #a query for every spot (the single-spot hook already
  queried by #a, so batching is free), counting only photos from people OTHER
  than the mapper. Folded into the 'no community signals' flag.

Verified by seeding local verdicts: none -> flags + Confirm/Hide; hidden ->
Undo; verified -> Undo; cleared -> back to neutral. tsc/eslint clean, 101 tests,
smoke PASS.
2026-09-28 01:06:04 +07:00
SkateSpots
0419c18810 feat(moderation): positive 'verified' verdict + trust/community signals in the review queue
- MODERATION_LABEL_VERIFIED: the positive counterpart to 'hidden'. Both labels
  are fetched in ONE query; useAdminModeration now exposes verifiedCoords/
  verifiedIds/isVerified() with a local mirror (addLocalConfirmed/
  getLocalConfirmed). useAdminVerify() publishes it (shares the publish path
  with useAdminHide).
- Review queue: a human review can now CONFIRM a spot, not just hide it. A
  confirmed spot clears from the queue and gets a 'Verified by SkateSpots' badge
  on the spot page (staff verdict, shown separately from the community badge).
- Two more signals: mapper trust (useBatchTrustScores) and whether any skater has
  filed a status report (useBatchSpotStatus reporterCount) -> 'mapper not in your
  web of trust' / 'no community signals'. The latter only fires alongside another
  flag so a brand-new real spot isn't punished for being new.
- Flag logic moved to src/lib/spotQuality.ts so the page and AdminReviewCard share it.

Verified: the queue lists the airport entry with suspicious name + no photo + no
community signals, shows mapper trust, and offers Confirm/Hide/Not a skate
spot/Look at it.
2026-09-28 00:25:12 +07:00
shakespeare.diy
79e8376f58 Make admin delete non-blocking + polish notification bell
DELETE HANG (real fix):
The admin takedown required TWO sequential Amber approvals (kind:5
deletion + kind:1985 hide label) and awaited both. On mobile the second
Amber prompt often doesn't resurface, so the button sat on "Deleting…"
until the 90s signer timeout. Now:
- Only the kind:5 deletion is awaited (the required action).
- The target is recorded in a local optimistic-hide store immediately, so
  the app hides it on this device the instant kind:5 succeeds — no waiting
  on a second signature.
- The cross-device kind:1985 label is fired in the BACKGROUND (not
  awaited); a second approval, if needed, happens after the dialog closes.
- The whole mutation is wrapped in a 40s hard timeout so the spinner can
  never hang indefinitely, whatever the signer/relay does.
- useAdminModeration merges the local optimistic set so hides are instant
  and survive reload.

NOTIFICATION BELL UI:
- Brand-lime (#ccff00) glowing badge with ring, replacing the flat teal dot.
- Bell wiggles + soft glow pulse when there are unread items (new `wiggle`
  keyframe).
- Gradient header with an icon chip, inline count, and a pill "Mark read"
  button / "All clear" state.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 22:59:16 +07:00
shakespeare.diy
82a74e01e0 Extend admin takedown to Lab profiles and gear/marketplace listings
Completes the admin moderation coverage from the previous commit.

Lab profiles (coaches, filmers, editors, photographers, builders, artists,
graffiti, tattoo, trainers, musicians):
- useLabProfiles / useLabProfile and useCoaches / useCoach now filter out
  admin-hidden entries (list) and 404 hidden ones (detail) via the shared
  moderation layer. Added filterAdminHiddenMixed() for the mixed-kind Lab
  union (kind read from each item's event).
- CoachDetailPage (the shared Lab detail page) now shows the delete/takedown
  card to admins too, not just the owner, with admin-specific copy.
  DeleteEntityDialog already handles the owner-or-admin logic.

Gear / marketplace listings (NIP-99, kind 30402):
- useMarketplaceItems / useMarketplaceAtSpot filter out admin-hidden listings.
- GearSwapItemCard: admins get a "Remove (admin)" action in the card menu with
  a confirmation dialog that routes through useDeleteEntity (NIP-09 delete +
  hidden label). Report stays for non-owners.

SECURITY.md: updated the admin-moderation coverage list and auditor file map.
Build + lint pass, no console errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-18 19:17:16 +07:00
shakespeare.diy
13eece3a1e Admins can delete/take down any listing (spot, crew, tour, event, shop)
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>
2026-08-18 17:51:34 +07:00