A one-shot 'Get started' nudge on the map for freshly signed-up users: Add a spot / crew / wallet. The crew CTA adapts to whether any crew exists yet — 'Join a crew' (/crews) when there are some, 'Start your crew' (/create-crew) when there are none. Dismissal persists; logged-out visitors see the welcome banner instead.
The stretched Link was z-0 behind the card content, and backdrop-blur made the card its own paint layer above it, so no click ever reached the link (the card looked dead in the map list view). Link -> z-10, drop backdrop-blur, move hover to the wrapper (group + group-hover:), and raise the action row to z-20 so its buttons stay above the link. Verified headlessly: card click navigates, kebab opens its menu without navigating.
FORECAST_DAYS=6 in lib/weather.ts is the single source for the fetch length and the UI labels (spot card + map popup). New count-interpolated labels spotUi.forecastDays / mapUi.forecastDays (en/es; other locales fall back to English). Legend colours moved to lib/skateabilityLevels.ts to keep the react-refresh lint at zero.
- WeatherLegend overlay on the map (only while the weather layer is on), colours mirroring calculateSkateability
- Spot weather card: freshness line + manual refresh, and an error/retry state when Open-Meteo is unreachable
- Sunrise/sunset row (weather.ts now fetches Open-Meteo daily sunrise,sunset)
- Score reasons render as chips, not a bullet-joined run-on
- Map popup muted text gray-500 -> gray-300 for contrast
Units stay Celsius/km/h (deliberate).
lib/relays gains withCommunityRelay/ensureCommunityRelay; AppProvider and NostrSync add wss://relay.skate-spots.org to a stored list that lacks it (unless useSkateRelay is off). NostrSync republishes kind-10002 once for non-Amber signers so other clients pick up the community relay; Amber users keep it local to avoid an unprompted popup.
Adds RETIRED_RELAY_HOSTS (goskatespots.nostr1.com, wotr.relatr.xyz) with isRetiredRelay/pruneRetiredRelays. Applied in resolveRelays (runtime), AppProvider (one-time cleanup on load) and NostrSync (when applying a fetched NIP-65), so existing users stop showing/using the retired relay without manual edits.
Newlay's Blossom enabled on the relay box (blobs inside the data volume); Caddy + DNS + TLS wired. The app now uploads there first, keeping blossom.primal.net and blossom.band as mirrors, and includes it in BLOSSOM_MIRROR_HOSTS for read fallback.
Verified the legacy relay held the same 115 events as the new one (0 diff) before removing it, so no data is lost. Drops wss://goskatespots.nostr1.com from the default pools, the DM/CVM/zap relay arrays, and the cvm-server + push-service defaults.
Point SKATE_RELAY, the default relay pools, the DM/CVM/zap relay arrays, the cvm-server + push-service defaults, and the archive default at wss://relay.skate-spots.org, keeping the legacy hosted relay as a fallback during the client-update window. Restored 115/115 archived events; the relay runs behind Caddy with TLS. Docs + AGENTS.md updated.
New data-driven Lab craft, mirroring the artist role: three skill
sections (What You Make [required] / How You Make It / Materials) plus
minimum order, turnaround, size range, samples and customisation extras,
inheriting the shared Lab extras (rate, payment, gallery, reel,
languages, availability, booking).
Wired through types.ts (kind, label/description maps, parse, collective-
capable), labelKeys, useLabProfiles (kind list + dispatch), becomeRoles
(role card), craftFields, LabPage (tab, filters, offer button, empty
state, counts, deep links), LabCrewsStrip, NIP.md, and en/es i18n
(labOptions, becomeRolesUi, labUi). /become/clothing added to the smoke
route list. tsc, eslint, check-i18n, vitest 101/101, build and smoke all
pass.
Seller UX: the offer thread shows a Close deal button on an accepted,
unsold listing. It opens MarkSoldDialog prefilled with the buyer, the
agreed amount, and the settlement method (lightning when a payment pointer
was attached, else cash), so the seller never pastes an npub by hand.
MarkSoldDialog gains defaultBuyer/defaultAmountSats/defaultMethod and
re-seeds on open. New marketUi.offers.closeDeal (en/es).
Trade-record state (revoked/verifying) and the listing pending status were
printed raw. New marketUi.trades.stateRevoked/stateVerifying and
marketUi.statusPending (en/es). Also reclassify the request tag as cosmetic
(never read in the client) in the audit.
Correctness batch from MARKETPLACE_FLOW_AUDIT.md:
- canBuy is disabled when the seller requires escrow (item.escrow ===
'required'), so a direct Buy-with-sats cannot bypass it.
- ask-seller (31872) is available on an active listing, not only once sold,
so a buyer who paid off-app is no longer at a dead end.
- payment receipts (lib/receipts.ts) are scoped to the payer's pubkey, so a
stale or foreign receipt cannot be attached to a trade by another account
viewing the same listing.
Finishes the escrow-mint work: advertiseEscrowKey now also publishes
cashu_mint (the seller's preferred mint from Wallet > Escrow) on kind 0,
documented in NIP.md. The buyer's EscrowRequestDialog reads the seller's
cashu_mint, includes it in the mint picker even if it is not in the buyer's
trust list, and prefers it (falling back to the buyer's preference, then
first trusted mint). Funds locked at a mint the seller actually uses are
the ones they can redeem. Still open: seller-side receive from a mint they
do not already have. New marketUi.escrowRequest.sellerMintHint (en/es).
- usePayPaymentPointer now checks the seller's committed payment_hash (from
the accepted offer) against the invoice actually paid; a swapped pointer
is rejected before any payment.
- PayWithSats no longer offers an in-app sats Pay button for a non-SATS
offer: a fiat amount is never paid as if it were sats. New
marketUi.pay.nonSats (en/es).
- ListingOffersPanel passes paymentHash + currency through.
Docs updated (still open: NWC->WebLN/Cashu fallback, empty preimage).
The escrow mint was silently the buyer's preferred/first trusted mint
(useCashuEscrow: prefs.preferredMintUrl || mints[0].url) with no UI, even
though the seller redeems there. Now:
- the escrow dialog shows the mint and lets the buyer pick from their
trusted mints (defaults to their preference);
- a mint whose NUT-06 info advertises no NUT-11 (P2PK) support is blocked
with a clear message before minting (cashuEscrow.mintSupportsP2PK);
- the create call uses the selected mint.
Still open (scoped separately): consult a seller-advertised mint and
seller-side receive from an unfamiliar mint. Recorded in the audit.
Next batch from MARKETPLACE_FLOW_AUDIT.md:
- bolt11.ts: getBolt11Expiry / isBolt11Expired; usePayPaymentPointer refuses
an expired invoice before paying (a lapsed offer invoice used to fail deep
in the wallet with a confusing error).
- PayWithSats shows unsupported pointer kinds (creq/clink) as unavailable up
front instead of enabling a button that fails after the click.
- useTradeAttestations: verify events (onlyVerified) and keep the newest per
(author, kind, d), so a newer s=revoked supersedes a stale valid copy;
useTradeReputation no longer trusts unverified relay events.
- EscrowRequestDialog persists the minted token locally before publishing
(lib/pendingEscrow.ts), so a failed send cannot strand the locked funds.
Deep audit (MARKETPLACE_FLOW_AUDIT.md): full trade/escrow/offers lifecycle,
severity-ranked findings, and a state-coverage matrix.
Fixes:
- Grid Mark as sold now opens MarkSoldDialog, so the kind-31871 attestation
(buyer, method) is recorded from the grid too, not just the detail page.
- Offers accept attaches a payment pointer only when the accepting user is the
listing owner (seller); previously the buyer could publish their own lud16 as
the payee and misdirect payment.
- advertiseEscrowKey refuses to publish kind 0 before the profile loads, so it
cannot overwrite name/picture/lud16 with a cashu_escrow-only profile.
- useListingOffers passes requireAny, so the offers panel error state actually
fires on an outage (previously an outage looked like no offers).
- Offer messages invalidate the offer queries, so accept/counter/decline update
immediately instead of after the stale window.
Next P1 slice from UX_AUDIT_2026-10-09.md:
- Marketplace hub tabs now live in the URL (?tab=...) so refresh, share and
back/forward keep the tab; unknown values fall back to Browse.
- ListingOffersPanel shows an error state on a relay outage instead of
returning null (a failure no longer reads as no offers).
- Nav customizer Reset reseeds from DEFAULT_TAB_IDS (4) instead of a
hardcoded 3, matching MIDDLE_SLOTS.
New marketUi.offers.loadFailed (en/es).
P0 fixes from UX_AUDIT_2026-10-09.md:
- useMarketplaceItem now filters admin-hidden listings and skips
showcase-only (not-for-sale) items, so a direct /listing/... URL cannot
surface removed or non-sale content.
- Listing detail hides Make an offer for logged-out users and for
fixed-price listings (Buy is the single primary CTA there).
Borrowed from the Plebeian market review: a /marketplace-guidelines page
(peer-to-peer, buyer-beware, no checkout, prohibited items, soft
moderation), linked from the marketplace hub; new marketTerms en/es keys.
Records the remaining borrowables in the audit doc.
A lone friend-of-a-friend no longer reads as trust. useViewerGraph now
counts distinct follows per friend-of-a-friend (fofCounts), and
deriveLocalScore returns verified only at FOF_MIN_RATERS (2) or more.
Provider assertions are unaffected; the full GrapeRank confidence model
stays provider-side. Docs updated.
Fifth slice of TRUST_SCORE.md. useNetworkAlerts reads the viewer's NIP-85
assertions for the accounts they follow and surfaces those trusted people
reported or muted. New /alerts page lists them with Ignore / Unfollow /
Mute / Report (reusing useFollowActions, useMuteUser, ReportDialog) —
inform, never auto-act. Settings -> Trust links to it with a live count;
ignored accounts are device-local. /alerts added to the smoke list.
Completes the safety loop. useMuteList reads the viewer's kind-10000 mute
list plus optimistic local mutes (lib/localMutes.ts); useMuteUser
republishes the list with the target added, records it locally and
invalidates content queries. useAdminModeration carries a new
mutedPubkeys set, and the shared filterAdminHidden/filterAdminHiddenMixed
drop muted authors — so every list hook filters them with no per-hook
changes (admin tooling with includeHidden still sees them). The flagged
notice gains a Mute action.
MarketplaceItemCard ran useTrustScore(item.pubkey) AND rendered
TrustBadge for the same author. It only needed the >=60 bit, which the
shared viewer graph (via useTrustBucket.localScore) already provides, so
the extra heavy per-card query is gone. Behaviour preserved.
TrustBadge called useTrustScore per card, which fetched the viewer's
follow list plus up to 100 friends' follow lists for EVERY card. The
graph does not depend on the target, so useViewerGraph computes it once
per viewer (one shared React Query) and deriveLocalScore returns the same
100/90/60/0 ladder (the NIP-05 30 tier classified as unknown anyway).
TrustBadge now costs no per-card network work on top of the batched
assertion read. Behaviour preserved; logged-out users still use the house
graph. Also records the ANALYZE=1 bundle findings in AGENTS.md.
Settings -> Trust is now functional, not read-only. useSetTrustProvider
publishes a kind-10040 designation for a custom provider (npub or 64-hex +
wss relay, the five 30382:<type> rows) and can clear it to fall back to the
community provider. The card validates input, prompts the signer, and
invalidates the provider + assertion queries so scores refresh. Docs
updated.
- Settings -> Trust section (?section=trust, TrustSettingsCard): a
show/hide-marks toggle (useTrustDisplay; Flagged is never hidden) and the
active provider + relay (useTrustProvider), linked to /trust.
- TrustBadge respects the display preference.
- Name AGPL-3.0 on the License page by updating its existing copy
(licenseLabel + body) in en/es.
- Settings + card i18n keys; /settings?section=trust added to the smoke
list. Docs updated.
Legibility pass: a trust verdict has to be explainable and sourced.
- TrustBadge tooltips now attribute the claim (from your network / the
community provider / the provider you chose) and link to /trust.
- New /trust page: the three states, why absence is not distrust, who
computes it, presence-is-not-endorsement, and point-of-view. Public,
linked from the badge; added to the smoke-test route list.
- New trustUi.* keys and four walletUi.trust attribution keys (en/es).
Second pass of TRUST_SCORE.md.
- useTrustAssertions now batches: every TrustBadge registers its subject into
a shared per-viewer union and ONE query fetches the union (no per-card
query), with a debounced refetch when a list grows. Same hook API.
- A direct follow (or self) is no longer shown as Verified - only a
friend-of-friend or a provider assertion counts; Flagged is never
suppressed.
- useUserReports reads public NIP-56 reports for an account, and
TrustFlaggedNotice shows a flagged counterparty with the reporters grouped
by the viewer's network plus Report / Dismiss. Wired into the listing
seller card and the coach detail page.
- New walletUi.trust.* keys (en/es).
First slice of TRUST_SCORE.md.
- lib/nip85.ts: parse kind-10040 provider designations, resolve a provider
with a house fallback, parse kind-30382 assertions (rank/followers/
reporters/muters/hops; absent hops means no path, absence is not
distrust), and author-check the provider.
- hooks/useTrustAssertions.ts: batch read from the resolved provider,
signature-verified via onlyVerified + the author check.
- lib/trustLadder.ts: three buckets and thresholds (flagged = trusted
reporters >= 2; verified rank >= 2).
- hooks/useTrustBucket.ts: prefers a NIP-85 assertion (verify + flag),
falls back to the local WoT ladder (verify only).
- TrustBadge.tsx: now renders Verified / Unknown / Flagged with glyph,
colour and tooltip; Unknown hidden by default, Flagged always shown.
Reaches the map/tour/event cards, marketplace card, listing detail and
coach detail through the shared component.
Open follow-ups (in TRUST_SCORE.md): batch the bucket at list level, a
'who reported' breakdown, a provider-choice setting, alerts, confidence
scoring.
Typing in the map search pill now opens a suggestions dropdown built
from the items that already match the active filters:
- Spots: title matches; selecting one flies to it and opens its sheet.
- Places: distinct city/region/country tokens split out of each item's
location; selecting one sets the query to that token (e.g. spain ->
Spain).
buildSearchSuggestions lives in lib (pure, testable) and
MapSearchSuggestions.tsx is presentational; MapPage owns the list and
keyboard nav (ArrowUp/Down, Enter, Escape). Adds mapPage.searchSuggestPlaces
(en/es only; other locales fall back to English).
A NIP-09 kind-5 delete is only honoured by relays and was never applied
locally, so on refresh the persisted React Query cache and the Dexie spot
cache re-rendered a just-deleted spot for a few seconds until fresh data
(or the admin hidden-label query) filtered it out.
Add src/lib/localDeletions.ts: a device-local tombstone keyed by coord
and event id, written optimistically by useDeleteEntity on the owner path
(rolled back if signing/publishing fails). The shared filterAdminHidden/
filterAdminHiddenMixed now also drop tombstoned items, so every list hook
filters them from first render with no per-hook changes. useSkateSpot
returns null for a tombstoned spot, and cacheSpots/cacheSpot refuse and
evict tombstoned rows. Prune by tombstone only (callers use limits 8-2000,
so pruning by 'not in fresh result' would wipe valid cached spots).
The language list now has a search input in both the header dropdown
(pinned above the scrolling list) and the inline list used in the mobile
nav sheet. Filtering matches the English name, native name, or locale
code, and shows a localized empty state. Adds nav.searchLanguages and
nav.noLanguages to en/es (other locales fall back to English).
The shared DropdownMenuContent used overflow-hidden with no max-height,
so any menu taller than the viewport (e.g. the language picker with 22
locales) was clipped and could not scroll to its last items. Apply
Radix's available-height CSS var as max-height and switch to
overflow-y-auto (canonical shadcn pattern).
The sheet tables carry 5-8 columns and were unusable on a phone: values were
truncated ("900" -> "90") and columns fell off the edge, and the cost-vs-quantity
table scrolled sideways.
- On max-width:639px the four data tables render as stacked cards (.ss-sheet-table
in index.css): thead hidden, each td a label/value row (label via data-label +
td::before), cells flex-wrap so a long label plus input never overflows.
- The cost-vs-quantity block is now a vertical list instead of a table, so it
never scrolls horizontally.
Verified at 390px: values are full and each row is a readable card.
- Remove the deckDesigner card from the DIY hub, so it is not listed publicly.
- Gate /diy/deck-designer to platform admins: DeckDesignerPage redirects to /diy
for everyone else and shows an "Internal preview" badge to admins.
Code and i18n keys are kept. To un-freeze, remove the Navigate guard and re-add
the card in diyGuides.ts (both noted in comments and AGENTS.md).
- The Client quote fields (15+ inputs) are now a collapsed <details>, so the
always-visible Margins card stays short; the chevron rotates on open.
- CalcSection buttons set aria-expanded.
- Every equipment/material/process/packaging row gains a duplicate (Copy) action.
- The sticky summary chip shows the order total when quantity > 1 and uses the
setup-inclusive cost.
- "List a deck at this price" passes a buyer-inclusive unit price (tax added on
top unless tax is marked inclusive).
A Simple <-> Advanced switch (not "Pro" — that name is Sasquatch Pro). Advanced
reveals four things a working shop needs:
- Per-operation labour rate (0 = shop rate) and per-operation scrap, which
compounds with the general scrap into an effective waste %.
- One-off batch setup (hours + cost), amortised over the order quantity, so the
unit price depends on qty; cost/board now uses the setup-inclusive unit cost.
- A tax / VAT line (inclusive or added on top), shown on the client quote as
Subtotal / Tax / Total or "includes X%".
- A cost-vs-quantity curve (1/5/10/25/50/100 decks).
All fields round-trip through CSV. The sheet is merged over DEFAULT_CALC on read
and clampPct is NaN-safe, so an old saved sheet or preset can't produce NaN;
currency switching also converts the new money fields.
A+B+C pass on /deck-cost.
A - Declutter: each sheet section is now an accordion (CalcSection) with its
live per-board subtotal in the header (Labor open by default, Margins always
visible), plus quick-start templates (Hobby starter / Small shop) that prefll a
realistic sheet.
B - Batch & trade: a quantity field (unit price vs order total), a wholesale
markup giving a wholesale/unit price, profit per deck, and a breakeven
(fixed / profit) with an optional target-monthly-profit goal. The client quote
shows "x N" and the order total; the deposit is taken on the total.
C - Guards: an amber warning block for markup < 1x, wholesale >= retail, rows
with 0 boards, fixed costs with 0 boards/month, no materials, equipment+overhead
over 60% of cost, and equipment spread over a single board; number inputs clamp
negatives to 0.
New state round-trips through CSV (margins rows). The default sheet produces no
warnings.
The arithmetic was already consistent, but three labels described something
other than what the code computes:
- "Overhead factor" multiplied labour only while a separate section held the
real overhead, inviting double-counting -> "Labour burden (x)" + a hint.
- "Labor cost" showed wage x burden, i.e. the loaded rate, not a per-board
cost -> "Loaded rate".
- "Profit margin (x)" was a markup -> "Markup (x)", with the implied gross
margin % shown so the name matches the number.
Also: distinct board-count columns (Lifetime boards / Boards per order /
Boards per run) instead of three identical "# boards"; "Scrap / waste (%)".
PDF and Markdown exports use the same names. CSV machine keys are unchanged so
existing exports still round-trip.
QuoteProfile gains payment methods (multi-select chips), a deposit percent
(PDF splits "Deposit due now" / "Balance on completion"), free-text payment
terms, separate payment details (bank/PayPal), and an optional Incoterm
(EXW/FCA/DAP/DDP). The client PDF gains a payment block; the PNG shows the
deposit. Quote state is now merged over DEFAULT_QUOTE on read so a quote saved
before a new field existed cannot crash the tool.
New /diy/deck-designer tool. src/lib/deckDesign.ts holds the pure geometry
(outline, waist, rounded ends, kicks, concave) and the exporters; the component
renders a live top view, side profile and concave section with presets, saved
locally.
Exports: true-size SVG, a tiled 1:1 PDF (A4/Letter, split across sheets with
corner registration crosses), and an R12 DXF (outline polyline + 3/16" truck
holes + centreline, $INSUNITS=4). Truck patterns: New school 53.975x41.275 mm
and Old school 63.5x41.275 mm.
Deliberately 2D: this is the outline a builder traces and cuts, not a SK8CAD
clone (SK8CAD is proprietary; only its JSCAD engine is MIT, and we copied
nothing).
The Quote details block gains NIP-05, npub and a Lightning address (lud16), plus
a "Fill from profile" button that autofills name/website/NIP-05/Lightning/npub
from the logged-in kind-0 profile. The client PDF lists the NIP-05 and npub in
the contact block and "Pay via Lightning: <lud16>" under the total; the PNG shows
the NIP-05 (or npub) under the business name. A PDF is a snapshot — the identity
is verifiable on Nostr, not on the paper.
- Extract the client-quote identity into its own store (QuoteProfile,
skatespots:deck-cost:quote) so it is NOT part of CalcState — loading or saving
a preset now keeps the same business name/logo/contact details.
- Client quote PDF: a real quote number counter (Q-0007, with a +1 button) and a
configurable validity window, shown as "Quote valid until <date>".
- PNG summary card now also draws the logo (top-right) and the business name.
A "Quote details" block (in the Result section) lets the user set a business/
shop name, a logo (stored as a data URL — no upload/network, capped 500 KB),
email/phone/website/address and notes/terms. The client quote PDF now renders a
logo + contact header, the line item + total, and the notes/terms. Bumped the
localStorage key to :v2 (and guarded preset/JSON loads) so an older saved sheet
without the quote block can't crash the tool.
Export ▾ now offers: PDF (full sheet), PDF (client quote — uses an optional
businessName), PNG summary card (canvas), XLSX multi-sheet workbook (lazy
SheetJS), CSV, Markdown, JSON, and Copy summary (clipboard).
Import auto-detects the CSV shape: a `name,order_cost,…` header is treated as a
supplier price list and replaces the Materials table; otherwise the CSV
round-trips the whole sheet. JSON still loads a backup.
- Export is now a menu: PDF (quote/print), CSV (spreadsheet-friendly
section,item,field,value rows), Markdown, JSON (backup).
- Import auto-detects .json / .csv: JSON loads a backup, CSV round-trips the
whole sheet (hand-rolled CSV quote/parse so commas in material names are safe).
- Currency picker now offers BOTH Bitcoin units — ₿ sats and ₿ BTC (plus fiat).
BTC is 1 per BTC, sats 1e8; switching converts between them and fiats
(BTC shows up to 8 decimals, trimmed).
- Default Materials is now a complete deck-build checklist, grouped: wood & core
(veneer/edge tape/logs), adhesives & fillers (glue/epoxy/filler), press/mold
consumables (release/vacuum film), shaping (sandpaper/saw+drill bits/masking
tape), finish & graphics (stain/primer/paint/clearcoat/grip) and Other. New
rows default to 0 so the worked example is unchanged.
The first pass overflowed the right margin (long value strings), had no section
separation, and split the Result across pages. Rebuilt the generator:
- accent rule + title, date/currency subtitle;
- each section gets a lime underline + a hairline rule;
- rows are two-line items (name + per-board value, then a grey detail line with
cost/boards/setup), so nothing runs past the margin;
- Result section is kept together (page-break guard) and the two selling prices
render in highlighted boxes;
- page numbers (n / N).
The toolbar's primary Export now generates a real PDF (lazy-loaded jspdf) — a
formatted one-page summary: labour, equipment, operations, materials, processes,
packaging, margins and the selling prices, plus the date/currency. Print-to-PDF
was rejected because window.print() doesn't work in the Capacitor WebView.
A secondary JSON export stays (for Import/backup); Import still reads JSON.
jspdf is dynamically imported so it never enters the initial bundle.
- Equipment now amortises setup time too: (cost + setupHrs × labor/hr) ÷ boards,
so the Setup (h) column actually does something.
- sats values round to whole numbers on currency conversion.
- Each section shows a per-board subtotal (equipment/materials/processes/packaging).
- Sticky summary bar (under the header) keeps cost + selling price visible while
you edit.
- Save/Load named presets (localStorage) + Export/Import JSON; Reset now confirms.
- Results gain "List a deck at this price →" (deep-links CreateListingPage with
?price¤cy, which now reads currency too) and "Get it made →" (Lab Builder).
Coinbase's exchange-rates response includes crypto (BTC, ETH, USDT, …). Filter
the picker to ISO-4217 fiat codes (FIAT_CODES) plus sats, so only real
currencies and bitcoin are selectable.
- getBtcRates()/useBtcRates() read every BTC→fiat rate from the ONE Coinbase
exchange-rates call the app already made, plus sats (1e8). No new endpoint.
- The calculator's currency control is now a searchable Popover: sats + every
fiat Coinbase reports, with a search box. Selecting a currency CONVERTS every
money field (labour, rent, utilities, subscriptions, equipment cost, material
order/shipping, packaging materials) by the live ratio; unitless fields
(overhead factor, board counts, minutes, margins) are untouched. Falls back to
relabel-only if rates aren't loaded.
- Currency symbols/formats for the common fiats.
A chip row at the top switches the whole sheet's unit: $ / € / £ / ₿ sats. Input
labels follow the unit and results reformat (sats are shown rounded, no
decimals). It relabels/reformats only — it does not convert values, since the
app only has a BTC/USD rate (no fiat↔fiat or reliable fiat↔sats conversion);
you enter amounts in the unit you think in. Choice persists with the rest.
- Removed the sticky category-chip row from /diy (sections still group the cards).
- New in-app tool at /diy/deck-cost (components/DeckCostCalculator.tsx): a
self-contained pricing model — labour rate + overhead, equipment amortisation,
operation expenses, materials (with order/unload time), process time,
packaging, profit margin and scrap → cost per board and a fair selling price
(no-packaging / with-packaging). State persists to localStorage; reset to the
worked example. The "Deck cost & pricing" card now links to it instead of
showing "Coming soon".
- Smoke test walks /diy/deck-cost.
Reworked the DIY hub per feedback: point at our own app sections where we have
relevant content, and render the topics we don't cover yet as unclickable cards
with a "Coming soon" badge — no external links.
- Live internal cards: Map your DIY spot (/create-spot), Buy parts & gear
(/marketplace), plus the "Do it with people" actions (Builder, crews, spot,
listing, coaching).
- Coming-soon cards: deck building, SK8CAD, cost/pricing, ledge/box, quarterpipe,
flatbar, concrete basics, coping, regrip, bearings/trucks, 3D-printed molds,
mold tools.
- Dropped external URLs/sanitizeUrl from the page; updated the footer copy.
New /diy hub: curated resources for making your own gear, with cross-links into
the rest of the app.
- src/lib/diyGuides.ts: seed data — categories (Boards, Ramps & wooden
obstacles, Concrete & DIY spots, Repairs & tuning, Tools & plans), resource
cards (Open Source Skateboards / SK8CAD / Instructables / YouTube), and an
actions block (find a Builder, find a DIY crew, map a DIY spot, sell builds,
find a coach).
- src/pages/DiyPage.tsx renders it (category chips + card grid + actions);
external links go through sanitizeUrl and credit the source.
- Route /diy (lazyWithRetry) + a nav-catalog entry (id diy) so it appears in the
desktop "More" menu and the mobile burger; also linked from the site footer
and the map links strip. Smoke test walks /diy.
The desktop footer is hidden on full-bleed map routes, leaving the
Newsletter/License/Privacy/Collaborators links (and the "not sponsored by
OpenSats" note) unreachable from the homepage. MapFooterStrip overlays them
inside the map container without stealing map height:
- Desktop: a slim full-width bottom bar.
- Mobile: a tiny collapsed "Links" chip that expands to a compact, scrollable
row.
- Dismissible (useMapFooterHidden, device-local, same-tab synced) and
restorable from Settings -> Appearance.
- The map's OSM attribution moved up (md:bottom-7) so the bar doesn't cover it.
The admin "It's Monday — write the drop" card was pinned at top-20 on every
route, so on the map it sat on top of the top-right toolbar (Add Spot / Weather
/ More). It now drops below that toolbar on the map routes (top-40) while other
pages keep top-20, and the dismiss button is a proper compact target.
The "Select Content to Delete" dialog was broken on mobile:
- Filter pills had no flex-shrink-0, so they squashed and their labels overlapped
("All (504Spots (Shops…"). Added shrink-0 + min-h-0 so the row scrolls cleanly.
- The global mobile `button { min-height:44px }` rule inflated the checkboxes
into giant squares. Added min-h-0/min-w-0 (and an explicit h-4 w-4).
- The per-row delete on Recent Activity was hover-only (invisible on touch);
it's now always visible on mobile and compact.
The /lab profile card and /stats Top Mappers were overflowing off the right
edge: Tailwind `grid md:grid-cols-2` (no base column) renders one implicit
`auto` track on mobile that can exceed the container when a name is long, so the
whole card got pushed off-screen. Added the base `grid-cols-1` repo-wide (plus
`min-w-0` on truncating flex/grid children).
/stats also gained a "Top Shops — most shops added" leaderboard (kind 30017)
beside Top Mappers (spots): `topShops` in useGrowthStats, a generalized
MapperRow (count + unit), and EN/ES copy. PERSIST_BUSTER → v6 (new persisted
field).
- useEscrowPrefs (device-local): default lock window, preferred escrow mint,
require-escrow default for new listings.
- EscrowSettingsCard on the Wallet (native-only, beside EscrowKeyCard).
- EscrowRequestDialog now defaults its lock window + funding mint from the prefs.
- Listings gain an optional ['escrow','required'] tag: a toggle on
CreateListingPage (defaulted from the pref) and a 🔒 badge on the card.
- Restore the always-on blink/pulse (still motion-reduce:hidden for a11y).
- Drag the bubble off the left edge to hide it; a tiny left-edge tab brings it
back. (The ✕/panel/Settings Hide paths remain.)
- Replaced the cramped inline "Go Pro" header pill with a clean full-width
strip at the top of the panel body.
Sasquatch (the look regressed): I over-corrected last pass by shrinking the
bubble on the map and bolting an always-visible ✕ over the avatar on mobile.
Restored the full-size bubble + glow on every route (the map only loses the
pulse), moved Hide to a desktop-hover ✕ and an EyeOff "Hide assistant" button
in the panel header (Settings → Appearance still re-enables it).
Mints: the add-mint dialog only offered a hand-picked "Recommended" shortlist.
It now also lists the full searchable MintRadar directory (useMintDirectory)
below the recommendations.
Events: CreateEventPage could only link a skate spot. Added an optional
"Link to a Shop" picker (useSkateShops, search, location autofill) that writes
['a','30017:…'] + ['venue-name', …].
- The wallet status bar (Cashu/WebLN/NWC pills + Manage) was a rigid
justify-between row that pushed the Manage chip off the right edge on phones.
It now wraps, with Manage right-aligned (ml-auto), so it stays on screen.
- The escrow card copy was jargon ("advertise a public key ... if the deal falls
through"). Reworded seller-facing: "turn on escrow ... a buyer can lock their
sats to you ... you release them when you deliver — if you don't, the buyer can
reclaim them after the agreed time." EN/ES.
One discoverable home for every paid extra, so the pricing model is legible
instead of scattered across the Sasquatch panel and each offer form:
- components/ProSupportScreen.tsx (Settings ?section=pro + a menu row): honest
"prepaid, not a subscription" framing; Sasquatch Pro status (days left) +
PRO_TIERS with per-day sats and a Go Pro / Extend button; Featured placement
(PLATFORM_FEES x DURATION_TIERS with per-day + best-value); your currently
featured Lab listings (featuredUntil); and the free paths (PoW listing, free
AI FAQ).
- Distinct naming in copy: Sasquatch Pro (AI) vs Featured placement (visibility).
- Smoke test now walks /settings?section=pro.
Addresses "the floating Sasquatch flashing is annoying":
- Pulse is now state-driven: the animate-ping halo shows only when there's an
unread tip (message), never perpetually; the unread dot matches.
- Honors prefers-reduced-motion (motion-reduce:hidden).
- Map-aware: Layout passes isFullBleed, so on the map the bubble runs compact
(smaller, no glow, no pulse) instead of competing with map controls.
- Hideable: a ✕ on the FAB hides it (useSasquatchHidden, localStorage
sasquatch-hidden, same-tab synced via an event), with a "Show Sasquatch
assistant" toggle in Settings → Appearance to bring it back.
The Edit action lived only in the sidebar Manage card; owners now get it in the
profile header, right next to Share. Non-owners are unaffected (Share keeps the
right alignment when there is no Edit).