Commit graph

737 commits

Author SHA1 Message Date
SkateSpots
3051858de7 contents(admin): tidy the review queue + add gallery screenshots
- Review queue: past verdicts (e.g. the 13 hidden labels from an Aug/Sep cleanup
  sweep) are correct but noisy — they buried the items that still need a
  decision. They are now COLLAPSED behind a 'Show actioned (N)' toggle, with the
  verdicts untouched (still hidden) and 'Undo all N' still available. Verified:
  the Spots tab renders 0 hidden cards by default with the toggle + Undo all.
- public/images/screenshots/{map,spot,marketplace}.png (1280x800, quantised) for
  the app-listing gallery.
2026-09-28 11:08:31 +07:00
SkateSpots
48a27d552d contents(spot): move Comments above Gear Swap and Spot Chat
Spot page order is now: title/badges -> description -> Comments -> Gear Swap ->
Spot Chat. Comments are the conversation about the spot, so they belong next to
the description rather than below the transactional blocks.

Verified in the browser by text position: description(379) < Comments(404) <
GEAR SWAP(507) < Spot Chat(618). tsc/eslint clean, 101 tests, smoke PASS.
2026-09-28 10:44:37 +07:00
SkateSpots
9a8056700d contents(about): rework the Credits list
- Remove MKStack and Shakespeare credits (owner request). AGENTS.md updated so a
  future session does not re-add the old 'Vibed with MKStack' instruction.
- 'Nostr' now links to the protocol repo (github.com/nostr-protocol/nostr) in
  both the Credits card and the 'Powered by' footer.
- Add: Cordn (cordn.net), Cashu (cashu.space), NWC (docs.nwc.dev), Amber
  (github.com/greenart7c3/Amber), BTCMap (btcmap.org).

Verified in the browser: no MKStack/Shakespeare text, all five new links present
with the right hrefs, BTCMap spelled correctly.
2026-09-28 10:31:43 +07:00
SkateSpots
3c0a91a379 feat(moderation): report inbox, bulk undo, crews + Lab in the queue
- Report INBOX: /review now defaults to a Reports tab. useRecentReports fetches
  recent NIP-56 (1984) and resolves the referenced event by id (one extra ids:
  query), so ANY report is actionable — content that isn't flagged, comments,
  profiles, anything. reportTargetMeta maps kind -> label + route for the link.
  Actions: Hide / Dismiss report (verify) / Undo / Look at it.
- Bulk undo: when a tab holds >1 verdict, a toolbar offers 'Undo all N' with an
  AlertDialog that warns each undo is its own label publish (one Amber approval
  each). Built for the 13 old hidden labels from Sep 3.
- Crews + Lab tabs: added includeHidden to useCrews/useLabProfiles. Hiding these
  removes a community / a person's listing, so their Hide goes through a
  confirmation (confirmHide/confirmHideNote on QueueList).

Verified in the browser: 8 tabs render; the inbox shows and resolves a real
report (target + reason); the Spots tab shows the bulk-undo toolbar.
tsc/eslint clean, 101 tests, smoke PASS.
2026-09-28 10:20:54 +07:00
SkateSpots
f1ddf5fdf1 feat(quality): review queue for every kind, soft photo gate, empty-map nudge
(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.
2026-09-28 09:59:09 +07:00
SkateSpots
e549aeee3a feat(spots): reports reach the admin, duplicates are caught, contributors are told
1) Reports were a VOID: ReportDialog publishes NIP-56 (1984) but the only reader
   in the app was a count on marketplace cards, so an admin never saw one. Now
   useSpotReports batch-reads kind 1984 by '#e' (one query for every spot) and
   the review queue ranks reported spots ABOVE the heuristics with a red
   'REPORTED: ...' chip. useNotASpotReport publishes to getWriteRelays()
   (community relay + the reporter's), not the default pool — the admin reads
   from the community relay, so reports written only to personal relays would
   never arrive. Verified end-to-end: published a report from Node, saw it
   surface in the queue.

2) Duplicate guard in CreateSpotPage: on submit, existing spots within 60 m are
   listed with distances + links, with 'It's a different spot — publish anyway'.
   Targets the clone-spam failure mode (five near-identical pins).

3) Contributor feedback: useSkateSpot no longer nulls a hidden spot for its
   AUTHOR (it used to silently 404 for the person who made it), and
   SpotDetailPage shows an author-only banner for hidden/verified.

tsc/eslint clean, 101 tests, smoke PASS.
2026-09-28 09:45:36 +07:00
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
SkateSpots
a43ff47b28 feat(spots): honest quality signals, photo required, off-topic reports, admin review queue
The 'test of secret island airport' problem: a junk pin wearing an app-endorsed
'Skateability 95/100'.

- Rename the weather score to 'Conditions · weather only' (SpotDetailPage +
  the SpotMap popup). It was never a verdict on the place.
- New Community verified / Unverified badge from signals only OTHER skaters can
  produce: community photos + useSpotStatus reporterCount (kind 2259).
- CreateSpotPage now REQUIRES at least one photo (guard + disabled submit +
  a required hint on the label).
- useNotASpotReport: one-tap NIP-56 (1984) report tagged 'not-a-skate-spot'
  (NIP-56 'other' + our L/l/t taxonomy), wired into the spot's More menu and
  added to the shared ReportDialog reasons.
- useAdminHide: the MISSING WRITE SIDE for admin moderation. useAdminModeration
  always read NIP-32 kind-1985 'hidden' labels but nothing published one, so the
  only working hide was device-local. Now publishes the label (+ optimistic
  local hide). Soft and reversible — the event stays on relays.
- /review (AdminReviewPage, admin-only → 404 otherwise): review queue over
  useSkateSpots with cheap flags (suspicious title, name <=3 chars, no photo, no
  description, no geohash), sorted by flag count, one-tap Hide or Not a skate
  spot. Entry via the admin-only AdminReviewCard in Settings → Advanced.
- /review added to the smoke-test route list.

Verified: the airport spot shows UNVERIFIED with 'Conditions · weather only' and
no 'Skateability'; the review queue lists it with flags + Hide/report actions;
create-spot blocks without a photo.
2026-09-27 23:41:49 +07:00
SkateSpots
0adfbcbd29 feat(app-shop): remove the subscribe band and the honesty note
Per request, the page is now: hero -> in the works -> free to print -> how
buying works. Nothing else changed.

Honesty is still carried by the remaining copy: the COMING SOON badge, the
'IN THE WORKS / not for sale yet' label on the teasers, and the hero's 'sold
locally, on request'. No prices, no buy buttons, no stock list.

Also drops the now-unused NewsletterSubscribeButton/ArrowRight imports, and
updates release-notes + AGENTS.md (they described the removed copy).
2026-09-27 23:23:14 +07:00
SkateSpots
4341653abe feat(app-shop): add free printable sticker + poster (vector, real QR)
- public/images/print/skatespots-sticker.svg (55 x 86 mm) and
  skatespots-poster.svg (A3) — the free artwork to print and stick up. Physical
  size is baked into the SVG (mm units), so 'Actual size' printing is exact.
- Self-contained: Outfit/Barlow fonts + the logo embedded as data URIs, and a
  real vector QR. VERIFIED both QRs decode to https://skate-spots.org (decoded
  with jsQR from a rendered PNG; logo centre, error-correction H).
- New 'Free to print' section in the App Shop: previews, FREE badge, sizes,
  cutting tip, 'Open & print' + 'Save file' buttons, and a 100%/Actual-size note.
- scripts/make-print-art.mjs regenerates the artwork (committed output).
- AGENTS.md documents the assets + the re-verify-the-QR rule.
2026-09-27 22:16:43 +07:00
SkateSpots
f3d88cc2a8 feat(app-shop): drop the stock stat strip and the teaser blurbs
- Remove the 0 / None / Nothing tiles (items in stock, pre-orders, for sale
  today) — the honesty footer already says there is no stock.
- 'In the works' cards are now just icon + label + SOON badge, no small print
  ('Heavyweight...', 'Small numbered runs...', etc).

Verified in the browser: strip and blurbs gone, everything else intact.
2026-09-27 22:01:48 +07:00
SkateSpots
059d9da5c4 feat(app-shop): local/on-request sales via DM (drop shipping claims)
- Remove the 'Shipped worldwide' and 'At cost, no markup theatre' chips; the
  remaining chips are Lightning, Cashu, Local handover.
- Copy now says sold locally, on request: message me -> we sort the details ->
  pay in sats at handover. No shipping, at all.
- 'How it'll work' -> 'How buying works' (it's a DM, not a cart): Message me /
  We sort it out / Pay in sats.
- New 'Request an item' CTA in the hero that deep-links into a private DM with
  the shop owner (/messages/<PLATFORM_OWNER_PUBKEYS[0] npub>), so a buyer can
  start the conversation with zero checkout machinery.
- Honesty footer states local handover only / no shipping of any kind.
- AGENTS.md documents the model (no cart/checkout/shipping without a decision).
2026-09-27 21:52:00 +07:00
SkateSpots
8dcc97fd81 feat(app-shop): add the App Shop section (empty on purpose)
New /app-shop page for SkateSpots merch, to be paid in sats only (Lightning or
Cashu) — no cards, no accounts, no middleman.

Design: gradient/glow hero ('Wear the scene' + Coming soon badge), an honest
stock strip (0 items / None pre-orders / Nothing for sale today), an 'In the
works' teaser grid (tees, decks, stickers, tools — dashed + 'soon', no prices),
a three-step 'How it'll work' (pick -> pay in sats -> ships), a newsletter
notify band (the drop lands there first) and a plain-English honesty footer.

Deliberately contains no items: no stock, no prices, no pre-orders, no buy
buttons. AGENTS.md documents that rule so nobody invents a catalogue later.

Wiring: registered in NAV_CATALOG as id 'app-shop' (appears in the desktop
'More' menu AND the mobile burger menu automatically), lazy route in AppRouter,
and added to the smoke-test route list so a broken page aborts deploys.

Verified: page renders desktop + mobile (all sections), listed in both navs,
smoke PASS with /app-shop.
2026-09-27 21:40:41 +07:00
SkateSpots
c07f857ed0 chore: sync package-lock to 1.5.4 2026-09-27 20:47:21 +07:00
SkateSpots
9aa5a9ca4e release: v1.5.4
1.5.3 shipped at 19:22, before the DM fix, the directions UI rework and the
newsletter reader fix. Bump to 1.5.4 so those can actually reach people.

- package.json 1.5.4 (-> versionName 1.5.4 / versionCode 10504).
- release-notes.txt: new v1.5.4 section (DM persistence + parallel send/read,
  bigger-map/app-like directions, underscore italics in the issue reader);
  trimmed v1.5.3 back to what was actually published.
2026-09-27 20:43:16 +07:00
SkateSpots
565fdb9211 fix(newsletter): render _underscore italics_ in issue bodies
issueToMarkdown wraps each issue's vibe in _underscores_, but the in-app
markdown renderer only understood *asterisk* emphasis — so every published
archive issue showed LITERAL underscores around its intro line (both in our
reader and here). Added the underscore form to the tokenizer.

Verified on the live issue-5 naddr: the vibe now renders as <em> with no
literal underscores.
2026-09-27 20:34:31 +07:00
SkateSpots
261e4c9115 feat(newsletter): add Drop #006 to the archive (+cover support), incl. Sasquatch and Mints
- NewsletterIssue gains an optional 'image' (per-issue cover); IssueArticle renders
  it above the header and buildIssueArticle uses it as the NIP-23 'image' tag
  (falling back to the spot-of-the-week image).
- NEWSLETTER_ISSUES now leads with #006 (2026-09-27): the Nostr-Twitter-Clone
  rant, the Directions fixes (route never drew + identical per-mode numbers),
  follow mode, choose-your-start + offline saved routes, Sasquatch Pro
  (sats-paid, streamed, personal tools, one signature per session), the
  Marketplace hub + 1-10 deck ratings, Mints (directory + the native JSON-error
  fix), Amber scoping, the DM inbox-relay bug, and the polish list.
- Cover: https://skate-spots.org/images/drop-006-cover.png (hosted in
  public/images, added in e075ee8).

Verified: the archive renders the cover + #006 copy, and the 'Drop #006 is out'
pill shows on other pages for 7 days. tsc/eslint clean, 101 tests, smoke PASS.
2026-09-27 20:26:14 +07:00
SkateSpots
e075ee8138 assets: add Drop #006 cover image (hosted at /images/drop-006-cover.png) 2026-09-27 20:17:39 +07:00
SkateSpots
1ef35c14b8 fix(dms): DM inbox relays were wiped on every load, so DMs went nowhere
Root cause: AppProvider validates the persisted config with
AppConfigSchema.partial().parse(), and RelayMetadataSchema only declared
'relays' + 'updatedAt'. Zod drops unknown keys by default, so dmRelays (and
localRelays + blockedRelays) were SILENTLY STRIPPED on every page load. The DM
inbox relays (kind 10050) are usually not in a user's NIP-65 list, so DM reads
and writes fell back to NIP-65 relays - where the other side isn't listening -
and messages looked like they never went through.

- Declare localRelays/blockedRelays/dmRelays in RelayMetadataSchema.
- Send: publish gift wraps in PARALLEL with per-relay timeouts and resolve on
  the first relay that accepts (a dead relay or auth.nostr1.com no longer
  stalls the send); deliver to the recipient's NIP-65 WRITE relays when they
  have no discoverable 10050 (the old fallback was dead code because our own DM
  relays are always non-empty); discovery of 10050/10002 now runs in parallel.
- Read: query all DM relays in parallel with per-relay timeouts. Previously one
  slow/dead relay burned the shared 15s budget and then the fired abort made
  every remaining relay fail instantly.

Verified with an nsec login + a hostile relay list (dead + auth-required):
before, the wrap reached only write relays and the recipient found 0; after, it
lands on the configured inbox relays and both the recipient and our own copy are
found. Send worst case 20s -> 4.5s.
2026-09-27 19:58:54 +07:00
SkateSpots
4474cc26e9 feat(directions): bigger map + app-like layout and richer route details
- Map is now the hero: h-[38vh] mobile / 400px desktop (was 256), with a metric
  Leaflet scale control.
- New layout: flex column with header/modes/From/map pinned, the details as the
  ONLY scroll region, and Start/Stop pinned at the bottom so it's always
  reachable. Previously the dialog was a single overflow-hidden box, so once the
  map grew everything below the fold (Start, steps) was unreachable.
- Summary rebuilt as stat tiles (distance / time / arrive) plus a detail line:
  step count, honest 'uses driving roads'/'driving estimate' notes, and the
  average speed the engine assumed ('21 km/h avg'; foot keeps the skate cruise
  estimate). Steps header shows the count; step list scrolls taller (max-h-72).
- Wider dialog on desktop (sm:max-w-xl).

Verified in-app: tiles + 9/8/5/5 steps per mode, follow banner still tracks
movement, offline chip still appears, map 371px -> 321px with the new layout.
2026-09-27 19:42:06 +07:00
SkateSpots
5a28b3e576 chore: sync package-lock to 1.5.3 2026-09-27 19:20:56 +07:00
SkateSpots
dff3ccc6f8 release: v1.5.3
1.5.2 is already published to Zapstore (verified: org.skatespots.app@1.5.2 on
relay.zapstore.dev), so the post-publish work needs a new version.

- package.json 1.5.3 (-> versionName 1.5.3 / versionCode 10503).
- release-notes.txt: new v1.5.3 section (real per-mode routing + steps + follow
  mode + choose-your-start + offline saved routes + Amber scoping); trimmed the
  v1.5.2 section back to what was actually published.
2026-09-27 19:16:53 +07:00
SkateSpots
5a819d5a31 feat(directions): follow mode, choose-your-start, offline route cache
Follow mode (explicit Start; off by default — a watcher drains battery):
watchPosition in geolocation.ts, live dot, 'NEXT · step N' banner with
distance-to-turn + distance-to-go, current step highlighted, map follows.
A transient watch error (timeout/unavailable) no longer kills it — only a
permission denial does; the watcher recovers on its own.

Choose your start: 'From → My location / Choose on map' — tapping the map sets
the origin, and the route recomputes from there.

Offline: src/lib/routeCache.ts keeps the last route per destination+mode with
the line downsampled to <=250 points. If a fetch fails (no signal / routing
server down) the dialog falls back to it with an 'Offline · saved route {age}'
badge instead of a dead error.

Also tidied the map overlays (offline badge bottom-left, Whole route
bottom-right) so nothing covers Leaflet's zoom control.

Verified with CDP: controls present; follow banner tracks movement (step 1 ->
step 7, distances update); blocking routing shows the cached route + badge;
picking a start on the map changed the route (968 m -> 922 m). Still no voice
guidance or re-routing if you leave the route (documented).
2026-09-27 19:11:14 +07:00
SkateSpots
59e5829b2a feat(directions): make the route dialog usable as guidance, not just a preview
Answering 'are we giving the best navigation experience?': no, it was a preview
with dead ends. Fixes the concrete gaps:

- ETA: 'arrive ~08:10 PM' alongside the duration.
- Mode memory: the last travel mode is remembered (skatespots:route-mode), so
  cyclists/drivers aren't reset to Skate/Walk every trip.
- Tappable steps: each step pans the map and highlights; added a 'Whole route'
  re-fit button to zoom back out.
- Taller route map on desktop (h-64 sm:h-80).
- min-h-0 on step rows: the app-wide 44px mobile touch-target rule was making
  every step a 44px slab (rows now 29px).

Still out of scope (documented, not faked): live turn-by-turn following, voice
guidance and offline routing.
2026-09-27 18:47:29 +07:00
SkateSpots
e69bace784 fix(amber,routing): scope Amber prompts to real actions; real per-mode routes + steps
AMBER — 'pops on every screen':
staleTime: Infinity only stops REFETCHING, never the FIRST decrypt. So the
encrypted useSyncedAppData blobs (read-state, preferences) each ran a queryFn on
mount (racing consumers multiplied it) and relay responses landing during
navigation made it look per-page. useSyncedAppData now has interactiveNetwork
(default false): for an interactive signer the blob is LOCAL-ONLY — query not
even enabled, patch() a no-op — no decrypt on mount, no publish on a nav-tab
change. Verified with a mocked x-amber login: network-on => Amber op at startup,
with the fix => 0. (useCrewDismissals/useStash were already gated.)

ROUTING — 'same time for every transport option':
router.project-osrm.org hosts ONE car dataset and ignores the profile in the URL,
so foot/bike/car returned identical numbers. Switched to the FOSSGIS per-profile
servers (routing.openstreetmap.de/routed-{foot,bike,car}); the demo server is now
only a last-resort fallback and its result is flagged approximate ('driving
estimate'). Request steps=true and surface turn-by-turn steps for orientation,
plus a '~X min on a board' skate estimate next to the walking time. Verified:
same trip => walk 13 min / bike 5 / moto+car 2, with 5-9 steps each.
2026-09-27 18:35:17 +07:00
SkateSpots
6364f08ede chore: sync package-lock version to 1.5.2
npm i (run by the android build) updates the lock's root version to match
package.json. Committing it keeps the tree clean — a dirty tracked file makes
the auto-deploy timer refuse to deploy.
2026-09-27 17:50:24 +07:00
SkateSpots
e2d367efab release: v1.5.2
Bump package.json (single source of truth → versionName 1.5.2 / versionCode
10502 via scripts/sync-android-version.mjs) and add the v1.5.2 release notes:
marketplace deck ratings + seller dashboard, working in-app directions, nav
fixes (desktop More / mobile Marketplace), the native mints fix, and the
newsletter CPU + relay fixes.
2026-09-27 17:46:00 +07:00
SkateSpots
e98c996945 fix(newsletter): infinite render loop + issues published to the wrong relays
Audit of the newsletter section found two real bugs.

1) /newsletter rendered in an infinite loop (100% CPU, React 'Maximum update
   depth exceeded'). Chain: useSyncedAppData returned a fresh object each render
   -> useReadState.apply (dep: synced) changed identity -> markNewsletterRead /
   markAllRead changed every render -> the page effect (deps include it) re-ran
   after every render -> the mark set a NEW state object -> loop. The
   notification bell looped the same way while open.
   - useLocalStorage: setter is now referentially stable (ref + functional set).
   - useSyncedAppData: returned object is memoized.
   - useReadState: apply reads synced/interactive from refs (stable); marks are
     idempotent (same object when already marked) so React bails.
   - NewsletterPage marks read once per visit (mount-once ref guard).

2) Issues + subscription markers were published via the default pool (the
   author's own NIP-65 relays) but READ via useSkateRelays (community relay +
   reader's relays; logged-out = community relay only), so an admin without
   goskatespots in their list would publish drops nobody could see. Composer,
   backfill panel and the subscribe hook now publish to getWriteRelays() and
   fail loudly if every relay rejects (useSpotChat pattern). NIP.md untouched
   (no schema change).

Also: guard the overlay pill/reminder against an empty archive, and drop a
redundant canPublish clause.
2026-09-27 17:40:02 +07:00
SkateSpots
6a442c394f fix(mints): native app fetched /api/mints from the local shell -> HTML/JSON error
On the native app the shell is served from https://localhost, so the relative
/api/mints/known hit the local asset server and returned index.html, surfacing as
'Unexpected token < ... not valid JSON' on /mints.

- Native now fetches https://skate-spots.org/api/mints/known via CapacitorHttp
  (bundled core plugin, no WebView CORS), with a plain-fetch fallback.
- Web/dev unchanged (same-origin relative path via the Netlify redirect / Vite
  proxy).
- HTML or unparseable bodies now become a friendly 'unavailable' message instead
  of a raw JSON parse error.
2026-09-27 17:25:16 +07:00
SkateSpots
4e4c50ab71 fix(nav): desktop 'More' was clipped; mobile menu never listed Marketplace
- Desktop: the nav's overflow-x-auto clips its trailing item when the nav grows,
  which is exactly the 'More' menu. Widened the header (max-w-[1700px] vs the
  1400px page container), tightened nav padding to px-3 and nowrap. 'More' is
  fully visible at 1280/1440/2000px with Marketplace pinned (was missing).
- Mobile: the burger menu hard-coded only Map/Lab/Events/Crews + a few extras, so
  Marketplace (and Mints) were unreachable. It now appends every nav-catalog item
  not already listed, giving mobile/desktop parity (Marketplace, Mints, …).
2026-09-27 17:18:11 +07:00
SkateSpots
0b5230d4ab fix(directions): route map never rendered + dialog stacked under the spot sheet
RouteDialog was effectively broken — users got an empty dialog / 'no route'.

- Map init: the Leaflet map lives in Radix Dialog content, which mounts AFTER
  the first effect pass, so the mapDivRef guard was null, the init effect bailed,
  and the map was never created (no route line, ever). Hold the node in state
  with a callback ref and add it to the effect deps.
- Height: the global .leaflet-container{height:100%} override beats h-64; against
  a height-less parent it resolved to 0 (invisible map). Height now lives on the
  wrapper (relative h-64) and the map is h-full.
- Stacking: from the map, Directions rendered the dialog inside the spot Sheet
  (two nested Radix modals; the sheet covered the route). The map now owns the
  RouteDialog at page level and closes the sheet first.
- Routing transport: call OSRM directly (it sends CORS '*') with the cors proxy
  only as fallback; distinguish busy / no-route / unreachable errors and guard
  against NaN coordinates.
2026-09-27 17:07:44 +07:00
SkateSpots
e5e2f7d7a1 fix(nav,amber,reputation,routing): header crowding, no read-state popups, anti-collusion rep, instant directions
- Header: shrink-0 logo + whitespace-nowrap + gap, nav shrinks/scrolls so it
  can never collide with (or glow over) the SkateSpots wordmark; tighter item
  padding. Fixes the top-left logo/nav overlap.
- Amber: useReadState keeps read markers LOCAL-ONLY for interactive signers, so
  merely opening the bell/DM no longer publishes (encrypt + sign = 2 popups).
- Reputation: lead with verifiedCounterparties (distinct people), cap each
  partner at 3 in the WoT-weighted score, warn when every confirmation is with
  one account. NIP.md documents the collusion limit honestly.
- Directions: skip the live GPS call when the cached fix is <2 min old; the
  blocking 'Finding your location' overlay only shows with no seed (silent
  refine chip otherwise). Near Me also flips the sort to proximity.
- .gitignore: cvm-server/package.json (local dev copy of package.cvm.json).
2026-09-27 16:42:39 +07:00
SkateSpots
efa8b2e521 feat(marketplace): 1-10 deck ratings + seller dashboard
- Reviews: confirming a trade now lets the reviewer set a 1-10 skateboard-deck
  rating, stored as ["rating","n"] on the kind-31871 attestation (NIP.md updated).
  The Reputation tab shows each review's decks plus an average.
- New DeckRating component (self-drawn deck glyph) and TradeConfirmRow (rating
  picker + confirm), used for both seller-confirm and buyer-confirm in My Trades.
- New 'Selling' tab: your marketplace listings (active/sold) with one-tap
  Mark sold.
2026-09-27 16:26:24 +07:00
SkateSpots
14b3ee0bf2 feat(marketplace): hub with Browse / My Trades / Reputation (+ reviews)
- /marketplace is now a hub with a sticky tab bar: Browse (the gear grid),
  My Trades (existing flow), and Reputation — the trader's reputation score,
  peer recommendations, and a Reviews/trade-records list built from kind-31871
  attestations (who, sold/bought, method, amount, note, link to the listing).
- Nav: the old 'trades' entry becomes 'Marketplace' -> /marketplace (no longer
  auth-only, since browsing is public).
- /trades and /listing/... deep links keep working.
2026-09-27 16:14:47 +07:00
SkateSpots
4ed4e6f6fb release: v1.5.1
Patch bump over the unreleased 1.5.0, folding in today's mobile fixes: Mints in
the phone nav, instant Directions, and no stray signer prompt for the AI balance.
2026-09-27 16:04:53 +07:00
SkateSpots
1ab8f96829 chore: sync package-lock version to 1.5.0 (matches package.json, keeps the tree clean) 2026-09-27 15:55:14 +07:00
SkateSpots
f95e2233c8 fix(mobile-ux): Mints in the nav, instant directions, no stray signer prompt
- Nav: MIDDLE_SLOTS 3->4 and add 'mints' to the default middle + desktop tabs,
  so the Cashu mints directory is reachable on the phone out of the box (also
  still linked from Wallet; any of the 4 slots can be swapped via long-press).
- Directions: RouteDialog now seeds the origin from the last known location and
  draws immediately, then refines with a live fix — no more hanging on 'Finding
  your location…'. Shortened the web geolocation fallback 15s -> 8s.
- Amber: /api/ai/status is now public (it only reports a small balance), so
  showing the AI balance no longer signs NIP-98 / opens the signer. The signer is
  reserved for actions that actually need it.
2026-09-27 15:54:29 +07:00
SkateSpots
7969e8e9de release: v1.5.0
Sasquatch Pro (AI), in-app directions (transit/bike/moto/car), Barbers &
Hairstylists in The Lab, and a smoother app shell (no remounts, no stray
signer pop-ups).
2026-09-27 15:42:52 +07:00
SkateSpots
821522327c feat(sasquatch): show the owner AI balance on the panel header
Owner-only chip (next to the Pro/CVM chips) showing the upstream sats balance,
amber when low. useSasquatchAiStatus gains an 'enabled' option; the panel only
fetches while it is open, so the NIP-98 signature never fires at app startup.
2026-09-27 15:36:09 +07:00
SkateSpots
69e1ae17b8 docs(agents): note the owner AI-balance status endpoint + card 2026-09-27 15:28:14 +07:00
SkateSpots
62b395f3d2 feat(sasquatch): align client models with the node + owner AI-balance nudge
- sasquatchBrain: PROXY_MODELS now match the Routstr node's slugs
  (gemini-3-6-flash, z-ai-glm-5-3, gemini-3-7-flash).
- ai.mjs: new owner-only POST /api/ai/status (NIP-98 + owner pubkey) returning the
  upstream balance + a 'low' flag (<500 sats); config.path now serves both routes.
- useSasquatchAiStatus + SasquatchAiStatusCard (owner-only, in Settings): shows the
  balance and warns when low — the top-up nudge, no webhook/notification key needed.
2026-09-27 15:27:32 +07:00
SkateSpots
972941d688 docs(agents): Routstr is now the AI upstream (githappens node)
Record the working node, the fund/refund recipe, the aggregator outage, the
'orangesync bad key' trap, and that Netlify snapshots env at deploy (redeploy
after env changes).
2026-09-27 15:22:37 +07:00
SkateSpots
00aab5369b feat(ai-proxy): server-authoritative models (provider switch = env only)
If the client asks for a model not in the allowlist, the proxy now uses its
configured default instead of 400-ing. So switching the upstream to a Routstr
node is purely server-env (SASQUATCH_AI_BASE/KEY/MODELS) with no client redeploy.
AGENTS.md documents Routstr discovery (kind 38421, endpoints in 'u' tags), the
live nodes found, and the fund->key recipe.
2026-09-27 15:11:25 +07:00
SkateSpots
a5a134c6c0 feat(ai-proxy): provider-agnostic upstream (enables a sats-only provider)
The proxy now reads its upstream from SASQUATCH_AI_BASE + SASQUATCH_AI_KEY
(falling back to Netlify's injected OPENROUTER_* vars), and the model allowlist
is overridable via SASQUATCH_MODELS. Default behaviour is unchanged (gateway).

This makes switching to a sats-only provider a config change, e.g. Routstr:
  SASQUATCH_AI_BASE=https://api.routstr.com/v1
  SASQUATCH_AI_KEY=cashuA… (raw token) or sk-… (session)
NOTE: api.routstr.com was returning 404 (empty models) on 2026-09-27, so we are
NOT switching yet. Documented in AGENTS.md.
2026-09-27 15:01:13 +07:00
SkateSpots
f94297b196 docs(agents): note the non-site deploy skip 2026-09-27 14:43:02 +07:00
SkateSpots
9d0c31f673 chore(deploy): skip production deploys for non-site commits
Netlify bills 15 credits per production deploy, and this cycle already spent
~315 on 21 deploys. auto-deploy.sh now skips when the change between the last
deployed SHA and origin/main touches only non-site files (AGENTS.md/*.md,
scripts/, cvm-server/, skills/) — it records the SHA without deploying, so doc
commits cost nothing.
2026-09-27 14:42:57 +07:00
SkateSpots
70440c4bce docs(agents): Pro gate is enabled in production (SASQUATCH_REQUIRE_PRO=1) 2026-09-27 14:32:30 +07:00
SkateSpots
8d272a9c76 feat(sasquatch-pro): crew agent sends Pro credentials too
useCrewAgentChat now signs the same NIP-98 auth + attaches the viewer's Pro
receipt (when they have one), so the proxy's Pro gate lets Pro users through the
gateway models; everyone else still falls back to the keyless provider.
2026-09-27 14:27:02 +07:00
SkateSpots
ccc04cc172 feat(sasquatch-pro): server-side Pro gate on the AI proxy + client credentials
- netlify/functions/ai.mjs: optional Pro enforcement (SASQUATCH_REQUIRE_PRO=1).
  Requires a NIP-98 kind-27235 auth header (x-sasquatch-auth) and, unless the
  caller is a platform owner, an active Pro pass (x-sasquatch-pro = a
  provider-authored zap receipt) verified OFFLINE — signature, user binding,
  platform tag, and paid-amount → tier. Off by default.
- src/lib/proAuth.ts: signs the NIP-98 once per session (no Amber popup per
  message) and attaches the Pro receipt.
- sasquatchBrain: threads auth headers through the proxy (streaming + non).
- useSasquatchPro: exposes the winning receipt event.
- Verified: 200 with the gate off, 401 with it on (env reset afterwards).
2026-09-27 14:18:33 +07:00