Commit graph

559 commits

Author SHA1 Message Date
shakespeare.diy
00482fc332 Security polish (audit F5-F6): disable broad allowMixedContent on Android (loopback cleartext stays via network_security_config); document CVM client's intentional fixed data-relay set + IP-disclosure privacy note
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-24 18:15:19 +07:00
shakespeare.diy
f316c77806 Security hardening (audit F1-F4): secure Cashu seed in native Keychain/KeyStore with mandatory first-time backup; verify event signatures for trust scores (WoT + NIP-85 Relatr); add safeJson guard on config boot parse; delete dead CashuWallet.tsx with raw alert(mnemonic)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-24 18:13:58 +07:00
shakespeare.diy
52dc0adbc7 Refine onboarding: collapse "Why bother?" for a lighter first view; update Suan Luang Backyard tagline
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-16 10:25:47 +07:00
shakespeare.diy
76411f1666 Add Suan Luang Backyard to Supporters friends section
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-16 10:20:03 +07:00
shakespeare.diy
88a5b03bcd Set zapstore.yaml repository to browsable nostrhub.io source URL so Zapstore renders a Source link
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-14 10:25:49 +07:00
shakespeare.diy
06cfadff33 Polish relay list UI: match CitrineCard style, add Local chip, on-brand read/write chips, and live connection dots
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-14 10:14:16 +07:00
shakespeare.diy
46fbb0df43 Map loading: replace fuzzy ghost-pin blobs with a clean top bar + pill
The cold-start loading state scattered fuzzy pink "ghost pins" across the map
(the same blob look that was disliked on real markers) which read as broken.
Removed MapSkeletonPins entirely.

New loading state, per the agreed plan (keep the map instant, never cover it):
- A slim animated top progress bar ("ss-loadbar" sweep on a faint track) that
  sits above the map while spots stream in.
- During a genuine cold start (no markers yet), a small labeled "Loading spots"
  pill with a spinner near the top — honest about what's happening (querying
  relays), not fake pins on the ocean.
- Reduced-motion respected (bar becomes static).

The map itself is untouched and still renders immediately; only the placeholder
changed. Dropped the now-unused MapSkeletonPins import. Build clean, no console
errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-14 09:54:07 +07:00
shakespeare.diy
a40c86e655 Map pins: use a single generic 🛹 glyph instead of per-category emojis
Replaced the per-category emoji map (street/park/DIY/plaza/transition) with one
generic skateboard glyph on every spot pin, per request. The pin's crisp shape,
tight halo and shelter roof-cap (covered/indoor) are unchanged. Dropped the now-
unused `category` option from createSkateSpotIcon and its call sites.

Build clean; the earlier "CATEGORY_GLYPH is not defined" console errors were
from a stale intermediate bundle and do not occur in the current build.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 18:50:34 +07:00
shakespeare.diy
345af97788 Map Pass 1: crisp pins, spot-type & shelter glyphs, floating loading, micro-interactions
Visual foundation for the map (ideas #2, #3, #4, #6 — features #5/#7 intentionally
deferred to a later pass):

#2 Crisp pins, not blobs:
- Reworked createSkateSpotIcon — solid neon teardrop with a TIGHT glow halo
  (inset dark ring + focused shadow) instead of the soft box-shadow that read as
  a fuzzy cloud on light tiles. Cluster badges given the same crisp treatment.

#3 Marker = spot type (a readable language):
- Category emoji inside each pin: 🛹 street / 🏞️ park / 🔧 DIY / 🏛️ plaza / 🌊 transition.
- Covered/indoor spots get a cyan roof "cap" so dry spots stand out at a glance —
  the payoff of the shelter data living on the map, not just a badge.
- createSkateSpotIcon now takes an options object (weatherColor/weatherIcon/
  category/shelter); all call sites updated. Weather-layer colouring unchanged.

#4 Less scrim, floating loading:
- Suspense map-load fallback dimming reduced (bg-black/50 → /20).
- "Finding spots" pill moved from dead-centre (top 46%) to a slim top pill so it
  never blocks the map where the spots are.

#6 Micro-interactions (CSS, GPU-friendly, reduced-motion aware):
- New pins drop in with a spring; hover/focus/tap lifts the pin and pulses its
  neon halo once.

Build clean, no console errors. No tile/theme change — the "og" light map is
untouched per request.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 18:46:15 +07:00
shakespeare.diy
a7b7e07e86 Add spot "Shelter" (weather protection) field
Skaters need to know if a spot is roofed/indoor so weather info isn't
misleading — a covered or indoor spot is still skateable in the rain. Added a
4-way shelter level: outdoor (default) / partial / covered / indoor.

Data model (types.ts + skateTags.ts):
- SkateSpot.shelter parsed from a `['shelter', value]` tag (validated to known
  values; absent/unknown → treated as outdoor).
- shelterOptions / DEFAULT_SHELTER / SHELTERED_VALUES + ShelterType exported.
- SHELTER_INFO display map (emoji/label/short/color) for badges.

Publishing (CreateSpotPage + EditSpotPage):
- New "Shelter" selector reusing the exact selectable-Card pattern used by
  Access Type / Spot Type (cn() borders, neon ring, corner check badge).
- Emits `['shelter', value]` always, plus a queryable `['t', value]` for
  sheltered spots (indoor/covered/partial) so "dry spots" filtering + Sasquatch
  search work at the relay level. Outdoor adds no t tag.
- EditSpotPage initialises from the existing spot and preserves the value.

Display:
- Shelter badge on SpotDetailPage, SpotCard, and the MapPage popup — only shown
  for sheltered spots (outdoor stays unbadged to avoid clutter).

Docs:
- NIP.md: shelter added to the optional-tags table, a dedicated Shelter section
  explaining values + the queryable-t-tag behaviour, and an indoor example.

Build clean, no console errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 11:38:43 +07:00
shakespeare.diy
993bea1b5b Sasquatch Pro: stop selling features that don't exist
Audit found the Pro paywall promised three things with no implementation:
- "Priority AI model, higher limits & longer memory" — Pro and free hit the
  exact same model/limits/history (only difference is the personal-tools prompt)
- "Proactive nudges (weather + spot tips)" — no nudge/scheduler exists anywhere
- "Pro-only Sasquatch skins" — single hardcoded avatar; no skin system
- "Supporter badge" — gating exists but no badge is rendered

Rewrote all Pro copy to promise ONLY what actually ships (personal assistant
over the user's own Stash/crews/spots/listings, confirm-before-publish action
drafts, personal-context answers, and directly funding an ad-free app):
- SasquatchProDialog PERKS list (dropped Sparkles import now unused)
- usePurchasePro receipt DM
- 6 STATIC_FAQ Pro answers in SasquatchGuide

Pricing, payment target (skatespots@minibits.cash) and npub-bound zap-receipt
verification were already accurate and left unchanged. Build clean, no console
errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 00:27:34 +07:00
shakespeare.diy
5506344893 Make Sasquatch tell the truth: direct relay queries, not a CVM server
The chat's data layer (useCVMClient) queries Nostr relays directly from the
browser — the CVM/ContextVM server round-trip was removed earlier for speed.
But the UI and system prompt still claimed data came "from the CVM server",
which is no longer true. Aligned every user-facing claim with reality:

- System prompt: reframed "CVM Tools / MUST route through CVM" as "Live data
  tools that query the Nostr relays directly". Tool-calling behaviour unchanged.
- Streaming status: "Querying CVM for X" → "Searching the relays for X".
- Tool-failure message: no longer blames a non-existent "CVM server"; now
  correctly frames it as relays being slow, with a retry nudge.
- Empty results: stop asserting "not added yet" as fact — an empty result can
  also mean slow relays or a misspelled place. Now offers retry + add-a-spot,
  and names the place searched. Applied to the local formatter, the system
  prompt rules, and the second-pass AI instruction.
- Chat footer: "Powered by ContextVM & Nostr Relays Data" → "Live data from
  Nostr relays" (links the primary community relay).
- About page credit: "AI powered by ContextVM" → "Trust & crew chat via
  ContextVM" (its real role: Relatr trust scores + encrypted crew chat).
- Owner CVM health panel: clarified it probes the standalone MCP server and
  that the in-app chat does not depend on it.
- Updated stale internal comments in SasquatchGuide + CreateSpotPage.

ContextVM references that ARE accurate (Relatr trust scores, crew-chat Cordn
coordinator) were left intact. Build clean, no console errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 00:16:59 +07:00
shakespeare.diy
ee23f3e063 Fix spot dedupe: merge same-named spots across geohash/city + name-suffix variants
The old identity dedupe keyed on `name|place` where place was a single field
(geohash OR location OR city, first non-empty). Two copies of the same spot —
one carrying a geohash, one carrying only a city — produced different keys and
both survived (the Yellow Park duplicate).

Now:
- Name is reduced to a signature that strips trailing generic words
  (skatepark/park/plaza/spot/diy/...) and leading articles, so "Yellow Park",
  "Yellow Park Skatepark" and "Yellow Park " collapse to "yellow".
- Places are compared on human-readable tokens (location/city/country) only;
  the opaque geohash is excluded. An empty place on either side is treated as a
  wildcard, so a geohash-only copy still merges with a "Bangkok" copy.
- Genuinely different spots (different name signatures, or non-empty places with
  no shared token) are still kept separate; single-word names are preserved so
  generic names don't over-collapse.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 23:56:00 +07:00
shakespeare.diy
7e7cf0a8bf Sasquatch fast-path: don't dead-end on empty/typos
The local fast-path was stricter than the AI it replaced: a typo like "bangok"
won't substring-match "Bangkok", so the fast-path returned a false "No spots
listed there yet" even though Bangkok has spots.

Two-layer fix:

1. Safety net (most important): if a finder fast-path comes back EMPTY, fall
   through to the AI path instead of rendering "nothing found". The AI normalizes
   spelling and can still find results. Stats/weather always render immediately.
   Net effect: fast when the local match works, correct-always via AI when it
   doesn't — never a false empty.

2. Typo tolerance: findSpots city matching now uses placeMatches() — substring
   OR edit-distance-1 against candidate word tokens (only for queries ≥4 chars,
   so short words can't over-match). So "bangok" matches "Bangkok" on the fast
   path too. Freeform content stays a strict substring check to avoid false hits.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 23:52:08 +07:00
shakespeare.diy
6df526bf27 Sasquatch: local fast-path for finder questions (kills ~30s AI wait)
Root cause of the ~35s "spots in bangkok" response: every live-data question
first waited on an AI round-trip just to produce a tool call, and the keyless
backup model (Pollinations) can take tens of seconds. The relay query and local
formatting were already fast — the AI was the bottleneck.

Fix: before touching the AI, try parseLocalIntent(). For clear finder questions
(spots/events/coaches/crews/shops/tours/gear + community stats + weather with a
parseable location) we now go STRAIGHT to the relays (capped at 4.5s) and render
locally — zero AI calls. Expected ~35s → ~2–4s on the most common queries.

Deliberately still routed through the AI:
- trip planning (needs the rich itinerary card)
- personal "my …" Pro queries (get_my_* tools)
- anything without a confidently-parseable intent/location

If the fast-path relay query errors, it falls through to the existing AI loop as
a backup, so nothing regresses.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 23:24:08 +07:00
shakespeare.diy
8b7d9ca4ea Fix Sasquatch spot results: honest count + collapse duplicates
Two bugs from "spots in bangkok" (said "Found 20" but showed 6, with repeats):

1. Misleading count: formatLocalResults hard-capped the visible list to 6 while
   the headline reported the full count. Now shows up to 10 and appends a
   truthful "…and N more — see them all on the map" line, so the number and the
   list always agree.

2. Duplicate spots: coordinate dedupe (kind:pubkey:d) can't catch the same
   real-world spot re-posted under a different identifier or by another account
   (e.g. "Yellow Park – Sukhumvit 50" appearing twice). Added
   deduplicateByIdentity() — a second-pass dedupe on a normalized
   name + geohash/location signature, keeping the freshest event — applied in
   findSpots so the count and list are clean.

Both fixes flow through the AI tool-call path and the offline fallback path.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 23:17:15 +07:00
shakespeare.diy
ada4bfee5e Accelerate Sasquatch custom questions (live-data queries)
Three low-risk speedups focused on the slowest path — questions that hit live
relay data:

1. Relay timeout 10s → 4.5s (6s for the fan-out community stats). The useful
   relays answer in ~2s; the old 10s just let one slow/dead relay stall the
   whole reply.

2. Results-first: for finder tools (spots/events/coaches/gear/crews/shops/
   tours/stats/weather) format the CVM result locally and SKIP the second
   "prettify" AI call. Removes a full model round-trip (~2–4s) — users want the
   list, not extra prose. New toolCallToIntent() maps a tool call to the existing
   formatLocalResults() path. Personal/Pro tools (get_my_*) still use AI synthesis.

3. Faster AI fallback: cap OpenRouter attempts to the top 3 free models before
   the keyless backup, and shorten 429 backoff (≤800ms vs ≤2500ms). Avoids the
   worst-case 15–20s grind through 8 models on a busy free tier.

Conversational/knowledge answers (single AI call) keep Sasquatch's personality
and are untouched.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 23:11:14 +07:00
shakespeare.diy
7eba7dfe22 Calendar: show session time for scheduled local tours
The shared EventsCalendar already renders tours alongside events with live
countdowns on cards/detail pages. This refines the agenda time display: local
tours now carry a specific session time, so the calendar shows that clock time
instead of a blanket "All day". Multi-day national/international/destruction
tours remain date-only ("All day" / date range), and events are unaffected.

- Add allDay flag to CalendarItem (set from tour category)
- Local sessions show their start time; other tours stay all-day

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 22:34:23 +07:00
shakespeare.diy
5d96f8dfa8 Add tour countdown badges + fix/standardize NIP-05 provider prompts
Countdown badges (upcoming tours):
- New getCountdown() in TourCard: "In 3 days" / "Tomorrow" / "In 2 weeks" /
  "Happening now" (pulsing) based on start/end dates, category-tinted
- Shown on both compact and full TourCard, and in the TourDetailPage schedule card

NIP-05 provider fix (removes obsolete nip05.social):
- New single source of truth src/lib/nip05Providers.ts with working, curated
  providers: nostrcheck.me + nostr.directory (free), Nostr Plebs + nostr.band (paid)
- New reusable Nip05Required component (full-page "gate" + inline "banner"
  variants) listing those providers and deep-linking to /settings?section=nip05
- Wired into CreateTourPage (gate), CreateListingPage (banner) and all four
  Become* pages (Coach/Filmer/Photographer/Builder) for a consistent prompt
- SettingsPage NIP-05 screen now reads from the shared provider list
- Updated internal planning docs to match

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 22:31:44 +07:00
shakespeare.diy
1c295e96e2 Improve Create Tour: consistent schedules, calendar export & punk redesign
- Add a unified Schedule section across all four tour categories:
  - Local: choose a fixed session (date+time) or flexible/by-appointment
  - National/International: required departure + optional return trip dates
  - Destruction: existing competition window, now with end>=start validation
- Reuse start-date/end-date tags for every category (parser already supported them)
- Fix datetime handling: dedicated toDateInput/inputToSecs helpers so date and
  datetime-local inputs round-trip correctly (previous mix of toLocalInput +
  type="date" was inconsistent)
- Consistent, logical mandatory-field validation with clear required markers
- Redesign the header into an immersive, category-tinted, grunge-striped banner
  and rebuild the category selector as elegant glowing cards
- New calendar utilities (.ics + Google Calendar) and AddToCalendar component
- TourDetailPage is now fully category-aware (badge, guide label, duration,
  SEO title) with a Schedule card + Add to Calendar for attendees
- TourCard shows the schedule for all categories (time for local sessions)
- Update NIP.md to document start-date/end-date across categories

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 22:16:16 +07:00
shakespeare.diy
a984084464 Instant cold-start: query persistence, optimistic geolocation, skeleton pins
Three UX wins so the app feels "already loaded" the moment it opens:

1. IndexedDB React Query persistence (lib/queryPersister.ts)
   - PersistQueryClientProvider rehydrates the last-seen spots, shops,
     events, tours, crews, coaches, listings and author profiles from
     IndexedDB on cold start, so returning users land on a populated map
     instead of a blank one; fresh data refetches quietly in the
     background.
   - Strict allow-list / deny-list: only public, JSON-serializable,
     cold-start-relevant queries are persisted. Private/encrypted data
     (stash, DMs, contacts, wallet/nwc/cashu/bitcoin, signer) is NEVER
     written to disk. 24h maxAge + version buster keep it fresh & safe.
   - Pinned @tanstack/react-query + query-core + persist-client to a
     single 5.102.8 to avoid a query-core mismatch (partialMatchKey).

2. Optimistic geolocation (lib/lastLocation.ts)
   - getCurrentPosition now caches every successful fix.
   - The map opens ALREADY centered on the user's last-known area
     (city zoom) instead of the world view; "you are here" + proximity
     sort seed from the cache instantly.
   - "Near Me" flies to the cached location immediately on tap, then
     refines to the fresh GPS fix. Silent background warm-up on web map
     mount so a later tap resolves instantly.

3. Cold-start skeleton (components/MapSkeletonPins.tsx)
   - When the map is genuinely empty + loading, show elegant pulsing
     ghost pins + a "Finding spots" pill instead of a blank map; they
     vanish the instant real (or cached) spots render. Splash now
     signals content-ready when spots actually resolve.

Respects prefers-reduced-motion throughout. Build + runtime verified
clean (no console errors).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 21:58:22 +07:00
shakespeare.diy
96f8c795c4 Bump version to 1.4.4 (package.json is source of truth for Android version)
Prevents the android:sync script from regressing versionCode/versionName
back to 1.3.7. 1.4.4 → versionCode 10404 (above the previous 10403).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 19:34:30 +07:00
shakespeare.diy
f9d0310c84 Make SkateSpots logo consistent everywhere
- Swap the map "you are here" badge (user-location.png) to the clean
  orange logo instead of the busy sticker art
- Remove stale red-pin icon.svg / generate-icons.html artifacts that
  were unused and would regenerate the wrong logo
- Rewrite LOGO_USAGE.md to describe the real logo, list every place it
  is used, and document how to regenerate the native Android launcher
  icon with @capacitor/assets
- Update IMPLEMENTATION_COMPLETE.md references

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 19:29:09 +07:00
shakespeare.diy
7c289c5429 Align nostr-tools spec with lockfile to fix local npm ci (1.4.4)
package.json pinned nostr-tools to an exact 2.13.0 while the committed
package-lock.json resolved 2.15.0 (root spec ^2.13.0), so `npm ci` on a
local machine refused to run ("lock file's nostr-tools@2.15.0 does not
satisfy nostr-tools@2.13.0"). Relaxed the pin to ^2.13.0 to match the
lockfile and every other dependency's caret range. Cloud build verified.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 19:13:56 +07:00
shakespeare.diy
40e4e53400 Hardening: https-only URLs, route-level crash resilience, cleaner errors (1.4.3)
Security & reliability audit follow-up. The app was already well-hardened
(strict CSP synced across index.html/_headers/netlify.toml, careful
textContent/escapeHtml in the Leaflet DOM, broad sanitizeUrl usage,
consistent author-filtering on trust-sensitive queries). This closes the
remaining gaps:

- sanitizeUrl is now HTTPS-only: bare http:// links are upgraded to
  https:// (not silently dropped); every other scheme (javascript:,
  data:, file:, relative) is rejected. Removes a downgrade/mixed-content/
  IP-leak vector and matches the native HTTPS-only WebView. Added a
  sanitizeCssString() allowlist helper for future CSS interpolation.

- Route-level ErrorBoundary (RouteErrorBoundary) wraps the routed pages,
  keyed by pathname: a crash in one page now shows a recoverable
  "this page slammed → go back" message while the header/nav/rest of the
  app keep working, instead of the single top-level boundary blanking the
  ENTIRE app. Resets automatically on navigation.

- ErrorBoundary fallback no longer leaks stack traces to users (full
  detail still logged to console for us); redesigned on-brand ("Slammed",
  neon, reassures that account + data are safe). Updated its test.

Author-filtering spot-check across admin/newsletter/featured/crew/stash
paths found no gaps — all trust-sensitive queries correctly constrain
authors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 12:40:43 +07:00
shakespeare.diy
8d7b4f914b Elevate splash to premium, choreographed launch (1.4.2)
Rebuilt the cold-start splash to a best-in-class standard:
- Determinate progress bar driven by real milestones (mount creep →
  relays-warm nudge → 100% on ready) with a sliding shine highlight —
  reads as genuine loading, not an infinite spinner.
- Choreographed staggered entrance (logo → wordmark → bar → status rise
  in sequence) on an expressive ease curve.
- Signature logo treatment: rotating red→lime conic halo ring + a gentle
  breathing pulse, over a dual breathing glow and a masked skate-map grid
  texture for depth.
- Satisfying "ready" beat: bar fills to 100%, status swaps to a lime
  check "Ready", then a soft scale-up crossfade reveals the app.
- 8px-grid spacing, refined tracking/typography.
- Fully respects prefers-reduced-motion (all motion gated) and safe-area
  insets on all four edges; identical on phone and desktop; still capped
  hard at 2.6s so it never traps the user.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 12:19:17 +07:00
shakespeare.diy
a12342f7d1 Splash: reveal on real content + graceful shell fade-in (1.4.1)
Make the landing feel "already loaded" instead of dismissing on bare
connection:

- MapPage fires `skatespots:content-ready` once the first spots query
  settles. The splash now dismisses on that signal (populated reveal),
  falling back to a short grace window after `relays-warm` for non-map
  cold-start routes, and still capped hard at 2.2s so it never traps.
- App shell eases in under the departing splash via a pure-opacity
  `appReveal` animation (no translate, so a full-bleed map never shifts),
  respecting prefers-reduced-motion. Result is a smooth crossfade onto
  content that's already there rather than a pop-in.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 12:10:47 +07:00
shakespeare.diy
f41af8f1ca Cold-start: pre-warm relays + branded splash screen (1.4.0)
A. Relay pre-warming — NostrProvider now opens the read-relay WebSockets
   the moment it mounts (fire-and-forget limit:0 probes with a 4s abort),
   instead of NPool's lazy open on first query. Sockets are already
   connected by the time the map queries run, so first events land sooner.
   Failures are swallowed so a dead relay never delays or breaks boot.

B. Branded SplashScreen — an intentional cold-start moment (logo with a
   neon glow pulse, a red→lime progress sweep, and rotating skater-voiced
   status lines) that replaces the "is it broken?" empty-map flash. It
   dismisses the instant relays report warm (`skatespots:relays-warm`
   event fired on the first relay response) OR at a hard 1.6s cap —
   whichever comes first — with a 650ms minimum to avoid flicker. Shown
   once per session. Fully respects prefers-reduced-motion and safe-area
   insets; works identically on phone and desktop, never blocks the user.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 12:05:35 +07:00
shakespeare.diy
aede706fd3 Fix signup flow skipping profile + recovery steps (1.3.16)
Root cause: the multi-step SignupDialog is hosted inside LoginArea, but
both the mobile header and the onboarding hero only rendered LoginArea
while logged OUT ({user ? menu : <LoginArea/>}). The signup flow logs the
user in at its FIRST step (to publish the profile), which flipped `user`
truthy and instantly UNMOUNTED LoginArea — destroying the dialog before
the profile and recovery steps could show. The skater was dumped on the
map with no recovery prompt (exactly the reported bug).

Fix: keep LoginArea mounted after login and merely hide its trigger
(dialog renders in a portal, so hiding the trigger is purely cosmetic).
MobileHeaderUserMenu / logged-in CTAs render alongside. The in-progress
signup now survives login and reaches the redesigned recovery step.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:34:23 +07:00
shakespeare.diy
3cc3b623cc Redesign signup "done" step to lead with recovery (1.3.15)
Reworked the final signup step so saving your login key is the hero,
not an afterthought:
- Celebration shrunk to a compact bar ("You're in! One quick thing…")
  so it no longer steals focus from the safety step.
- "Save your login key" card is now the centerpiece with a loud amber
  primary CTA (Reveal & save), a skater-native framing ("keys to your
  board", "only way back in — no email, no password reset").
- Download button flips to a green "Saved ✓" state for clear feedback.
- "Skate now" is intentionally quieter and honest: reads "Skip for now,
  skate" with a reassuring note until the key is saved, then becomes a
  confident lime "Let's skate" once done. Never blocks the user.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:30:06 +07:00
shakespeare.diy
d8adf0746d Pin "Save your login" banner above the fold after signup (1.3.14)
New skaters landed on the onboarding page with the recovery/backup
prompt below the fold, so it read as "nothing about recovery". Moved the
critical "Save your login" banner ABOVE the hero (stronger border, glow,
uppercase headline) so it's the first thing a newly-signed-up user sees.
Removed the duplicate lower banner. Still non-blocking and stays until
backup-confirmed; the post-login bottom sheet remains as a second net.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:27:16 +07:00
shakespeare.diy
4dc2b0129e Make backup gently unavoidable, not skippable-forever (1.3.13)
Closes the real safety hole: "Remind me later" used to set a PERMANENT
localStorage flag, silently becoming "never remind again" — so users who
never backed up were never reminded again.

- BackupReminder: "Later" now snoozes for the current session only
  (sessionStorage), so it returns each visit until backup is actually
  done. Only `backup-confirmed` stops it for good. Never blocks the user.
- Downloading OR copying the recovery code (in SignupDialog's done step
  and in SecuritySettings) now marks backup-confirmed — genuinely safe
  users stop being nagged automatically.
- Plain-language copy pass (EN + ES): "Save your login" / "recovery code"
  instead of "nsec / recovery phrase" jargon.
- Non-blocking and identical UX on both phone and desktop (bottom sheet).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:23:44 +07:00
shakespeare.diy
e3fb84d539 Use vector skater glyph in Lab cross-link (1.3.12)
The PNG icon kept failing to render (corruption + browser caching of the
broken response) on both phone and desktop. Replaced it with the
PersonStanding Lucide vector icon in neon red with a red glow — it always
renders, never caches broken, is truly single-color and on-brand, and
works identically across phone and desktop. Removed dead image assets.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:20:19 +07:00
shakespeare.diy
0bc2a7c60b Fix broken skater icon + add signup reassurance line (1.3.11)
- Regenerated the Lab skater icon (previous file was corrupted, showing a
  broken-image placeholder). Now a valid monochrome neon-red PNG.
- Signup welcome step: added a subtle, jargon-free reassurance line under
  "Create quick account" — "Takes 5 seconds · stays on this device ·
  nothing to remember" — to make quick signup feel effortless and safe.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:17:46 +07:00
shakespeare.diy
a54d73c6e8 UI: cleaner single-color skater icon for Lab cross-link (1.3.10)
Swapped the busy multi-color icon for a clean, monochrome neon-red
skater glyph that reads clearly at small sizes. Refined the tile with a
subtle red border and clearer hover state for better visibility.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:12:11 +07:00
shakespeare.diy
acda8646fd UI: use small neon-colored skater icon in Lab cross-link (1.3.9)
Replaced the large black-and-white photo panel with a compact,
app-styled skater icon (neon red #ff0040 + lime #ccff00) sitting in the
rounded icon tile used elsewhere in the app. Added a red drop-shadow
glow to match the brand aesthetic. Removed the unused photo asset.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:10:22 +07:00
shakespeare.diy
8145379190 UI: feature skater photo in Lab cross-link + polish mobile bottom nav (1.3.8)
- Lab "Just a skater?" card now features the uploaded photo, framed and
  faded into the card body with a neon-red edge consistent with the app's
  look; adds a "The Crew" eyebrow label and a circular arrow affordance.
- Mobile bottom nav redesigned: glowing rounded pill behind the active
  tab (replaces the tiny dot), gradient glass bar with a top hairline
  glow, ring-2 wallet badge, and refined active/press micro-animations.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:07:57 +07:00
shakespeare.diy
1d769ed564 Onboarding: "Save your login" banner + collapse FAQ to reduce load (1.3.7)
Two greenlit onboarding improvements, keeping the existing layout intact.

#5 Save-your-login banner (the safety one):
- Right under the green "You're in!" bar, logged-in users who haven't backed up
  see a prominent amber banner — "Save your login — do this now" — that taps
  straight to Security settings. It reads the shared `backup-confirmed` flag
  (same source of truth as BackupReminder), so it PERSISTS until they actually
  back up; unlike the dismissible reminder sheet, it can't silently turn into
  "never". Once confirmed it flips to a calm green "Login saved" note. Plain
  language ("save your login", not "back up your nsec"). Pulse dot respects
  reduced-motion.

#1 Lighten first-glance load:
- The 9-item FAQ ("Common questions") is now a collapse-by-default section — one
  tidy row until tapped — via a small reusable CollapsibleSection (aria-expanded,
  reduced-motion aware). Nothing removed; the page just feels lighter on arrival.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 10:59:55 +07:00
shakespeare.diy
67e422307a Citrine audit: endpoint fallback, a11y, reduced-motion, UX polish (1.3.6)
Full self-audit of the Citrine feature. Fixes found:

Robustness
- Probe now tries BOTH ws://127.0.0.1 and ws://localhost and routes to whichever
  actually answers (probeCitrineEndpoints + CitrineProbeResult.url). We no longer
  bet the whole feature on one loopback address form; useCitrine.url reflects the
  winning endpoint so reads/writes go to the right place.

Correctness
- Preview toggle was mutating the REAL setting while in dev-preview; handleToggle
  is now a no-op under preview (forced state owns the visuals).

Accessibility / "flawless" bar
- Respect prefers-reduced-motion: status pips, chip pop-in and the expand slide
  now motion-reduce:animate-none (no looping glow/pulse for reduced-motion users).
- Recheck button gets an aria-label; status text is role="status" aria-live so
  screen readers announce searching→detected transitions; decorative pips are
  aria-hidden.

UX copy
- "Waiting" state no longer presumes Citrine isn't installed. Clearer two-step
  guidance (Open Citrine → Start; come back → Recheck) covering the common
  "installed but not started/detected yet" case.

Honest note: MIXED_CONTENT_ALWAYS_ALLOW (1.3.5) is the high-probability fix for
the 0-connections block but is not device-verified from here; the dual-endpoint
probe hedges the second most likely cause (address form).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 10:54:22 +07:00
shakespeare.diy
c7e890e65c Citrine: allow loopback ws:// in WebView so the local relay connects (1.3.5)
The real fix for "Citrine running but 0 connections": the app is served over
https in the WebView and Citrine listens on plaintext ws://127.0.0.1, which
modern Android WebViews block as mixed content BEFORE it reaches the network
layer — allowMixedContent + the loopback network-security-config don't fully
cover WebSocket mixed content.

MainActivity now sets the bridge WebView's mixed-content mode to
MIXED_CONTENT_ALWAYS_ALLOW after super.onCreate(), which lets the localhost
ws:// connection through for every user. All real traffic stays wss:///https://;
this only unblocks the loopback relay. The WebViewClient is left untouched
(overriding it previously caused a black screen). Wrapped in try/catch so it can
never crash the launcher.

Also removed the temporary on-screen probe debug readout (kept the diagnostic
fields in useCitrine, unused by UI, for future use).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 10:47:39 +07:00
shakespeare.diy
97adfba7e0 Citrine: on-device probe diagnostics (no USB needed) (1.3.4)
Citrine is running (listening on ws://127.0.0.1:4869) but the app can't connect
and shows 0 connections — a WebView-layer block, not our logic. To diagnose
without adb/USB, the probe now returns { ok, reason } capturing which stage
failed (blocked / connect-error / timeout-no-open / opened-but-no-nostr-reply /
closed-before-open with code), and the Settings card shows a small debug readout
(probe url + reason) whenever not connected. Purely additive; no behaviour
change to routing.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 10:44:38 +07:00
shakespeare.diy
cd3a353c8b Release notes for 1.3.3 (Citrine local relay + 1.2.34–1.3.3 fixes)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 19:40:13 +07:00
shakespeare.diy
913786c7c9 Citrine audit: verify Nostr handshake on probe + stop probe churn (1.3.3)
Self-audit of the 1.3.0–1.3.2 Citrine feature found two real issues:

1. False-positive detection (the important one). probeCitrine resolved true on
   bare socket OPEN, so ANY process squatting on 127.0.0.1:4869 would be treated
   as Citrine — and we'd then route reads AND published events at a non-Nostr
   peer. The probe now completes a minimal Nostr handshake: open → send a bounded
   REQ with a random sub id → require a protocol-shaped reply (EVENT/EOSE/CLOSED
   echoing the sub id, or a NOTICE) before declaring the relay live. Non-JSON /
   non-Nostr responses fall through to a clean timeout=false. A real relay
   answers with EOSE in ~ms on localhost, so the happy path is unchanged.

2. Probe churn. The reachability query re-opened a fresh ws:// probe every 5 min
   and on every window focus for the whole session — needless battery/wakelock,
   and a localhost poke on every Amber round-trip. Now it only polls (30s) and
   refetches-on-focus WHILE still waiting for Citrine to appear; once connected
   it stops the background churn (a per-session local relay won't vanish
   silently, and a failed read re-probes naturally).

Reviewed and left as-is (deliberate, safe): writing every kind to Citrine incl.
gift-wrapped DMs / Stash is intended on-device encrypted backup — all sensitive
kinds are NIP-44 encrypted before reaching the publish layer, so nothing
plaintext-sensitive is mirrored. Preview flag remains render-only.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 19:21:32 +07:00
shakespeare.diy
335fb40f72 Citrine: add safe dev-preview flag to review the UI in a browser (1.3.2)
The CitrineCard is Android-only (renders null off-native), so its four visual
states couldn't be reviewed without an APK build. Adds a strictly cosmetic
preview switch:

  localStorage.setItem('citrine:preview', '1')         // show card, real behaviour
  localStorage.setItem('citrine:preview', 'live')      // force connected state
  localStorage.setItem('citrine:preview', 'waiting')   // force not-detected
  localStorage.setItem('citrine:preview', 'searching') // force searching
  localStorage.removeItem('citrine:preview')           // back to normal

Safety: the flag unlocks RENDERING ONLY via isCitrinePossible(). It never sets
citrine.active and never yields a relay url, so no query or publish is ever
routed anywhere different — production relay behaviour is identical with or
without the flag. A forced state is purely visual (enabled/probing are derived
locally in the card for preview; the real probe still governs active/url).

lib/citrine.ts: citrinePreview()/isCitrinePreview() + CitrinePreviewState.
useCitrine: exposes `preview`. CitrineCard: forced state overrides the computed
state for review only.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 19:18:51 +07:00
shakespeare.diy
2e6a20dc2b Citrine: native cleartext config, default-pool write wiring, polished UI (1.3.1)
Completes the local-relay integration so it actually works end-to-end and feels
first-class.

Native (Android):
- android-templates/network_security_config.xml — permits cleartext for
  LOOPBACK ONLY (127.0.0.1 / localhost / ::1); all real traffic stays TLS. This
  is what lets ws://127.0.0.1 (Citrine) connect on API 28+, which otherwise
  silently blocks cleartext.
- scripts/inject-network-security.mjs — idempotently copies the XML into
  res/xml/ and adds android:networkSecurityConfig to <application>, mirroring
  the existing inject-amber-queries pattern so it survives `cap sync`.
- Wired into android:init / android:sync / android:build.

Reads AND writes now reach Citrine app-wide:
- NostrProvider's NPool routers (reqRouter/eventRouter) fold in the active
  Citrine URL via a ref, so every default-pool query hits it first (instant
  local reads) and every useNostrPublish write also lands on-device (backup +
  offline capture). Previously only explicit useSkateRelays paths benefited.
- A dedicated effect refreshes ['nostr'] queries when the local relay comes/goes
  so reads switch over immediately. Fully no-op off-Android (url stays null).

UI/UX (Relays settings CitrineCard):
- Four crisp states — off / searching / live / waiting — with a breathing then
  glowing status pip, benefit chips (Instant reads · Works offline · On-device
  backup) that pop in on connect, a spinning Recheck, an actionable install CTA,
  and a persistent encryption-reassurance note.
- Graceful first-run: shows "searching" until the first probe settles (via new
  useCitrine.settled), never flashing the orange "not detected" state.

Tailwind: added status-breathe / status-live / pop-in keyframes+animations.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 19:11:51 +07:00
shakespeare.diy
bd9b71095d Citrine: optional local on-device relay for instant reads + offline backup (1.3.0)
Integrates Citrine (github.com/greenart7c3/Citrine), an Android app that runs a
full Nostr relay locally on the phone. Because it's "just a relay URL", it folds
into the existing useSkateRelays / nostr.relay() machinery with no new protocol.

What it buys SkateSpots:
- Instant reads: a localhost hit (~1ms) is queried first, remote relays run in
  parallel and reconcile — every skate-content surface feels faster.
- Offline-in-the-field: reads work with no signal; writes land on-device and
  publish to remote when connectivity returns.
- Resilience/backup: a durable local copy of the user's own events survives
  public-relay outages and addressable-event eviction.
- Privacy: local-first data can lean on the device instead of public relays.

Design / safety:
- Android-only (Capacitor native + platform === 'android'); renders nothing and
  never probes localhost on iOS/desktop web. Mixed-content ws:// is already
  permitted via capacitor allowMixedContent.
- Opt-in only via config.useCitrineRelay (default off). No data is ever routed
  through Citrine unless the user enables it AND a relay is actually reachable.
- Reachability probe (short-lived WebSocket) cached through react-query, shared
  app-wide, re-checked periodically + on focus, with a manual Recheck button.
- When active, the local URL is prepended to reads and included in writes; the
  read-relay set feeds getQueryKeySuffix so caches refetch automatically when
  Citrine comes/goes.
- Encryption unchanged: Stash/DMs stay NIP-44 encrypted regardless of
  destination — the Settings card states this explicitly. Local ≠ plaintext.

New: src/lib/citrine.ts, src/hooks/useCitrine.ts, CitrineCard in the Relays
settings screen. AppConfig gains useCitrineRelay (zod-validated, optional).

On-device verification still needed: confirm Android permits ws://127.0.0.1
cleartext to localhost (network security config) once Citrine is running.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 18:53:16 +07:00
shakespeare.diy
a724dadad1 DM audit: stop group messages leaking into 1:1 threads/list (1.2.42)
NIP-17 group messages (rumor with >1 recipient p-tag) were bleeding into the
1:1 surfaces:

- useConversation (thread view): a group message from `otherPubkey` where I was
  also a recipient satisfied isFromOther && isToMe, so group chatter appeared
  inside the 1:1 thread.
- useConversations (list): group messages were collapsed to conversationWith =
  sender (or an arbitrary recipient for my own sends), spawning phantom 1:1
  entries and double-listing every group (once here, once in
  useGroupConversations).

Both 1:1 hooks now skip any rumor with more than one p-tag. Groups remain the
sole responsibility of the useGroupConversation(s) hooks, which already require
pTags.length > 1. Genuine 1:1 rumors carry exactly one recipient p-tag
(createRumor doesn't tag the sender), so no legitimate message is dropped.

Verified solid (no changes): the seal(13)/rumor(14) kind checks, the
rumor.pubkey === seal.pubkey impersonation guard, and the amberSafeQueryOptions
decrypt-storm protection for interactive signers are all correct.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 18:37:39 +07:00
shakespeare.diy
c376f37164 Newsletter audit: unique auto-slug so recurring titles can't clobber prior issues (1.2.41)
The composer derived the kind 30023 addressable `d` identifier straight from
slugify(title). Because 30023 is addressable, two issues sharing a slug —
a recurring "Weekly Roundup", or "Update!" vs "Update?" — would share a
coordinate and the newer publish would silently overwrite the older issue.

Blank slug now auto-generates a unique coordinate (title-slug + UTC date +
short base36 time suffix). An explicit manual slug is still honored verbatim,
so intentionally editing/replacing a specific issue keeps working.

Verified clean (no changes): backfill panel is idempotent (skips slugs already
on-relay, deterministic issue-{n} identifiers) and the subscriber-count hook
uses the standard newest-marker-per-author tally with no double-counting.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 18:34:54 +07:00
shakespeare.diy
6ad112dea3 Spot detail audit: dedup claim provenance tags (1.2.40)
Audited the spot detail surface — status reports, ownership, and
community contributions. It's well-built:

STATUS (useSpotStatus) — a proper trust×recency consensus: each report
weighted by recencyWeight (30-day half-life) × trustWeight (0.2 floor so
everyone counts, trusted users dominate), only the newest report per
reporter counts (no ballot stuffing), reports past 180 days are dropped,
and the parser rejects any unrecognised status value so garbage can't
poison the tally. Single + batch paths both invalidate on publish.

CONTRIBUTIONS — photos (kind 1063) and tag suggestions (kind 1111) are
SEPARATE events referencing the spot via #a, so contributors never touch
the owner's addressable spot event (no clobber). Tag votes dedupe per
contributor. The detail gallery spreads owner images first so the hero
is always the owner's photo when present; community shots are badged.

OWNERSHIP — transfer/claim model is intentional (claiming republishes
under the claimer's own key = their own coordinate; the original
survives, as the UI states). One real rough edge: handleClaim copied ALL
original tags then appended claim-from/claim-reason, so claiming a spot
that was itself a prior claim would STACK multiple provenance pairs on
the event. Fixed by filtering out any existing claim-from/claim-reason
before appending a single fresh pair pointing at the immediate prior
owner. No functional/authorization change.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 00:19:10 +07:00
shakespeare.diy
c6e74cc189 Wallet audit: warn about at-risk balance before removing a mint (1.2.39)
Audited the wallet surface (real money: Cashu proofs, Lightning, NWC).
Most of it is solid — the Cashu singleton init with browser-CORS mint
probing/auto-heal, the send/receive token flows, the Lightning receive
path (double-tap guard + redeemed-quote listener cleanup + actionable
CORS errors), and the melt-based send all check out and match the zap
path.

One real money-loss gap: removing a mint calls
mintRepository.deleteMint(), which drops that mint's proofs from the
device — so removing a mint you still hold sats in strands those funds.
There are two remove-mint dialogs and they disagreed:

  • MintPrivacyManager already warned with the concrete at-risk balance.
  • CashuWallet's dialog showed only a generic static "make sure you've
    withdrawn funds" line — no balance, no escalation — so a user could
    delete a mint holding sats with effectively no warning.

Brought CashuWallet up to (slightly past) parity: when the mint being
removed has a non-zero balance it now shows a loud red warning with the
exact sats at risk and relabels the button "Remove anyway"; empty mints
keep the mild yellow note. Deletion is still allowed (a permanently
offline mint must be removable even with a stuck balance) — it's now an
informed choice, not a silent one.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 00:15:07 +07:00