Commit graph

12 commits

Author SHA1 Message Date
SkateSpots
6746353670 feat: add Transport Lab craft with self-attested fleet profiles 2026-10-29 22:34:28 +07:00
SkateSpots
075aea7f44 perf: lazy content images and targeted crew member queries 2026-10-28 13:11:10 +07:00
SkateSpots
e94c16a479 perf: batch profiles, gate map layers, warm public cache 2026-10-28 12:42:39 +07:00
SkateSpots
5728c226d5 fix(audit P1): comment cache key, ticket verify fails closed, featured amount check, no empty-object persist, sticky sub-bars 2026-10-05 21:07:07 +07:00
SkateSpots
084c002c45 Fix mobile grid overflow + add a Top Shops leaderboard
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).
2026-10-04 18:01:49 +07:00
SkateSpots
1a834f7b40 fix(stats): robust trade volume instead of a sybil-inflatable sum
The marketplace 'Sats Traded' was a naive sum of the self-reported amount tag on
31871, which two colluding accounts could inflate for free. tradedSats is now the
robust figure - mutual trades only (a listing attested by >=2 distinct pubkeys),
per-pair cap 3, amounts >1 BTC dropped - with tradedCount + tradedTraders, and
tradedSatsClaimed keeps the raw claim so the card shows both. Added query fields
=> PERSIST_BUSTER v5. No trade attestations exist on the relay yet (honest 0).
2026-10-01 15:53:13 +07:00
SkateSpots
9fe219aac6 feat(stats): reconcile the region card, and geocode shops too
Answers 'why don't the map and stats match' by making the gap visible, and
brings shops into the region breakdown.

- src/lib/geocodeRegions.ts is now the single reverse-geocode helper (spot
  create, shop create, shop edit), so the Nominatim call + Accept-Language + the
  parse rules can't drift. Never throws.
- Shops now publish city/country/country_code (previously none), so they can
  appear in the region breakdown.
- Stats: regions cover spots AND shops; a new 'Uncategorised (N)' row plus a
  '{N} of {M}' footnote let the card reconcile with the location total. Section
  retitled 'Spots & Shops by Region'.
- PERSIST_BUSTER v3 -> v4.
- /privacy: wording now 'spot or shop geocoding'.

The map deliberately still reads the community relay + the reader's relays
(forcing canonical would hide an opted-out user's own spots) - documented.
2026-10-01 13:27:44 +07:00
SkateSpots
58154bf5d5 feat(stats): stable country grouping, an Other row, and disclose Nominatim
Answers 'how are these accounted?' and fixes the privacy-page gap it exposed.

- Spot creation now also publishes ['country_code', <ISO-3166-1 alpha-2>], kept
  alongside the display 'country' name so existing search/display still work.
- Stats group countries by country_code when present, else a lowercased name;
  the label is the most common raw name in the group, so a localised spelling
  can't split one country across two rows.
- The tail beyond the top 6 is surfaced as an 'Other' row, and the section is
  relabelled 'Spots by Region' (it counts spots, not people) with a footnote.
- PERSIST_BUSTER v2 -> v3 (GrowthStats shape changed again).
- /privacy: disclose OpenStreetMap Nominatim — spot geocoding sees your IP and
  the spot's pin (never the device's GPS). It was missing from the IP list.

Docs: AGENTS.md.
2026-10-01 13:16:02 +07:00
SkateSpots
863b6a115d fix(stats): don't crash on a persisted cache from the old data shape
/stats slammed with 'Cannot read properties of undefined' for anyone whose
IndexedDB held a growth-stats cache saved before the new fields existed —
stats.topCountries.length threw.

- Bump PERSIST_BUSTER v1 -> v2 (the documented way to invalidate a shape change;
  it is wired into App.tsx).
- Read the new fields defensively (stats?.x ?? []) so a future field can't do
  this again.
- AGENTS.md: note that the smoke test cannot catch this (clean profile, no
  persisted cache).
2026-10-01 13:02:21 +07:00
SkateSpots
c79373ed05 feat(offline): the map survives a dead relay (and says so) + skater@skate-spots.org
Relay outages no longer look like 'no spots'. querySkateContent swallowed every
relay error and returned [], so a dead relay would blank the map AND overwrite
good cached data with an empty list (then persist it). Now:

- querySkateContent/queryCanonical take an opt-in 'requireAny' that throws when
  NO relay answered. Only the map's own feeds use it, so the blast radius is
  small (79 other call sites keep today's behaviour).
- useSkateSpots/useSkateSpot fall back to the local IndexedDB cache on failure,
  so the map still works at the park; they only error when nothing is saved.
- The persister refuses to persist an empty array, so an outage can't wipe the
  last-known map on the next cold start.
- MapPage shows an honest 'Offline · showing saved spots' banner (reusing the
  existing relayProbe/useRelayStatus) and a 'Can't reach the relays' card
  instead of falsely claiming 'No spots mapped yet'.

Also seeds skater@skate-spots.org for the second admin (caba497f…) and reserves
the name so nobody else can claim it.
2026-09-28 12:48:54 +07:00
shakespeare.diy
8d464a05f3 fix: resolve pre-existing tsc errors blocking the test gate
Two type errors (surfaced by `npm run test` / tsc --noEmit on a full
toolchain; the in-browser bundler does not run tsc, so they were latent):

- useCVMClient.ts: remove a malformed cast
  `spots as typeof spots & { tags; created_at }[]` feeding
  deduplicateByIdentity(). The intersection dropped pubkey/kind and broke
  the reassignment to `spots`. deduplicateAddressable() already returns
  the correct shape, so the cast was both wrong and unnecessary.

- queryPersister.ts: import PersistedClient / Persister from
  @tanstack/query-persist-client-core (their canonical source) instead of
  @tanstack/react-query-persist-client, whose `export *` re-export tsc
  won't resolve for named type imports. Added the core package (5.102.8,
  matching the pinned wrapper) as an explicit dependency.

Both are pre-existing issues on the branch, unrelated to the audit
remediation commits.

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

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

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

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

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

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