Sasquatch now knows the crew it lives on. The agent is seeded with a rich
crew dossier — its own upcoming events, recent feed digest, roster size,
founder, open call, vibe, band, home spots, website/IG — and the system
prompt instructs it to answer crew-specific questions ("what's our next
jam?", "who runs this?", "what are we looking for?") directly from memory,
while still using CVM tools for wider-network discovery.
- useCrewAgentChat: expanded CrewContext + crew dossier + two-mode prompt
- CrewDetailPage: wires real crew events + feed digest into the agent ctx
- CrewAgentPanel: crew-specific suggestions, crew-aware copy
- Replaced 🐝 bee emoji with the Sasquatch mascot avatar everywhere
(panel header/rows/empty state, crew page button, CrewForm toggle)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A notification center should surface the user's own actionable items, not a
generic nav shortcut — and it showed on every tab (including Social) where it
made even less sense. Removed the footer entirely; the tabs now sit flush.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
useConversations reported unreadCount: 0 for everything, so the inbox never
showed new messages. Add a lightweight, reliable unread signal:
- useDMRead: per-conversation, per-user last-read timestamps in localStorage
(local-only; compares against the inner rumor's real created_at, not the
fuzzed gift-wrap time, so it's accurate).
- DirectMessagesPage: a conversation is unread when its latest message is from
the other person and newer than last-read — rendered as a red dot, bolder
name, brighter preview text, and a subtle highlighted card.
- ConversationPage: opening a chat marks it read up to the newest message, so
the dot clears on return (each page mounts fresh, so no stale state).
Scope: kept to the Messages page. Deliberately did NOT add a global nav badge,
which would require background-decrypting all gift wraps on every page — the
same heavy cost avoided for the crew bell.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Per request, a display name is now mandatory when signing up:
- Removed the "Skip for now" button from the profile step — the only forward
path is Save, which is disabled until a non-empty name is entered.
- Marked the field required (*), added helper copy ("Required — how you'll show
up across SkateSpots. Change it anytime."), and a hint under the button.
- finishSignup guards against an empty/whitespace name (defense-in-depth for
keyboard/Enter), trims the name before publishing kind 0.
- Reworded the step copy: name required, bio + avatar still optional.
Only affects the primary nsec signup dialog (also the endpoint the mnemonic
flow feeds into); the mnemonic dialog has no name step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Instead of wiring encrypted crew chat into the global notification bell — which
would mean background-decrypting every crew's messages to power a badge (slow,
battery-hungry, unreliable with NIP-17's random timestamps) — add a subtle
unread dot to the crew's own Chat tab.
- useCrewChatUnread: member-only, single-crew, runs only while the crew page is
open. Uses a pure metadata query (newest Cordn envelope by created_at, no
decryption) vs. the local last-seen timestamp. For the built-in NIP-17
transport (random timestamps, needs decryption) it deliberately returns false
rather than fire false pings — the in-chat "New" divider already covers
"what did I miss" once the tab is opened.
- Tabs made controlled; the dot is suppressed while the Chat tab is active, so
it clears the moment you look, and is gone on the next visit (last-seen
advances). No global notification noise.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Close the metadata gap: the outer kind-4497 envelope was signed with the
sender's real key, so a coordinator/relay could see exactly who was talking.
- buildEnvelope now signs each envelope with a fresh ephemeral key (NIP-59
style) — the coordinator never learns the sender.
- Authorship moves inside the ciphertext: a new v2 sealed payload
(`nip44-group-v2`) carries a fully-signed inner author-proof event (kind 4498)
bound to the crew via an `a` tag. openGroupMessage verifies the signature,
author, content match, and group binding — so authorship stays unforgeable and
a proof can't be replayed into another crew.
- Transport trusts the verified v2 proof and surfaces the real sender; legacy
v1 envelopes remain readable (outer-key binding) for back-compat.
- NIP.md updated with the v2 format + an honest note on the residual
verbatim-replay caveat (full replay/forward-secrecy hardening lands with MLS).
Scope decision: kept to crew chat only. Regular 1:1/group DMs already use NIP-17
gift wrap (ephemeral keys, sealed per recipient) and have no coordinator, so
they need no change and stay idiot-proof.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Decision (per Marmot/Concord/Cordn analysis): a semi-public, socially-verified
skate crew fits Concord-style shared-key chat, not MLS. The built-in NIP-17
giftwrap group chat is already the right answer and works with zero setup — so
stop implying a coordinator is required and stop over-claiming MLS guarantees
the code doesn't yet provide.
- CrewCoordinatorCard: default state now reassures that private E2E chat is
already ON (no setup) for everyone, instead of prompting founders to "set up a
coordinator". Founders get a small, honestly-labeled advanced link to the
self-hosted Cordn option. Fixed the "E2E · MLS / forward secrecy" badge — the
coordinator path is NIP-44 group encryption routed over ContextVM, not an MLS
ratchet.
- CrewForm: the coordinator field is now clearly "experimental · self-host",
states you don't need it, and links to the Cordn repo.
- NIP.md: reworded the crew-chat section + tag/profile descriptions to match the
honest crypto seam (NIP-44 group today; MLS a future upgrade), so docs no
longer claim forward secrecy that isn't shipped.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A returning member couldn't tell new messages from old ones. Add the universal
unread-divider pattern (Slack/Discord-style) directly inside the chat:
- useCrewChatRead: per-crew, per-pubkey last-seen timestamp in localStorage
(local-only, zero network cost, works for both NIP-17 and Cordn transports).
- CrewChat snapshots last-seen once per open so the divider stays put while
reading, draws a minimal "New" line above the first message since last visit,
and quietly advances last-seen to the newest rendered message.
No new fetches, no config, nothing for the user to manage — it just works.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Turn the collective join-request into a genuinely seamless flow instead of a
dead-end deep-link:
- New useApproveCollectiveJoinRequest: owner appends the requester to their own
Lab-profile roster (re-signed, preserving all tags) and clears the request in
one action — mirrors the crew founder's "Add to crew".
- CollectiveJoinRequestCard now resolves the collective and offers a single
"Add to collective" tap, with graceful states for loading, already-a-member,
and unresolvable-profile (never traps the owner).
- Removed the old ?manage=members deep-link (pointed at a read-only card).
- Requester-side copy now sets accurate expectations ("added in one tap").
Trust model unchanged: only the owner signs their profile event.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Skaters can now request to join a Lab collective (studio/build team), mirroring
the crew join-request flow:
- New addressable kind 36387 (collective join-request), documented in NIP.md
- useCollectiveJoinRequests: send / incoming / "already requested" hooks
- Owner-only notification surfaced in the bell (All + Crews tabs) via
CollectiveJoinRequestCard, reusing the synced dismissal set
- "Request to join this collective" button on collective Lab profiles
(RequestToJoinCollective), hidden for the owner and existing members
Trust model preserved: only the owner can edit their own profile roster, so the
card links them to the member editor — no forged membership possible.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The crew<->collective links existed on both sides (crew.labRefs, profile.crewRef)
but were unverified one-sided claims. Now:
- CrossLinkCards resolve the other entity and show a green "Verified" badge only
when BOTH sides link each other (crew lists the Lab profile AND the profile
lists the crew). One-sided claims show a plain link — never false trust.
- LinkedCrewCard / LinkedLabCard take a selfCoord; CoachDetailPage and
CrewDetailPage pass their own coordinate so the check works.
- CrewLinkField + LabLinkField now explain the mutual-verified behavior, so the
feature is discoverable. Both fields already restrict to the user's OWN
entities, so a verified link genuinely requires one owner to confirm both sides.
- Completed LabLinkField TYPE_LABEL for all crafts (artist/graffiti/tattoo/
trainer/musician/judge no longer fall back to "Lab").
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New PaymentReadinessNote component: checks the logged-in user's Nostr profile
(kind 0) for a Lightning address / Silent Payment and either confirms "you can
get paid" or links them to Edit Profile to add one. Wired into CraftProfileForm
(coach, filmer, editor, photographer, builder, artist, graffiti, tattoo,
trainer, musician, judge) and the standalone coach/filmer/editor/photographer/
builder forms, alongside the existing CLINK (noffer) field. Lightning address
stays a single source of truth on kind 0 — never duplicated/stale per event.
- become-judge: removed the "Downhill / Longboard" event format (not part of the
skateboarding activities this app covers).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The chips (ANYTIME / TODAY / THIS WEEKEND / THIS WEEK / THIS MONTH) ran off the
right edge on mobile and got cut off. Now on phones they render as a 5-column
grid that fits the screen exactly — with compact labels (Any / Today / Wknd /
Week / Month) so nothing truncates. Desktop keeps the full-label flex row.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The mobile toolbar stacked 5 rows of controls (title, view tabs, upcoming/past
sub-tabs, search+city, date chips) before any card showed. Tightened for phones
without removing any feature or touching desktop:
- Toolbar vertical padding + section spacing reduced (py-2/space-y-2 on mobile)
- View tabs and Upcoming/Past sub-tabs use py-1.5/py-1 on mobile
- Date-window chips: smaller top margin + padding
- Results grid top margin reduced on mobile
- "N results" count line hidden on mobile (pure noise on a small screen)
All changes are md:-gated, so desktop is unchanged.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Audited the whole events flow end-to-end (create, hosting dashboard, discovery
grid, detail, RSVP, sponsors) and confirmed the wiring is coherent:
- Create → parse → display round-trips correctly for sponsors, tour/spot/crew/
musician links, entry fee, and featured listings
- Sponsor parser safely skips shop-coordinate `sponsor` tags (no name at pos 2),
so events and tour/shop sponsorships never collide
- Batch RSVP counts feed the grid; hosting dashboard + detail keep per-item counts
Fix surfaced by the audit: CreateEventPage let a host pick a start time in the
past, silently producing a dead event in the Past tab. Added a `min={today}` on
the date pickers and a submit guard (10-min grace) with a clear message.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The filter toolbar was already sticky (stays put while cards scroll under it).
This reclaims the wasted vertical space above it on phones: smaller title,
reduced top padding + margins, and the subtitle is hidden on mobile. Desktop
layout unchanged.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
On phones (max-width 767px) the gray scrollbar track is now hidden across the
app (html/body) — swipe-to-scroll still works, only the bar is gone. Desktop
keeps its custom 8px scrollbar unchanged.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
UX (all participants):
- EventCard now shows a live status pill (In 2 days / HAPPENING NOW / ENDED) on
both the full and compact variants — the at-a-glance signal was previously
detail-page only
- EventCard shows a "Presented by <sponsor>" ribbon so paid sponsor placements
get real visibility in the discovery feed (not just after a click)
- Added a bottom legibility gradient so overlays stay readable on any cover
FIX (crash risk): EventsPage getTrustScore did `trustScores?.map.get` — since
useBatchTrustScores returns a plain Map (no `.map`), this threw TypeError once
scores loaded. Now reads the Map directly.
PERF (N+1 query storm): rendering the grid fired one RSVP query PER card
(~100 queries on a full page). Added useBatchEventRsvps — a single #a-filtered
query for all event coordinates — and thread goingCount into EventCard as a prop.
Cards without the prop (detail page, hosting dashboard) keep their own query.
Removed now-dead getRelativeTime/relativeTime/isUpcoming (superseded by the
EventCountdown pill).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Admins (the two pubkeys in src/lib/admins.ts) can now moderate any event:
- Post authoritative Event Updates (incl. cancel/reschedule) on events they don't own,
clearly labelled "Admin" and marked with an ["admin","true"] tag
- Take down any event (already wired via DeleteEntityDialog + isAdminPubkey)
SECURITY FIX: kind-7103 Event Updates are now only trusted from the event's host
(pubkey in the `a` coordinate) or a SkateSpots admin. Previously ANY npub could
publish a fake "CANCELLED" update — spoofing the detail-page banner and spamming
RSVPers' notification bells. Enforced in both parseEventUpdate() and
useActivityNotifications.
Note: admins intentionally CANNOT open the edit form on others' events — an
addressable event (31923) can only be replaced by the key that signed it; editing
under a different key would fork it, not edit it. Moderation is done via
authoritative updates + takedown instead.
Docs: NIP.md updated with the host/admin trust rule and the admin marker tag.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- FIX: EventCountdown label never updated (memo ignored the interval tick) — now recomputes every 30s for both EventCountdown and useEventStatus
- Add loud cancelled / rescheduled banner on EventDetailPage derived from host updates; close RSVP + dim hero when cancelled so attendees don't show up to a dead jam
- Clean up non-standard NIP-52 `a` tags (['a', coord, '', ''] → ['a', coord]) in RSVP and host-update publishing
- Correct misleading kind comment in useEventRsvps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Per-event og:title/description/image/url + twitter:summary_large_image via useSeoMeta
- Share description composes date + location + blurb; falls back to site card image
- handleShare now uses the canonical /#/event/... URL, richer text, and toast feedback on clipboard copy
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Immersive EventDetailPage hero: full-bleed cover with legibility gradient,
bold uppercase title, type pills, host/music-act chips, and a 3-up meta bar
(date block · time · where). Past events render dimmed.
- EventCountdown: live status pill — "IN 3 DAYS" / "IN 2H 14M" /
"HAPPENING NOW" (pulsing) / "ENDED", refreshing every 30s.
- Actions (edit/calendar/share) moved into a clean glass overlay on the cover.
- Event sponsors: EventSponsor type + parse/serialize (mirrors TourSponsor),
EventSponsors "Presented by" strip (title sponsor hero row + logo grid),
sponsor editor on CreateEventPage, and sponsor preservation on edit.
- Document event sponsor tag in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- AttendeeList: tabbed Going/Interested with faces + names + a "who you trust"
signal (reuses batch trust scores); replaces the plain avatar stack
- Event Updates (kind 7103): usePostEventUpdate + useEventUpdates; host-only
composer with update/reschedule/relocation/cancelled categories that p-tags
every RSVPer so they get notified; public feed on the event page
- EditEventPage: auto-post a reschedule/relocation update to notify attendees
when the host changes time or location
- Activity bell: new 'event-update' notification type for RSVPers
- EventsPage: "Hosting" tab (logged-in) with a HostingDashboard listing the
user's upcoming/past events, live RSVP counts, and quick edit/update actions
- Document Event Update kind 7103 in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The desktop "More" dropdown was only populated from the customizable nav
catalog, so directory/account destinations that live only in the mobile
burger menu (notably Skaters) never appeared. Add Skaters, View Profile
(logged in), and Settings to the desktop More menu so nothing is lost on
larger screens.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New useActivityNotifications: single combined #p query classifies reactions,
reposts, zaps, mentions and replies referencing the user; parses zap sats
- New useNotificationsRead: per-account last-read timestamp for unread badge
- New ActivityNotificationCard: actor avatar + type badge, verb, snippet, links
to the actor's profile
- Rewrite NotificationBell into a tabbed activity center (All / Social / Crews)
with unread tracking, mark-all-read, and the existing crew invites/requests
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New src/lib/routing.ts: keyless OSRM client, travel modes, distance/skate-time formatters
- New RouteDialog: self-contained Leaflet map drawing the route from the user's
location to a spot/shop/event, with a mode toggle and distance/time summary
- Wire the "Directions" buttons on Spot/Shop/Event detail pages to route in-app
instead of opening Google/Apple Maps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Closes the admin/founder asymmetry without breaking the trust model: only the
founder authors the crew event, so only they add members — but an admin can now
publicly *vouch* for a join-request, and the founder ratifies with one click.
Protocol (SkateSpots-native kind 9087, regular event):
- e-tags the join-request, a-tags the crew, p-tags the requester (role
"requester") and the founder (so it hits the founder's bell).
- Clients keep only endorsements authored by a real admin of the endorsed crew
and dedupe one-per-admin-per-request (no credibility inflation).
- New parseCrewEndorsement + CrewEndorsement type; documented in NIP.md.
Hooks:
- useEndorseJoinRequest (admin publishes an endorsement, admin-gated).
- useJoinRequestEndorsements: batch-fetch endorsements for all visible requests
in ONE #e query, grouped per request, admin-verified per crew.
- useCrewNotifications attaches endorsements to each request entry.
UX (JoinRequestCard):
- Founder: primary becomes "Approve & add" when vouched, with a vouch strip
(stacked admin avatars + "Vouched by Rider +1 more") as social proof.
- Admin: the old dead-end "Only the founder can add members" becomes a real
"Vouch for them" action, then a "Vouched — waiting on the founder" state,
plus a one-line explainer so the path isn't a mystery.
- Endorsements enrich the existing request card (no duplicate notification).
Build clean, no console errors. Not deployed.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Fixes the caveat that dismissing a handled invite/join-request on one device
left it un-dismissed on another (it was per-device localStorage).
- New useCrewDismissals hook: stores the dismissed-id set as a single private,
addressable NIP-78 event (kind 30078, d=skatespots:crew-notif-dismissals)
with a NIP-44 payload encrypted to the user's own key. One list covers both
invites and join-requests.
- useDismissCrewNotification: optimistic local update + best-effort encrypted
publish, so dismissals feel instant and sync across every device the user
logs in from.
- Graceful degradation: always resolves the UNION of the synced list and a
local mirror, so it still works offline and on signers without NIP-44 (they
just don't sync) — no regression from the old behavior.
- Rewired useCrewInvites (accept/decline/approve/dismiss) to the synced set;
removed the two localStorage-only dismissal helpers. Filtering moved into
useCrewNotifications so both invite + request lists share one source of truth.
- Documented the kind-30078 dismissal event in NIP.md.
Build clean, no console errors. Not deployed.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Crew roles previously covered only filmer/photographer/editor/artist plus
manager/og. Bring in the rest of the Lab professions so a crew can label any
member by the craft they bring:
- Added: coach, builder, graffiti, tattoo, trainer, musician, judge —
mirroring the Lab profession kinds 35001–35012 (with a `labKind` link in
CREW_ROLE_META for future cross-linking to Lab profiles).
- Kept the crew-specific manager + og, and preserved existing slugs so no
stored crew data breaks.
- Reordered for display: media crafts → Lab crafts → crew-specific.
- Aligned emoji with the Lab (📸🎓🔨🖌️💉💪🎵⚖️).
All consumers (CrewForm role chips, CrewLeadership, CollectiveMembers,
parseCrewRoles) pick these up automatically via CREW_ROLE_ORDER / _META.
Updated the crew-role docs in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Wizard chrome:
- Replace the flat progress bar with a proper numbered stepper — icons per
step, a filled/checkmark state when a step has content, past-step checks,
and clickable dots (forward jumps still gated on a name).
- Direction-aware step transitions (slide from right going forward, left
going back) with fade-in.
- Sticky footer gets arrow icons, a shadow, a "ready to roll / add a name"
hint on the final step, and Create Crew disabled until the name is valid.
- Enter / form-submit before the last step now advances instead of publishing
a half-filled crew (edit mode unaffected).
Field-level feedback:
- Crew name: inline "Looks good" + check icon when valid, teal border, helper
line, 100-char cap.
- About: live character counter (turns amber near the 3000 limit).
Member input:
- Live preview chip (avatar + name + npub) that resolves the typed
npub/nprofile/hex BEFORE adding, so founders confirm the right skater.
- Add button disabled until a valid, not-yet-added key is present; amber hint
when the text isn't a key yet; "Added" state when already on the roster.
- Extracted a shared decodePubkeyInput helper (also now accepts nostr:nprofile).
Crew Agent:
- Honest beta framing (browser-run, free-tier AI, occasionally slow) and an
on-state confirmation line when enabled.
No publish/handleSubmit behavior changed; all edits are UX/IA only.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Audit finding: the crew form was one 1,100-line endless scroll with ~15
sections in a punishing order (logo dead-last, MLS chat coordinator sitting
inline with the crew name), only one validation gate, and no sense of
progress or what's optional.
Fix — split the flow by intent:
- Create mode: a guided 4-step wizard with a sticky progress bar, per-step
hints, Back/Continue + "Skip to end", and a sticky footer nav.
1. The Basics — logo, name, about, home turf (all you actually need)
2. Identity & Vibe — vibe tags, crew type
3. Squad & Spots — members/roles, home spots, open call
4. Extras — treasury, video, sponsor, links, band, NIP-05,
agent, and the advanced chat coordinator (reframed
in plain language, clearly skippable)
Only the crew name is gated, and only at step 1. Clickable step dots allow
jumping back freely.
- Edit mode: unchanged UX intent — renders every section flat on one page
(power users want it all at once).
All field state and the entire publish/handleSubmit logic are preserved
verbatim; sections were extracted into composable blocks with zero behavior
change. Softened the coordinator copy and the map-pin section so a first-timer
isn't scared off.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Part A — Sponsor picker:
- CrewSponsorPicker searches the shop directory and emits a 30017 coordinate
so founders select a sponsoring shop instead of pasting a raw coordinate.
- Wired into CrewForm, replacing the raw coordinate input.
Part B — Notifiable join-requests (kind 31969):
- New addressable join-request event p-tagged to founder + admins, making the
request→approve direction symmetric with invites (surfaced in the bell, not
buried in a DM inbox).
- useCrewInvites: useSendJoinRequest, useIncomingJoinRequests,
useApproveJoinRequest (founder adds member), useDismissJoinRequest (local).
- CrewRequestToJoin now publishes the structured event.
- JoinRequestCard (Add to crew / Dismiss) wired into NotificationBell and
counted in the unread badge.
- Documented kind 31969 in NIP.md alongside invites.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The crew coordinator is no longer just a pointer — when a crew configures one,
the Chat tab runs through it for real.
- src/lib/cordn: clean transport/crypto split so an MLS (ts-mls) upgrade is a
drop-in swap of the crypto layer only.
- types.ts: CordnMessage/CordnEndpoint + envelope kind 4497 + app tag.
- crypto.ts: sealGroupMessage/openGroupMessage — real NIP-44 group encryption
(per-recipient sealing), the exact seam MLS encrypt-to-epoch slots into.
- client.ts: buildEnvelope/publishEnvelope/fetchGroupMessages with per-group
cursor ordering and ciphertext→author sender binding (anti-spoof).
- endpoint.ts: resolve a crew's `coordinator` to a usable relay endpoint
(wss/ws direct; https falls back to the ContextVM relay).
- useCordnChat: fetch-then-poll hook mirroring the NIP-17 chat API.
- CrewChat: uses Cordn when the crew has a coordinator, NIP-17 fallback
otherwise; header badge shows the active transport (Cordn vs Private).
- CrewCoordinatorCard: reflects that private chat is live.
- NIP.md: document envelope kind 4497, delivery (fetch-then-poll + cursors),
the nip44-group sealed content, and the honest crypto→MLS seam.
Real encryption, real coordinator routing, non-breaking (fallback intact),
with the MLS ratchet as a clearly-marked next step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A skate team is more than "members" — the filmer, photographer and editor are
core functions. Crews can now assign optional, non-pretentious skate roles:
- Types: CrewRole (filmer/photographer/editor/artist/manager/og) + CREW_ROLE_META
labels/emoji, `crew-role` tag parser, and `crewRoles` on SkateCrew. Roles are
additive to membership and grant NO powers (only `admin` does); a member can
hold roles AND be an admin. Only honoured for founder/listed members.
- CollectiveMembers: optional rolesByPubkey → renders 🎥 Filmer / 📷 Photographer
flex chips next to each member.
- CrewLeadership: leader + captains show their skate-role chips.
- CrewForm: per-member role toggles + a founder ("Your roles") picker; publishes
`crew-role` tags scoped to the roster.
- CrewDetailPage: builds rolesByPubkey and threads it to members + leadership.
- NIP.md: document the `crew-role` tag + example.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A skate crew is defined by its footage. Crews can now feature a headline video:
- videoEmbed.ts: safe parser accepting ONLY YouTube and Instagram URLs (full
parts on YT, clips on IG). Extracts video id / shortcode; rejects everything
else so we never render an untrusted iframe. No native/self-hosted video.
- CrewVideoSection ("The Part"): cinematic section — YouTube click-to-load
facade (poster + play button, no heavy iframe/cookies until play) and an
Instagram permalink embed, framed with skate language.
- SkateCrew gains `videoUrl` (tag `video`), validated through the embed parser.
- CrewForm: "The Part" input with live source detection (YouTube/Instagram) and
validation; publishes the `video` tag.
- Crews grid: crews with footage flex a "▶ Part" badge as a discovery signal.
- NIP.md: document the `video` tag (YouTube/Instagram only) + example.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Extend SkateCrew with `sponsorRef` (sponsoring shop, NIP-15 stall coord) and
`coordinator` (Cordn/MLS encrypted-chat Delivery Service pointer), plus parser.
- CrewSponsorBanner: "Sponsored by [Shop]" banner that resolves and links the
backing shop; renders nothing if the shop can't be resolved.
- CrewCoordinatorCard: surfaces the crew's private-chat coordinator (E2E · MLS),
explains what the coordinator can/can't see, shows the operator, and prompts
the leader to set one up when absent — the UI foundation for Cordn chat.
- Wire the sponsor banner (under hero) and coordinator card (sidebar) into
CrewDetailPage.
- CrewForm: add "Sponsored by" and "Private Chat Coordinator" inputs with
validation; publish `sponsor` and `coordinator` tags.
- NIP.md: document the `sponsor` and `coordinator` tags, the structured invite
kind 36229 (with accept/decline flow), and the Cordn/MLS private-chat model
and why it's the chosen fit for crews.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New CrewHero: immersive full-bleed banner with per-crew accent glow, club-crest
logo overlapping the banner, member-count crest, big team name with neon shadow,
reputation crown + live activity pulse, focus/verification chips, "Led by"
team-leader inline, vibes and bio. Actions are passed in from the page.
- New CrewLeadership card: frames the founder as "Team Leader" and admins as
"Captains" with role crests, verified ticks and quick-DM — establishing the
crew hierarchy and laying groundwork for coordinator roles.
- Rewire CrewDetailPage to use CrewHero + CrewLeadership; wire live activity via
useCrewsActivity and confirmed-member/admin sets. Removed the old flat hero
card and now-unused imports.
- Upgrade CrewsPage header into a proper hero band with gradient, glow and
stat pills.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Upgrade CrewInviteDialog with a "Send invite" tab: invite a specific npub
with live invitee preview, optional note, and founder-only "invite as admin"
toggle. Preserves the shareable-link flow on a second tab.
- Add NotificationBell to desktop + mobile headers: unread badge, popover with
incoming crew invites (accept/decline) and pending-confirmation crews
(confirm with public/crew-only visibility).
- Wire useSendCrewInvite / useMyCrewInvites / accept / decline hooks and the
useCrewNotifications aggregator into the UI.
- Membership stays unforgeable (founder p-tag + invitee NIP-32 attestation).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Move sponsors from a buried sidebar card to a prominent full-width "Backed by"
bar right under the stats strip, above the About/content grid
- Redesigned as a horizontal pill row with an Award badge + gradient/glow accent,
so reppin' brands reads as a badge of pride
- Removed the duplicate sidebar Sponsors card
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Even 2x2 (mobile) / 1x4 (desktop) grid instead of a ragged flex-wrap where
"Gear" dropped to its own row and "Member since" floated awkwardly
- Each stat now has an icon badge + prominent count + label, so the number reads
as a number (previously the tiny colored count looked like a status dot)
- Zero-value stats render in a muted, inert state — a brand-new profile now reads
as "nothing yet" rather than a wall of dead grey zeros
- Moved "Zaps received" + "Member since" into a clean secondary meta line below,
keeping the primary grid focused on the four core contributions
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- StatPill now accepts an onClick and renders as a hover-lift button when it has
a non-zero value (inert div when empty), with clear affordance
- Spots / Shops / Crews / Gear pills smooth-scroll to their matching section
(added ids + scroll-mt-24 offset so headings aren't hidden under sticky nav)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Dialog now sizes to the viewport (w-[calc(100vw-2rem)] max-w-sm) with
overflow-hidden and min-w-0 children so the QR + URL never force lateral scroll
- Slightly smaller QR (48/52) to fit comfortably in the narrow dialog
- Share links/QRs now always anchor to https://skate-spots.org instead of
window.location.origin, which in the editor resolves to the sandbox iframe host
(the *.iframe.diy URL you saw)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Top-level `import QRCode from 'qrcode'` crashes on its canvas dependency at
module load, leaving the QR blank. Switch to the shared generateQRCode helper
which imports qrcode lazily (dynamic import), matching every other working QR
in the app. Also reset the QR while regenerating and log failures.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NIP-05 (the field you couldn't find — it existed but was a bare input):
- Promote to its own prominent, self-explaining row (out of the cramped grid)
- Live verification: checks the handle resolves back to your own key, with checking/verified/mismatch/unreachable states, an inline status icon, plain-language copy, and a "How?" help link
Share + QR:
- New ShareProfileDialog: QR of the profile URL (great for IRL/stickers), copy-link, and native share-sheet fallback
- "Share" button added to every profile header
At-a-glance stats:
- Compact stat strip near the top (Spots / Shops / Crews / Gear / Zaps + "Member since")
- Removed the now-redundant sidebar "Contributions" card
Cleaner empty states:
- Spots/Shops sections no longer show dead "nothing here" cards on other people's profiles — hidden entirely when empty
- Own empty Spots state is now an actionable nudge linking to /create-spot
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Fields collected on the Become-Skater form were being dropped on the profile:
- Instagram: now rendered in the profile Info card (normalizes @handle / full URL, links to instagram.com), matching where website/lightning live
- Location (city/area): now shown as "Based in" in the skate-layer stats row
- hasSkateLayer now also triggers on location/instagram so the skate card renders even when those are the only fields filled
All other fields (photo from kind-0, bio, styles, stance, level/custom level, years, home spot + mapped link, website) already round-trip consistently between the two pages.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Keeps the skater directory fully open — NIP-05 is a trust signal, not a gate:
- Extract reusable NIP-05 verification into lib/nip05.ts (parse, fetch well-known, verify against expected pubkey); refactor useCrewNip05 to use it
- New useSkaterVerifications hook: batch-fetches kind-0 metadata for all skaters and verifies each nip05 resolves to its own pubkey, returning a Set of verified pubkeys
- SkatersPage: add a "Verified (N)" toggle (disabled when none verified) that filters to genuinely verified skaters; show ✓ badge only for truly-verified skaters; verified-aware empty state
- NIP19Page profile: nip05 line now reflects real verification (lime ShieldCheck + "Verified" pill when the handle resolves to the key, muted grey otherwise)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Home spot:
- Add "Browse spots" button opening a searchable dialog of real mapped spots (useSkateSpots)
- Picking a spot stores its title + an `a` (30818:pubkey:d) coordinate; shows a linked-spot chip with unlink
- Profile page renders a linked home spot as a link to /spot/:pubkey/:identifier
- Parse homeSpotRef from the `a` tag; document in NIP.md
Profile pic consistency:
- Remove the skate-specific image override/upload — the skater card now always
uses the kind-0 profile picture so both sections show the identical photo
- "Profile Pic" section is read-only with a "Change profile photo" link to settings
- Drop the `image` tag from published skater events; note the change in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Become-Skater form: photo section relabeled "Profile Pic"
- Level picker now includes a "Custom" toggle with a free-text input (≤40 chars) for labels like "OG", "Weekend Warrior", "Retired"
- Store custom level as a `level_custom` tag (mutually exclusive with preset `level`)
- Render custom level everywhere presets show: profile (NIP19Page), SkatersPage cards, SkaterDirectoryCard
- Document `level_custom` tag in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Make the skater layer and the universal Nostr profile feel like one person,
with kind-0 as the single source of truth for shared fields.
1. Public profile (NIP19Page): render the skate layer inline — stance, level,
styles, years, home spot as a branded card right under the About. Own-profile
nudge ("Add your skate details") when no 35000 exists. Website in the sidebar
now falls back to the skater profile when kind-0 has none.
2. Become-Skater: prefill bio/website from kind-0 and default the photo to the
kind-0 avatar (with "using your profile photo" affordance). Store shared
fields ONLY as overrides — matching values are dropped so kind-0 stays
authoritative and nothing goes stale. "From your profile" hints + a
linked-to-your-profile banner make the relationship explicit.
3. Fallback-on-render for website/image/bio wherever the skater field is empty.
4. Bidirectional cross-links: new SkaterDirectoryCard in Settings > Profile
shows directory status (level + style chips) linking to edit, or nudges the
user to join; Become-Skater links back to full-profile editing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New app-native Skater Profile kind 35000 with `skateskater` marker tag,
the anti-spam boundary that keeps generic kind-0 Nostr profiles out of the
directory (queried only from the community relay).
- Skater type, stance/style/level constants, and parser in types.ts.
- useSkaters + useUserSkaterProfile hooks.
- /skaters directory page: immersive hero, live count, search, style filter
chips, ranked skater grid with NIP-05 verification badges and lazy author
metadata.
- /become-skater opt-in flow (bio, photo, styles, stance, level, years, home
spot, location w/ geohash, socials).
- Wire "Skaters" into the mobile nav and a cross-link banner in The Lab so
plain skaters finally have a home.
- Document kind 35000 in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Restructure EditProfileForm into clean, titled sections (Identity,
Bitcoin & Payments, Skate Profile, Account) with gradient icon
headers, consistent spacing, and a reusable FieldRow with inline
icon + validation status.
- Sponsors is now a dynamic per-line list (add/remove rows, Enter to
add) instead of a textarea; stored newline-separated. The profile
renders each sponsor as its own badge.
- Lightning address placeholder is now generic (you@yourdomain.com) —
no specific wallet names anywhere.
- Nicer image upload: click-the-thumbnail-to-upload with hover overlay,
round avatar preview / wide banner preview.
- Sticky Save bar at the bottom of the form.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Editable in EditProfileForm and displayed on the profile:
- Bitcoin Lightning Address (lud16) — now editable in-form, shown with a
labeled Lightning row in the Info card.
- Bitcoin Silent Payment Address (BIP-352, custom `bitcoin_silent_payment`)
— validated (sp1…), shown as a copyable, truncated row.
- Sponsors & Support (custom `sponsors`) — freeform highlight, rendered in
a dedicated pink "Sponsors & Support" sidebar card.
- Cordn contact link (custom `cordn`) — private MLS messaging link
(cordn.net), shown as a "Message privately" row in the Info card.
Crews and the CLINK noffer tip button were already present, so they are
left as-is. Added isValidSilentPayment / isValidLightningAddress
validators and documented the custom kind-0 fields in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use the established "Stash" term everywhere instead of introducing a new
word. The profile section is now titled "Stash", visibility copy reads
"Shown in your public Stash on your profile", and internal identifiers
were renamed (useUserQuiver → useUserPublicStash, QuiverItem →
PublicStashItem, parseQuiverItem → parsePublicStashItem). NIP.md updated
to match.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Previously the "Public" visibility on Stash items did nothing — the
whole Stash is a single NIP-44 encrypted blob, so nobody could ever see
a "public" item. This makes Public actually mean public.
Model (hybrid A + marketplace):
- Public items are now ALSO published as separate unencrypted NIP-99
kind 30402 events tagged `t: stash-showcase`, keyed by `stash-<id>`.
- Public + not-for-sale → showcase-only (`showcase: true`, price 0):
shows on the profile "Quiver", filtered OUT of the sellable market.
- Public + for sale/trade → full listing: shows on the profile Quiver
(badged For Sale/Trade) AND flows into the marketplace.
- Going private or deleting an item publishes a NIP-09 deletion for its
public event, keeping everything consistent.
Changes:
- New hook useStashShowcase (sync/unpublish + useUserQuiver query).
- StashPage wires add/edit/remove through sync handlers; honest
visibility copy in add + edit dialogs.
- Profile page (NIP19Page) renders a new "The Quiver" grid with
For Sale/Trade badges + prices, and a Gear count in Contributions.
- Marketplace hooks skip showcase-only events so they don't clutter
buy/sell listings.
- NIP.md documents the showcase event schema.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The avatar "Profile" tab in the mobile bottom bar linked to /settings,
so tapping your own face took you to Settings instead of your public
profile. It now routes to /:npub (View Profile) when logged in, and
still falls back to /settings when logged out (where login lives).
Long-press to customize the nav is preserved.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Let the title badges (Encrypted · Syncs everywhere · sats) wrap
gracefully instead of colliding with the toolbar controls
- Hide the list/grid view toggle on the empty state (nothing to toggle)
so it no longer clutters the space next to the Add button
- Give the title row room to grow on mobile (min-h-14 + padding)
instead of forcing everything into a fixed 56px row
- Make the search input flex-fill and shrink the category select on
small screens so the filter bar no longer overflows
- Add aria-labels to the view-toggle buttons
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Panel now anchors md:bottom-16 md:left-4 (was bottom-right) so it opens on
the left as before, and sits ABOVE the desktop footer so it never covers the
"Made with..." row or footer links.
- Tightened max-height to clear the top nav too (never slides under the menu).
- Bubble stays bottom-right on desktop for discoverability (clear of footer).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- findCoaches now covers EVERY Lab craft (coach, filmer, editor, photographer,
builder, artist, graffiti, tattoo, trainer, musician, judge) via role param;
reads skills from `t` tags and strips self-tags.
- New findShops (kind 30017, t=skateshop) and findTours (kind 30823) tools.
- getCommunityStats counts all crafts individually + lab_total + shops + tours,
and filters spots to t=skatespot (drops foreign kind-30818 events).
- Wired new tools + expanded role vocabulary into the Sasquatch brain: system
prompt, intent parser, keyword detector, and both dispatch/format paths.
- Privacy preserved: the public CVM data layer never queries the encrypted
Stash (kind 30001) — verified no 30001/decrypt references. Personal 'my
stash' stays login+Pro gated and decrypts locally with the user's own key.
- Normalized useCVMClient.ts to clean UTF-8 (comment glyphs only; no code change)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Move bubble & panel to bottom-RIGHT on desktop (was bottom-left, colliding
with / hiding behind the desktop footer). Mobile stays bottom-left.
- Raise bubble/panel z-index to z-60 so it always sits above the footer.
- Re-clamp a stale saved drag position on mount: a bubble dragged off-screen
on one device no longer disappears on another.
- Stop showing the long welcome greeting + action button on every open once a
conversation has started (chatMessages > 0), so it no longer "says the same
thing all over again".
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Flesh out SkateJudge as a full craft-based Lab profile (parseCraftBase),
with JUDGE_FORMAT / SPECIALTY / LEVEL label sets covering Game of S.K.A.T.E.,
best trick, jams, contests, am series, world tour stops, etc.
- Wire judge into useLabProfiles (union type, self-tag, parse case)
- Add "judging" ServiceType to LabPage: tab, accent, filter sections, empty
state, counts, profile card icon, and ?craft=judging deep link
- New BecomeJudgePage + /become-judge route + FAB entry
- CoachDetailPage renders judge profiles (accent, icon, labels, noun, title)
- Community Stats "Judges" card now links to /lab?craft=judging
- Document kind 35012 (skatejudge) in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useGrowthStats now queries and exposes every Lab craft individually: artists,
graffiti, tattoo, trainers, musicians and judges (previously folded into an
opaque "other" bucket with no visible breakdown).
- CommunityStatsPage renders a card for all 11 Lab crafts, so the section matches
what /lab actually lists instead of only showing 5 categories.
- labTotal + the content-mix donut's "Lab" slice now sum every craft, keeping the
"total" badge and chart consistent.
Note on relay mismatch: the app dedupes addressable events (keeps newest per
pubkey+kind+d) and merges across the community relay + the user's relays, so
counts can legitimately differ from a raw single-relay browser that shows stale
duplicate versions.
Bump version to 1.2.1 (versionCode 10201).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. Map filter: renamed the "Bitcoin" toggle to "Spedn" (matches the toolbar
button); tooltip now says "Spedn your sats".
2. Community stats (/stats): useGrowthStats now queries the same relay set as the
rest of the app (community relay + user relays) via useSkateRelays instead of
only the single hardcoded SKATE_RELAY. Lab profiles / spots / etc. that live on
a user's own relay are now counted, so the numbers match what /lab shows.
3. Near Me on GrapheneOS: even after granting the OS location permission, the
WebView's navigator.geolocation stays blocked → "permission denied". Fixed by
fetching the position from the native Capacitor Geolocation plugin first
(LocationManager, honours the granted permission), falling back to
navigator.geolocation only if that fails. Also auto-inject
ACCESS_FINE/COARSE_LOCATION into AndroidManifest via the manifest script.
Bump version to 1.2.0 (versionCode 10200).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause found in the plugin's native getPublicKey (NostrSignerPlugin.java):
when a packageName is set, it FIRST tries Amber's ContentProvider (background,
no-UI). On GrapheneOS that cross-app ContentResolver query is blocked and never
returns, so it never falls through to startActivityForResult(nostrsigner:) —
the branch that actually opens Amber's approval UI. Result: eternal
"Waiting for Amber".
Fix: getAmberPublicKeyViaPlugin no longer calls setPackageName and no longer
passes packageName; it passes only the get_public_key permission. With no
package name the plugin skips the content-resolver branch and launches the
intent directly, opening Amber's UI. Amber returns both pubkey and its package,
so later signing still works. Paired with the auto-injected <queries> manifest
block so the OS can resolve the intent in the first place.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two concrete root causes for the eternal "Waiting for Amber…":
1. setPackageName could hang forever on GrapheneOS. getPlugin() awaited it with
no timeout, so getPublicKey() (and its 90s timeout) was never even reached.
Now setPackageName races a 3s timeout and continues regardless — the Amber
package is passed explicitly on every call anyway, so it's non-essential.
2. AndroidManifest.xml had no <queries> block. On Android 11+/GrapheneOS the OS
silently blocks the nostrsigner intent without it, so Amber never opens. Added
scripts/inject-amber-queries.mjs (idempotent) and wired it into android:init /
android:sync / android:build so the block is guaranteed present in every APK,
surviving cap sync regeneration.
Bump version to 1.1.9.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: LoginDialog and useCurrentUser detected the native app via
window.Capacitor, which is unreliable inside the WebView (false at mount / not
attached in some builds). So the ZapStore APK fell into the web intent:// redirect
flow and never opened Amber ("Waiting for Amber…" hang).
Fix: add isNativeAmberAvailable() in capacitorAmberSigner.ts using the imported
@capacitor/core Capacitor + native plugin presence, and use it (evaluated at
click time) in LoginDialog.handleAmberLogin and in useCurrentUser's signer
selection. The APK now calls getAmberPublicKeyViaPlugin() → native intent → Amber
opens directly, no redirects.
Bump version to 1.1.7.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Login (Amber tab):
- Emphasize the proper NIP-55 redirect flow: one tap → approve in Amber →
redirected back to /amber-callback → poll auto-completes login. No copy/paste.
- Replace the indefinite spinner with a clear "Waiting for Amber…" state that
auto-signs-in on return, with an "Open Amber again" retry.
- Demote manual pubkey entry to an explicit "Having trouble?" escape hatch that
the user must tap — it never hijacks the flow anymore.
- Removed the aggressive 2.5s auto-switch to paste; keep background polling on
both waiting and paste states so a late callback still completes.
Signup:
- Welcome step now recommends "Sign up with Amber" first on Android (key lives
inside Amber, SkateSpots never touches it), with a clearly-secondary "quick
account on this device" option. Wired onUseAmber through LoginArea to open the
Amber login flow.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- CrewCard: rebuilt to fit any screen width — clip overflow, truncate name/location, single wrapping badge row, consistent avatar sizing; responsive header
- Amber web login: after opening Amber, drop user on manual paste step after 2.5s so the spinner never hangs when the callback opens in a new tab or clipboard is blocked; keep polling on the paste step so a late callback still auto-completes
- Stash: hasNip44/hasNip04 now fall back to the active login when pubkey matching fails, so logged-in nsec/amber users are no longer wrongly told "Encryption Required"
- Signup: added an explicit welcome step that explains account creation and only generates the keypair when the user taps "Create my account" (no more silent auto-creation on dialog open)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- signerCapabilities: recognize x-amber login type as NIP-44/NIP-04
capable (Amber implements nip44_encrypt/decrypt via native intents and
URL redirects). Fixes "Encryption Required" wall on Stash for users
logged in with Amber on Android.
- CrewDetailPage: restructure hero header so actions wrap below the
title on mobile instead of squeezing it in a cramped 3-column flex;
smaller avatar/title on small screens, truncating location.
- MapPage: remove Quick Snap from the mobile Add speed-dial and all its
handlers, refs, web-fallback input, and now-unused imports.
- zapstore.yaml: explicit orange icon + metadata_sources: [] so the
correct logo is always used on the Zapstore listing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The guide's action buttons used window.location.href = '/onboarding', a hard
full-page navigation. In the SPA/native (Capacitor) context this reboots the
app at its entry point (home) rather than resolving the client-side route, so
tapping "Learn More" dumped users back on the home page.
Switched all Sasquatch guide navigation (Learn More, Login to begin, Login to
Offer Services / Create a Crew, Start exploring) to React Router's navigate(),
which performs proper in-app client-side routing and reliably renders the
target page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Since signup now defers the key backup, users who skip it MUST be able to get
their recovery code later — otherwise we've just moved the trap. This adds a
"Your Recovery Code" card at the top of Settings > Security that:
- For nsec logins: reveals the recovery code on demand (hidden by default),
with Copy, Download (.txt), and QR options, plus a Hide button and clear
"never share this" warnings.
- For bunker / browser-extension / Amber logins: explains that SkateSpots
never held the key and points the user to back it up in their signer app.
Also removed the old placeholder QR handler that only showed an error, and
updated the "Backup Not Confirmed" copy to direct users to the new reveal
card instead of falsely claiming lost keys are unrecoverable.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- "Sign Up" now goes straight into a one-tap flow: a key is generated silently
and the user is logged in immediately, landing on optional profile setup.
- Removed the method-choice dialog (mnemonic vs nsec) and the generate/download
steps from the default path — no crypto vocabulary, no gates.
- Removed the forced key-download gate (Continue was disabled until download).
Backup is now optional and deferred (BackupReminder already nudges post-login;
the "done" step offers a gentle, jargon-free recovery-code reveal).
- Softened copy throughout: "secret key" -> "recovery code / account backup",
"your identity on Nostr" -> plain reassurance, CTA -> "Save & Start Skating".
- Advanced signer-app path preserved under a subtle link.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- NofferField: link → https://clinkme.dev/specs.html (instead of just /)
- EditProfileForm: link → https://clinkme.dev/specs.html (instead of just /)
- SasquatchGuide: added [specs] and [apps] inline links next to the clink description, pointing to the new pages
The app already referenced clinkme.dev homepage; these add the two new deeper pages for discoverability.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Completes the admin moderation coverage from the previous commit.
Lab profiles (coaches, filmers, editors, photographers, builders, artists,
graffiti, tattoo, trainers, musicians):
- useLabProfiles / useLabProfile and useCoaches / useCoach now filter out
admin-hidden entries (list) and 404 hidden ones (detail) via the shared
moderation layer. Added filterAdminHiddenMixed() for the mixed-kind Lab
union (kind read from each item's event).
- CoachDetailPage (the shared Lab detail page) now shows the delete/takedown
card to admins too, not just the owner, with admin-specific copy.
DeleteEntityDialog already handles the owner-or-admin logic.
Gear / marketplace listings (NIP-99, kind 30402):
- useMarketplaceItems / useMarketplaceAtSpot filter out admin-hidden listings.
- GearSwapItemCard: admins get a "Remove (admin)" action in the card menu with
a confirmation dialog that routes through useDeleteEntity (NIP-09 delete +
hidden label). Report stays for non-owners.
SECURITY.md: updated the admin-moderation coverage list and auditor file map.
Build + lint pass, no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The two SkateSpots admin pubkeys (already in src/lib/admins.ts) can now remove
any listing, not just their own. Because a NIP-09 deletion signed by a
non-author isn't honored by most relays, admin takedowns are made reliable with
a client-side moderation layer:
- useDeleteEntity: owners still publish a plain NIP-09 kind:5. Admins acting on
someone else's content publish the kind:5 AND a NIP-32 kind:1985 `hidden`
label referencing the target coordinate/id, authored by the admin.
- useAdminModeration (new): fetches admin-authored `hidden` labels app-wide and
exposes a fast lookup + filterAdminHidden() helper. Only labels whose author
is in the hardcoded ADMIN_PUBKEYS are honored (author = trust boundary,
re-verified client-side; never derived from a d-tag).
- Wired the hide filter into the list + detail hooks for spots, crews (incl.
by-focus), tours, events, and shops via React Query `select`, so removed
content disappears from lists and 404s on its detail page.
UI:
- DeleteEntityDialog now renders for owner OR admin, with distinct
"Remove (admin)" framing and copy (used on Crew, Coach, Tour, Event pages).
- SpotDetailPage & ShopDetailPage: routed their inline delete through
useDeleteEntity, show the delete button for admins, and relabel the confirm
dialog for admin takedowns.
- Added the delete control to Tour and Event detail pages (previously none).
SECURITY.md: documented the admin moderation trust boundary and extended the
auditor file map. Verified build + no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Address the two genuinely exploitable findings from the OpenCode audit:
C1 (critical) — sasquatchBrain.ts shipped a hardcoded OpenRouter API key in
the client bundle, extractable and abusable by anyone. Removed it; OpenRouter
is now opt-in via VITE_OPENROUTER_API_KEY and skipped when unset. Sasquatch
still works for everyone via the existing keyless Pollinations fallback.
(The leaked key must also be rotated/revoked on openrouter.ai.)
H1 (high) — SpotMap popup interpolated external weather-API data directly into
innerHTML. Added an escapeHtml() helper and applied it to every weather/AQI/
forecast value so a compromised weather API can't inject HTML/JS.
Triage of the rest: C3 (crew agent derived key) is a documented, deliberate
tradeoff — agent holds no funds and only speaks for itself. H2 (chart.tsx
dangerouslySetInnerHTML) is the stock shadcn chart with app-controlled config,
not user input. H3/H4 (GitHub Actions) don't apply — no .github/ in this repo.
C2 (NWC in localStorage) is standard for web Nostr clients. npm audit fix +
key rotation are operator steps done outside the app.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
An NWC connection string is a spend credential stored client-side. If a
connection is granted an unlimited budget, a compromise could drain the
wallet up to that limit. Add a prominent security notice in the "Connect
NWC Wallet" dialog instructing users to set a small daily budget and scope
the connection to pay_invoice only, and reminding them it can be revoked in
their wallet. This caps the blast radius of the credential the app must hold.
(The CSP referenced in the report was already added in the prior commit via
public/_headers; storage of nsec/NWC strings in localStorage is inherent to
client-side Nostr signing/payments and is mitigated by the XSS + CSP work.)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Critical: Leaflet marker popups built untrusted Nostr event fields (and
BTC Map API fields) into innerHTML, bypassing React's output encoding. Any
user could publish a spot/tour/crew/event with an <img onerror> payload in a
text field; it executed on /map render with no interaction, letting injected
script read the plaintext nsec from localStorage (nostr:login) and the Cashu
seed (cashu-mnemonic) — permanent, unrotatable account + wallet takeover.
Fixes in src/components/SpotMap.tsx:
- Rebuild all popup DOM with createElement + textContent instead of innerHTML
for every untrusted field: spot.location; tour.title/location/meetingPoint;
evt.title/location; crew.name/location; BTC Map name/address/opening_hours/
phone/description.
- Validate every event/API URL with sanitizeUrl() before it reaches an
href/src: crew.image (popup + marker icon) and BTC Map website. Drop
anything that isn't http(s). tel: links reduced to safe characters.
- Replace inline onerror handler on the crew marker image (would be blocked
by CSP anyway) with a scripted img.onerror fallback.
- Add popupRow() helper so untrusted detail rows can never be innerHTML'd.
Defense in depth (web only, native Capacitor build unaffected):
- Add public/_headers with a strict Content-Security-Policy: default-src
'none'; script-src 'self' 'wasm-unsafe-eval' (no unsafe-inline/eval);
object-src 'none'; base-uri 'none'; frame-ancestors 'none'; plus the
https:/wss: allowances a Nostr client genuinely needs for relays, media,
and mints. Also X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy.
- Move the inline boot-crash-guard script to public/boot-guard.js so
script-src can stay 'self' with no 'unsafe-inline'; index.html now loads it
externally. The guard also now paints errors via textContent, not innerHTML.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
useCrew previously relied on a single combined filter (kind + #d + authors,
limit 1). Some relays don't reliably match combined author + d-tag filters,
causing the crew detail page to render NotFound even when the crew exists.
Now, if the precise addressable lookup returns nothing and the author pubkey
is known, we fall back to fetching that author's crews (kind + authors) and
matching the identifier client-side. This survives relays with quirky tag
indexing while still preferring the exact match when available.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Latitude/longitude fields are now optional for local/national tours
- If provided, both must be filled and valid; otherwise geohash and
coords tags are omitted from the published event
- Updated input placeholders and added helper text clarifying coordinates
are optional
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
CreateTourPage called usePlatformFee('featuredTour') but never imported the
hook, causing a "usePlatformFee is not defined" ReferenceError that crashed
the /create-tour page. Added the import to match the other pages that use it.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The "Tip the Mapper" card now links the avatar/name to the mapper's profile
(/{npub} via the NIP-19 router) and adds a "Message Mapper" button that opens
an encrypted DM (/messages/{npub}). Makes it easy to contact whoever added a
spot — e.g. to ask them to add photos to a photoless submission. The message
button is hidden when you're viewing your own spot.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Add issue #5 (2026-08-08) to NEWSLETTER_ISSUES with a Groucho-Marx satiric
tone. Highlights: the 3x-bigger Lab (10 crafts), the Lab mobile grid fix,
Crews covering every scene with opt-in visibility + web-of-trust vouching,
the redesigned CrewDetail page, and Music<->Events interconnect. Mocks
"OpenCrack" for volunteering to fix Bitcoin's crypto libraries, and owns
that the app has never been audited (one day it will be — and we'll shoot
ourselves in both feet). Includes matching nostrText teaser for kind 1.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
On mobile the 10 craft filters (Coaches, Filmers, Photo, Editors, Builders,
Artists, Graffiti, Tattoo, Trainers, Music) were in a sideways-scrolling
strip, hiding options off-screen. Switch to a 5-column grid on phones (two
tidy rows of five) while keeping the flex-wrap layout on desktop, so every
craft is visible at a glance with no horizontal scrolling.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>