Commit graph

550 commits

Author SHA1 Message Date
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
shakespeare.diy
56e2b17990 Lab/Marketplace/Stash audit: useLabProfile picks newest revision (1.2.38)
Audited all three remaining surfaces. Two of them needed no changes:

MARKETPLACE / GEARSWAP — sound. useMarkListingSold preserves every tag
and flips only `status` (author-gated, right comment explaining why it
doesn't rebuild). Queries validate + dedupe, exclude showcase-only Stash
items from the sellable market, and GearSwap filters out expired/sold
listings and separates WTB from WTS. No bugs.

STASH — sound, and notably careful. The private list is one NIP-44
encrypted-to-self blob; all three mutations (add/update/remove) use
fetchLatestStash() (fetch-before-write) which falls back to non-empty
cache and THROWS rather than risk clobbering the user's gear, and is
Amber-gated (staleTime: Infinity). The public/private showcase
reconciliation in StashPage is complete: going public publishes the
kind-30402 showcase event, and going private / deleting / converting to
a wishlist item all call unpublishItem — so a public event can't linger
after an item is made private (no privacy leak). No bugs.

THE LAB — the collective join-request clobber was already fixed in
1.2.37. One robustness gap remained: useLabProfile fetched the profile
with limit:1 and trusted whatever came back. Relays can hold multiple
revisions of an addressable event, so a stale revision could win —
showing an old bio or, worse, a roster missing recently-approved
members. Now fetches a few (limit:10) and reduces to the newest
created_at, matching the pickMostRecent discipline already in useCrew.
On detail pages an author filter is always set, so newest-of-one-author
is exactly right.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 00:09:00 +07:00
shakespeare.diy
b6b3931ecc Crew audit: fix member-clobber on rapid roster writes (1.2.37)
Audited the whole crew surface. The trust model is genuinely solid:
membership is double-sided and unforgeable (founder p-tags + the
member's own NIP-32 kind-1985 attestation, cross-referenced in
useCollectiveMembers), roles are founder-controlled via the crew's
addressable event (useCrewRole), moderation only honours labels authored
by listed moderators, and the invite/join/endorse flow is symmetric with
proper author validation and batched, deduped queries throughout.

One real bug found — a stale-snapshot member clobber. The crew roster is
a single addressable event rewritten wholesale on every member add, but
three write sites rebuilt it from the CACHED crew/profile snapshot
without re-fetching first:
  • useApproveJoinRequest (founder "Add to crew")
  • useSendCrewInvite (founder pre-list on invite)
  • useApproveCollectiveJoinRequest (Lab collective owner add)

So approving two join-requests back to back rebuilt the second write from
the pre-first-write roster and silently DROPPED the member added by the
first (addressable events replace by coordinate). This is exactly the
fetch-before-write discipline already applied to spots/events/shops/
contacts/stash — the crew/collective roster writes were the ones missed.

Fix: added fetchLatestCrewEvent() (and an inline equivalent for Lab
profiles) that re-queries the freshest roster event by coordinate and
merges the new member into THAT, never regressing to an older revision,
falling back to the passed-in snapshot only on a relay hiccup so an add
still goes through. Founder-only / owner-only authorization and the
membership-attestation trust model are unchanged.

Member removal / admin promotion go through the founder's own profile
editor (a deliberate full-roster overwrite by the sole owner), so they
aren't concurrent-merge scenarios and are left as-is.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-08 00:02:30 +07:00
shakespeare.diy
85cb76b458 Map audit: fix dead trusted-first sort + broken stash-card borders (1.2.36)
Two real bugs surfaced auditing the Map page.

1. The "trusted-first" soft sort was silently dead for the default
   config. filteredSpots ran a standalone .sort() to float trusted
   content up, then IMMEDIATELY ran a second unconditional .sort() by
   sortBy — which re-ordered everything and wiped the trust ordering.
   For a logged-in user with ≥5 follows the smart default is
   trustFilter='trusted' + sortBy='recent', so the documented
   "sort trusted first, badge unverified" behaviour never actually
   happened. Fixed by folding the trust tier into the MAIN comparator as
   its primary key, with the chosen sortBy as the tiebreaker within each
   tier. Strict modes (verified/following) still hard-filter as before.

2. MiniStashCard (gear pins in the map drawer) built its border colour
   with an interpolated Tailwind class (`border-${colour}-500/40`) that
   the JIT compiler can't see, so it was never generated — dead class
   emitting an invalid string into the DOM. The inline
   style={{ borderColor }} that actually worked was also applied
   unconditionally, overriding the intended cyan border on WISHLIST
   items with the rarity colour. Now: wishlist keeps its static cyan
   border; gear uses the rarity colour via inline style only.

Geolocation (native vs web, denied/timeout/no-provider handling),
batched trust/status/comment/weather queries, and the layer plumbing
into SpotMap were all audited and are sound.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 23:03:34 +07:00
shakespeare.diy
719dd2c4a8 Route event notifications to the event, not the actor (1.2.35)
Audited the RSVP / event-update notification flow end to end. The data
layer was already solid and secure — RSVPs p-tag the host, updates
p-tag every RSVPer, and useActivityNotifications guards event-update
spoofing by requiring the author to be the event's host (from the `a`
coordinate) or an admin. The gap was on the rendering side.

Fixes:

- ActivityNotificationCard hardwired every notification to link to the
  ACTOR's profile. For an RSVP ("Alice is going to your event") the host
  wants the event + attendee list, and for an event update ("moved to
  Sunday") the attendee wants to READ the update — not visit a profile.
  Both events carry the event's `a` coordinate (31923:pubkey:identifier),
  so these two types now route to /event/:pubkey/:identifier. Everything
  else still lands on the actor's profile. d-tags containing ':' are
  reconstructed correctly.

- useRsvp only invalidated the single-event RSVP query, so the grid /
  calendar "going" counts lagged behind by up to the 30s staleTime after
  RSVPing. Now also invalidates the batch-event-rsvps query so counts
  update promptly.

- Social-tab empty-state copy omitted RSVPs and host updates even though
  they land there; made it honest.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:56:20 +07:00
shakespeare.diy
f253fc55fa Add month-calendar view for events & tours (1.2.34)
Events and tours could only be browsed as a card list — there was no
way to see what's happening on a given date, which is exactly the
clarity the calendar ask called for.

New EventsCalendar component: a purpose-built month grid (not the
date-picker) with:
- Prev/next/today month navigation
- Per-day item dots (max 3 + overflow count) with correct multi-day
  overlap handling for ranged events and competition tours
- A selected-day agenda panel showing title, time (or date range /
  "All day" for tours), location, and RSVP going counts, each linking
  straight to the detail page
- Month total footer, empty-day state, full responsiveness, and
  aria-labels / aria-pressed for accessibility

Wired a List⇄Calendar layout toggle into EventsPage for both the
events and tours tabs. Calendar mode reuses the already-filtered
event/tour sets (search, trust, type filters all still apply), hides
the list-only date-window chips, and shows a matching skeleton while
loading. Tours without a concrete date range are omitted from the
calendar (they have no fixed date) and remain browsable in list view.

Going counts are threaded through from the existing batch RSVP query,
so the calendar adds no extra relay load.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:52:52 +07:00
shakespeare.diy
9d069cb2f0 Add tour editing — the gap flagged in the addressable-events audit (1.2.33)
Tours (kind 30823) were the only major addressable entity with no edit path: an
organizer had to delete and recreate (losing the URL/identifier) just to fix a
typo or price. Rather than duplicate the enormous multi-category tour form (and
risk schema drift / the exact tag-clobber bugs fixed earlier this session), I
made the existing CreateTourPage do double duty:

- CreateTourPage now accepts an optional `existing?: SkateTour`. When present it
  prefills every field (all four categories: local/national/international/
  destruction — including challenges, prize distribution, film-release, partner
  shop, cities/countries, coords, dates), reuses the same `d` identifier, and
  adapts copy (title, submit button, toast).
- On save it PRESERVES unmanaged tags (sponsors, tags from other clients) and
  keeps any existing paid `featured` window — same "preserve unmanaged tags"
  discipline as the event/shop edit fixes, since 30823 is addressable and
  re-publishing replaces the whole tag set.
- Guarded the category→duration reset effect so it no longer clobbers the
  prefilled duration on the first render in edit mode.
- The paid FeaturedUpgrade upsell is hidden when editing (existing status is
  carried through automatically).

New EditTourPage wrapper loads the tour, gates to the owner (login / access-
denied states), and renders <CreateTourPage existing={tour} />. Added the
/edit-tour/:pubkey/:identifier route and an owner-only Edit button on
TourDetailPage beside the existing Delete control.

This closes the one concrete gap the addressable-events audit surfaced: every
entity now has both a working edit path (preserving unmanaged tags) and owner
management.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:42:34 +07:00
shakespeare.diy
a9f9931430 Tours: drop debug log; close addressable-events audit (1.2.32)
Removed the leftover console.log of the full tour event payload in
CreateTourPage — the last of the "Publishing … event" debug logs (spot, shop,
tour all now clean).

Audit findings for Tours (no code change beyond the log):
- Tours (kind 30823) are create-only with a timestamped identifier, so there is
  NO edit path and therefore no edit-clobber risk to fix. (There's also no
  EditTourPage / /edit-tour route at all — a genuine UX gap where an organizer
  can't fix a typo/price/meeting-point without deleting and recreating, but
  building an editor for the large multi-category tour form is a feature, not an
  audit fix, and rushing it would risk the exact tag-clobber bugs just fixed
  elsewhere. Leaving it flagged rather than half-doing it.)
- Owner/admin management is already correct: TourDetailPage renders
  DeleteEntityDialog, which internally gates to owner-or-admin.

This wraps the addressable-events pass (spots, events, shops, tours, Lab
profiles, crews, marketplace): every EDIT path either preserves unmanaged tags
or was confirmed safe, and every entity has a working owner management path
except tour-editing, which is now explicitly flagged as future work.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:34:03 +07:00
shakespeare.diy
7554a39379 Shops: preserve unmanaged tags on edit + drop debug log (1.2.31)
EditShopPage rebuilt a shop's tag set from scratch on save. A shop (NIP-15
stall, kind 30017) is addressable, so re-publishing REPLACES all tags — any tag
the form doesn't manage would be dropped. The app's own SkateShop parser only
reads the managed fields today (so no current in-app data loss), but a stall
created/edited in another Nostr client can legitimately carry currency,
shipping, or other NIP-15 tags that would vanish on the first SkateSpots edit.
Applied the same "preserve unmanaged tags" passthrough used for event edits:
keep every non-managed tag untouched, with `t` handled specially (form owns the
'skateshop' marker, foreign topics preserved).

Also removed a leftover console.log of the full shop event payload in
CreateShopPage (matching the earlier spot-form cleanup).

Audit note (no change needed): ShopDetailPage already provides full owner
management — an Edit link and an owner/admin Delete with confirmation via
useDeleteEntity — so unlike the marketplace listings fixed in 1.2.30, shops
were never missing a management path.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:29:51 +07:00
shakespeare.diy
080785a8c5 Marketplace: give sellers real listing management (mark sold / relist / delete) (1.2.30)
Gap found: the app's data model, sales tracking (useSellerSales), and Stash
showcase all key off a listing's status='sold', and both listing cards render a
"sold" state — but there was NO way for a seller to ever set it. Owners had zero
management actions: the MarketplaceItemCard dropdown only rendered for
non-owners (Report), and GearSwapItemCard's owner branch literally said "owners
manage their listing elsewhere" — a place that didn't exist. So sold gear stayed
"active" forever (until 30-day expiry) and the whole sold/sales-count feature was
unreachable.

Added owner-facing management on BOTH listing cards (GearSwapItemCard drives the
main GearSwap grid; MarketplaceItemCard is used on spot pages):
  - Mark as sold / Relist as active
  - Delete listing (NIP-09, via the existing useDeleteEntity)

New useMarkListingSold hook flips status in place, PRESERVING every other tag
(title, price, images, size, spot ref, geohash, expiration, category t-tags) —
kind 30402 is addressable, so rebuilding from scratch would wipe the photos and
price. Same "preserve unmanaged tags" discipline as the recent event-edit fix.

Also:
  - GearSwapItemCard now shows a clear diagonal "SOLD" overlay when sold, so the
    status is actually visible to buyers and the owner (previously invisible).
  - Generalized its admin-remove flow to handle owner deletes too, with wording
    that adapts (owner "Delete" vs admin "Remove").
  - DeleteEntityDialog gained an optional controlled open/onOpenChange so it can
    be driven from inside a dropdown menu without unmounting on menu close.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:24:47 +07:00