Commit graph

70 commits

Author SHA1 Message Date
SkateSpots
7e48884eb9 feat(map,i18n): single-copy world clamp + regional flags + pt-PT
- Map: min zoom = smallest integer zoom whose 256*2^z world covers the
  viewport (no grey bands, no duplicated continents). Horizontal wrap is
  restored (worldCopyJump, no longitude maxBounds) so Hawaii/Pacific are
  reachable by dragging; vertical panning stays clamped.
- Flags: real SVGs for Catalonia, Basque Country, Galicia and England
  (no Unicode region flags exist; English is not the UK). Portuguese now has
  Brasil (pt) and Portugal (pt-PT).
- pt-PT shares the pt dictionary with a European drift for the visible chrome
  (Guardar, Palavra-passe, Autocarro, Utilizador, ...).
- check-i18n covers pt-PT: 24 codes at 100%; tsc/tests/build green.
2026-10-26 16:09:21 +07:00
SkateSpots
d4c6261b81 feat(i18n): add Catalan, Basque and Galician (23 locales, all 100%)
- Full dictionaries (8,703 keys each) in src/i18n/{ca,eu,gl}, 7 namespace
  batches, composed with deepMerge; placeholders verified, 2-4% identical
  strings are only brand/protocol names.
- Register in Language/LANGUAGE_LOADERS, detectLanguage (before es),
  LanguageToggle (CA/EU/GL text badges - no Unicode region flags exist) and
  dateFnsI18n (ca-ES/eu-ES/gl-ES).
- check-i18n.mjs now covers ca/eu/gl: all 23 locales at 100%, tsc/tests/build
  green.
2026-10-26 15:56:56 +07:00
SkateSpots
f56a8b28e5 chore(deps): surface the OSV total + retry 3 days after a failed run
Notifications now include the OSV summary. A failed run restores the lockfile
and schedules a retry in ~3 days instead of consuming the full 20-day interval.
2026-10-26 12:01:12 +07:00
SkateSpots
77b3c23633 chore(deps): run npm dedupe in the automated job
A minor bump can nest a duplicate transitive copy and break tsc type identity
(@tanstack/query-core, @nostrify/*). Dedupe after npm update before the test gate.
2026-10-26 11:27:51 +07:00
SkateSpots
aa2a70e9fd feat: osv-scanner in the deps job + desktop welcome sign-in modal
- update-deps.sh now runs osv-scanner (OSV DB) pre/post update, alongside npm
  audit; it immediately found 107 known vulns (5 critical) npm audit missed
  (vite/vitest dev deps, xlsx with no fix, zod patch).
- Layout opens the themed LoginDialog once for a logged-out desktop visitor
  (non-native, >=1024px, ~900ms), dismissible and remembered; it also offers
  SignupDialog. Reuses the existing extension/bunker/Amber/nsec/NostrConnect
  methods.
2026-10-26 10:59:53 +07:00
SkateSpots
a9a5966714 Cordn native: browser probe, group-ref sharing, image messages
- scripts/cordn-browser-probe.mjs: headless-Chromium proof that the built
  native chunk runs in a real browser — imports the chunk, runs a session
  against the coordinator (publish kp, create group, send), then reopens from
  the Dexie/IndexedDB store. Not part of npm test (needs network + Chromium).
- Group ref: the panel shows/copies the group's cordn1... ref
  (encodeGroupRef from the crew coordinate + coordinator), so it can be opened
  in Staircase/cordn-web. Computed in the hook via the lazy native import.
- Images: attach button uploads to Blossom (useUploadFile) and sends the URL
  inside the encrypted MLS envelope (['image', url]); CordnChatMessage.imageUrl
  + ImageWithFallback rendering. Covered by the live test.
- Unread badge deliberately not built (needs a background signed request).

npm test + smoke + browser probe + 3 live scenarios all pass.
2026-10-12 22:42:44 +07:00
SkateSpots
4ff5e25fee chore(deps): scheduled dependency update + vulnerability scan every 20 days
scripts/update-deps.sh (systemd user timer skatespots-deps.timer, daily tick, self-gated to 20 days): scans npm outdated + npm audit, applies non-breaking updates only, gates on npm test + smoke, then commits+pushes on green or restores the lockfile on red. Never --force; skips on a dirty tree; bounds the signed push.
2026-10-12 20:21:04 +07:00
SkateSpots
36d87c273f chore(backup): archive via SSH tunnel to the relay host (DNS-independent)
backup-relay.sh now tunnels to the relay host's 127.0.0.1:7777 (ssh alias skatespots-cvm, override SKATESPOTS_RELAY_SSH) and falls back to the public URL, so the weekly archive no longer depends on public DNS. Verified: 115 events archived + restic push.
2026-10-12 12:59:39 +07:00
SkateSpots
909ae8a713 chore(relay): host health check + self-heal + ntfy alerts
scripts/host/relay-health.sh (run by skatespots-health.timer every 5 min on the relay box): checks relay/ai/push/cvm/caddy + public endpoints + disk/mem, restarts the relay when its localhost NIP-11 dies, and alerts via ntfy.sh on state changes.
2026-10-12 12:54:08 +07:00
SkateSpots
fc050691cb chore(relay): configure NIP-86 admins + systemd unit for Newlay
Document the two admin pubkeys' NIP-86 tier/assignments (verified admin 200 / non-admin 403 / no-auth 401) and add scripts/systemd/skatespots-relay.service for the host compose wrapper.
2026-10-12 12:46:35 +07:00
SkateSpots
6407310213 feat(relay): cut over community relay to self-hosted Newlay
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.
2026-10-12 12:38:13 +07:00
SkateSpots
0b645d6d67 feat(lab): add Clothing Designer craft (kind 35015)
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.
2026-10-10 19:30:47 +07:00
SkateSpots
f00031f5fa fix(scripts): archive rider cards, park passes, shipment status, badges
The archive kind list had drifted: it omitted four app kinds that shipped
after it was written — 32271 rider cards, 35679 park access passes, 34967
shipment status, 1293 witnessed skill badges. Without them a restore to a
new relay would have dropped that content. Verified the archive/restore
loop end-to-end: 113 events archived, restored into a throwaway relay,
113 read back, same kinds and authors, 0 failures.
2026-10-10 17:24:15 +07:00
SkateSpots
0d15a7ce3d fix(marketplace): close the P0 audit gaps + borrow the guidelines page
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.
2026-10-09 20:21:04 +07:00
SkateSpots
18a9e28f0b feat(trust): network alerts for reported accounts you follow
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.
2026-10-09 18:32:47 +07:00
SkateSpots
fd9316331b feat(trust): Settings -> Trust and name AGPL on the License page
- 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.
2026-10-09 17:45:56 +07:00
SkateSpots
d1fae2688c feat(trust): attribute the badge and add a /trust explainer
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).
2026-10-09 17:30:00 +07:00
SkateSpots
feac9f764b i18n: register 10 locales (framework + scaffold); ro/el partial 2026-10-08 14:59:27 +07:00
SkateSpots
1460b06277 i18n: lazy-load locales + register 8 new (pt fr de it nl pl ru ko) with fonts 2026-10-07 21:23:39 +07:00
SkateSpots
ab05973ff0 i18n(ja): add Japanese as fourth locale (plumbing + font + batch 1 shell) 2026-10-07 17:21:18 +07:00
SkateSpots
b8c306125c i18n(th): batch 2 — map (mapUi + mapPage) + AGENTS note 2026-10-06 13:35:20 +07:00
SkateSpots
b247be13b0 DIY: parametric 2D deck designer with 1:1 printable template
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).
2026-10-05 11:16:34 +07:00
SkateSpots
b90fef1d2e DIY: drop the category chips; add the deck cost calculator
- 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.
2026-10-05 10:08:01 +07:00
SkateSpots
84fde55ddd Add the DIY section (resources for boards, ramps & concrete)
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.
2026-10-05 09:55:26 +07:00
SkateSpots
b91224699a Add scripts/transfer-content.mjs (move spots/shops between keys)
Addressable events (30818/30017) are author-signed, so "transferring" them
means re-signing each under the destination key + publishing, and optionally
publishing NIP-09 deletes for the originals. This script does that; it needs
TO_NSEC (destination) and, only for --delete-originals, FROM_NSEC. --dry-run
(default) needs no keys and just lists what would move.
2026-10-04 18:06:07 +07:00
SkateSpots
4cad201716 Add a Settings → Pro & Support hub
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.
2026-10-04 14:28:13 +07:00
SkateSpots
fa0b9cfe1c Fix raw i18n keys on coach chips; add structured rate + form polish
The coach option chips rendered raw 'labUi.coachForm.…' keys because the 29
per-option translation keys were referenced dynamically (t(option.key)) and had
never been added — the literal-only i18n guard missed them. Add every option
key (EN/ES) and extend scripts/check-i18n.mjs to also scan data tables for
key/titleKey/labelKey/badgeKey entries so this can't recur.

Also fix the chip layout: the global mobile 'button { min-height:44px }' rule
turned every chip into a 44px slab; add min-h-0 to the coach option chips,
language chips, Lab facet chips and Lab active-filter chips.

Remaining nice-to-haves:
- Structured rate: rate_amount + rate_currency (sats/USD/EUR/GBP) + rate_unit
  (hour/session/half-day/day/camp), rendered on the detail page; legacy
  freeform 'rate' still shows and acts as an optional note.
- Section the long coach form with a FormSection divider.
- Restyle the coach identity block from a red error-looking box to a neutral
  avatar card.
2026-10-04 10:05:11 +07:00
SkateSpots
391eeb9bf9 fix(ux): recoverable route error screen + smoke-test shop/spot routes
- The per-route 'slammed' screen only offered 'Go back', so a stale-chunk
  failure after a deploy trapped the user. Add a primary 'Reload' that clears
  the lazyWithRetry session flag and does a fresh reload.
- Smoke test now boots /shop/:pubkey/:identifier, /spot/…, /event/…, /crew/…
  so a broken detail route can't ship again.
- Investigation note: no reproducible slam across 11 live shops; the report
  matched a transient chunk miss from two rapid deploys.
2026-10-02 10:52:18 +07:00
SkateSpots
dbb9ea98c0 feat(card): make Rider Cards real & verifiable
- Publish the Rider Card as an addressable event (kind 32271, d=rider-card) and
  hydrate the editor from it, so customization persists across devices.
- New routes: /card/:npub (public read-only card) and /verify/:naddr
  (signature check + park venue binding).
- Park Access Cards can bind to the issuer's own spot (a tag 30818:...) and the
  verify page confirms the issuer owns the venue.
- QR on the card (+ canvas export) now encodes the verify URL; the real QR
  renders in the React card too (was a placeholder / broken profile path).
- Extracted shared card visuals to src/components/RiderCard.tsx; helpers in
  src/lib/riderCard.ts; hooks useRiderCard/useRiderBadges.
- Documented in NIP.md; i18n cardUi namespace (EN/ES); smoke test covers the
  new routes.
2026-10-02 10:26:54 +07:00
SkateSpots
09735375c2 feat(i18n): Spanish (Castellano) — infra + Map/Spots, Marketplace, Lab (batch 1)
- First-visit language auto-detect (Spanish devices land in es; stored choice wins).
- Sasquatch AI replies in the app language (skate trick names kept English).
- i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t()
  key missing from en/es or any en/es key drift.
- ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and
  ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace,
  trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters.
- Policy: only skate TRICK names stay English; user-generated content untouched.
2026-10-01 23:57:20 +07:00
SkateSpots
807037ee9b feat(push): self-hosted closed-app push service (Phase 1 backend)
Our own keyless Bun service (not Linky's): watches relays for kind-1059 wraps
carrying the plaintext marker ["skatespots","push"] on the recipient copy,
extracts only {recipient, wrapId}, and sends a Web Push. NEVER decrypts - the
client fetches + decrypts the named wrap itself.

Endpoints: GET /health, GET /vapid-public-key, POST /auth/challenge ->
/subscribe|/unsubscribe with a one-time NIP-98 (kind 27235) challenge proof.
State is a JSON file; VAPID keys from env. Wired like the other standalone
servers (typecheck:push + check:push in npm test, deploy:push, Caddyfile +
systemd unit). Verified locally end-to-end: auth -> subscribe -> marked wrap ->
watcher -> push attempt, and proof replay rejected.

Client half (service worker, subscribe hook + Settings toggle, marker on DM
sends) is the next step.
2026-10-01 15:34:43 +07:00
SkateSpots
09b51a764a fix(ops): don't flag a dirty-tree skip that's already been committed
The deploy timer can record a dirty-tree skip mid-edit; once the tree is clean
that reason is stale. Verify git status before flagging it (and parse the timer
NEXT row only when it is a real timestamp).
2026-10-01 11:15:16 +07:00
SkateSpots
126eb38330 fix(ops): don't cry wolf about a deploy that just hasn't ticked yet
A push seconds before an ops-health run has no skip reason yet — that is
normal, not a problem. Only flag a missing skip reason once the deploy script
hasn't run for >6 min (i.e. the timer really looks stopped).
2026-10-01 10:59:14 +07:00
SkateSpots
b6cc7797e6 fix(ops): don't imply the current sha was itself deployed
A non-site-only skip advances the recorded sha without a production deploy,
so 'deployed Nm ago' could read as if that sha was deployed. Say 'last
production deploy Nm ago' instead.
2026-10-01 10:42:51 +07:00
SkateSpots
50fcb419f4 feat(ops): a "why isn't it live?" health check (signer + deploy)
Two failures today were invisible: the ngit/Amber remote signer stalling
mid-push, and the deploy timer skipping while origin/main had moved.

- scripts/auto-deploy.sh records the reason for every skip (and the run time)
  to ~/.local/state/skatespots-deploy/{last-skip,last-run}.
- scripts/ops-health.sh reads that back and prints repo/push sync, the skip
  reason, the throttle window and timer health; exit 1 when action is needed.
  --probe-push runs a bounded real push (the only true signer probe),
  --notify desktop-notifies (deduped), --quiet is silent when healthy.
- Periodic watch: skatespots-ops-health.{service,timer} (every 15 min) plus
  scripts/install-ops-health-timer.sh.

Docs: AGENTS.md Deploy section.
2026-10-01 10:42:01 +07:00
SkateSpots
ae2a1c70b8 feat(coordinator): profile discovery — publish yours, add theirs in one tap
Carrier is the existing kind-0 'cordn' field (app convention, NIP.md) — no new
kind. It now holds a coordinator *pointer* rather than only a URL.

- EditProfileForm: accepts nprofile / cordn.net link / npub / hex / http URL,
  validates via parseCoordinatorInput, normalises a bare pointer to Cordn's
  canonical add-link on save, and adds 'Use my coordinator'. Label -> Coordinator.
- NIP19Page: when the profile's cordn value parses as a pointer, render a
  Coordinator row with a one-tap Add (sets the viewer's coordinator, with toast)
  and an 'Open in Cordn' link; otherwise keep the old plain-link row.
- NIP.md: document the broadened cordn field (pointer formats + one-tap add).
- smoke test: now boots a real profile route too.
2026-09-29 16:17:16 +07:00
SkateSpots
7b33d544b1 feat(coordinator): Cordn client slice — pick/add a coordinator + run-your-own hand-off
SkateSpots stays a Cordn *client*: it points at a coordinator the user
chooses or runs, and hands phone-hosting off to Cordn's own app. No embedded
coordinator, no default SkateSpots coordinator (deliberate: liability).

- src/lib/coordinator.ts: Cordn's own formats — an nprofile pointer, the
  canonical add-link https://cordn.net/chat/coordinators?c=<nprofile> (from
  Cordn packages/server/src/main.ts), default relay wss://relay.contextvm.org,
  the docker one-liner. Parses nprofile/npub/hex/link; encodes an add-link.
- src/hooks/useCoordinator.ts: device-local store + presence probe. Health is
  NOT Cordn RPC — it queries the coordinator's relays for authors:[pubkey] (its
  ContextVM announcement/heartbeat), so a private/unannounced coordinator
  honestly reads 'Not seen' rather than failing.
- src/components/CoordinatorSettingsCard.tsx + Settings -> Coordinator:
  empty/connected states, add by paste or QR scan, add-link copy + QR, and a
  'Run your own' hand-off (open Cordn, scan its QR, copy the docker command).
- smoke test now boots /settings?section=coordinator.
2026-09-29 15:11:36 +07:00
SkateSpots
010910500b test(smoke): also boot the /settings?section=profile route
The Edit Profile screen (preview card + checklist) had no route coverage in the
boot test. Guards the lazy Settings chunk on that exact URL.
2026-09-29 14:51:14 +07:00
SkateSpots
5b63a69a2c chore(backup): support a second offsite repo (restic-2.env)
backup-relay.sh now pushes the archive to restic.env AND, if present,
restic-2.env — so adding an R2/B2 bucket gives true 3-2-1 (local + CVM +
object storage) instead of replacing one offsite with another. Each repo is
sourced in its own subshell so RESTIC_* don't leak between them.

Verified with a throwaway local second repo: both pushes ran, both snapshots
exist. Also documents the umask trap (mode-600 cache dirs break restic).
2026-09-28 14:24:57 +07:00
SkateSpots
40799e1453 fix: homepage is skate-spots.org (not .com) + skip no-op deploys
The package.json homepage pointed at skate-spots.com — a different domain on a
different host, not this app.

Also adds a deploy guard: auto-deploy.sh now hashes exactly what would be
uploaded (dist/ + netlify/ + netlify.toml) and skips the Netlify deploy when it
is byte-identical to what is already live, so metadata-only commits (version
bumps, homepage) stop costing 15 credits.
2026-09-28 14:00:06 +07:00
SkateSpots
ec06a63b01 feat(ai-proxy): run the AI proxy on the CVM host, ready to switch over
Same proxy as netlify/functions/ai.mjs, rebuilt as a standalone Bun service on
the CVM host so AI calls stop spending Netlify compute/bandwidth credits and
the sats-paid Routstr key lives on our box.

- ai-proxy/server.ts: Bun.serve on 127.0.0.1:8787 — NIP-98 + Pro gate, model
  allowlist, per-IP throttle, /api/ai + /api/ai/status. Own tsconfig
  (typecheck:ai in npm test); eslint-ignored like cvm-server.
- ai-proxy/{package.json,.env.example,skatespots-ai.service,Caddyfile,DEPLOY.md}.
- scripts/deploy-ai-proxy.sh (npm run deploy:ai): scp + bun install + unit +
  restart, mirroring deploy-cvm.sh.
- Client: one place decides the AI base, src/lib/sasquatchAi.ts (also the NIP-98
  u tag). Defaults unchanged (/api/ai on Netlify); set VITE_SASQUATCH_AI_BASE to
  move it. Fully reversible.

Deployed and verified on the host: /api/ai/status returns the live Routstr
balance. Public TLS (DNS + Caddy) is the remaining step before switching the app
over, so no client behaviour changes yet.
2026-09-28 13:32:05 +07:00
SkateSpots
f46f6a8967 chore(deploy): throttle production deploys to one per 24h
The timer deployed on every site-affecting commit at 15 Netlify credits each
(~68 deploys / ~1,020 credits in a week). auto-deploy.sh now enforces a minimum
gap between successful deploys, default 24h, overridable with
SKATESPOTS_DEPLOY_MIN_INTERVAL=<seconds> (0 disables). A throttled tick records
nothing so the change still deploys later; failures retry immediately.

Deploy on demand with: scripts/auto-deploy.sh --force

scripts/docs only — skipped by the timer, so this costs 0 credits.
2026-09-28 13:20:31 +07:00
SkateSpots
c89d32d92e chore(backup): offsite on the CVM host (SFTP) + proven restore drill
- Offsite backup is now live: restic over SFTP to the CVM host via a scoped
  ~/.ssh/config alias (no host/key in tracked files); restic.env (600, outside
  the repo) holds the repo URL + generated password. Verified: first push
  dedupes to ~32 MiB and 'restic check --read-data-subset=10%' is clean.
- scripts/drill-relay.mjs: throwaway in-memory NIP-01 relay (ws) for restore
  drills. Ran the loop end-to-end against the real archive: 114/114 events
  restored, 0 missing.

Scripts/docs only — the deploy timer skips this (no Netlify credits).
2026-09-28 13:16:53 +07:00
SkateSpots
c01521ea3f feat(backup): scoped weekly archive of the community relay dataset
scripts/archive-relay.mjs snapshots the app's own content off
goskatespots.nostr1.com to gzipped JSONL of the raw signed events, so a
restore can republish them verbatim to any relay. scripts/restore-relay.mjs
re-verifies signatures and publishes to an explicit --to (no default, to
avoid replaying a snapshot onto the live relay). scripts/backup-relay.sh
wraps it and pushes with restic when a repo is configured; a weekly systemd
user timer runs it.

Scoped on purpose: that relay is PUBLIC and carries general traffic (15.7k
kind-0 profiles, 2k kind-30078 app-data from other clients, 408 foreign
NIP-99 listings), so the archive keeps only the app's own kinds/tags plus
contributor profiles, SkateSpots labels, and reports that reference archived
content. ~114 events / 63 KiB.

Not configured yet: offsite (restic.env) and a full restore drill.
2026-09-28 12:41:50 +07:00
SkateSpots
a43ff47b28 feat(spots): honest quality signals, photo required, off-topic reports, admin review queue
The 'test of secret island airport' problem: a junk pin wearing an app-endorsed
'Skateability 95/100'.

- Rename the weather score to 'Conditions · weather only' (SpotDetailPage +
  the SpotMap popup). It was never a verdict on the place.
- New Community verified / Unverified badge from signals only OTHER skaters can
  produce: community photos + useSpotStatus reporterCount (kind 2259).
- CreateSpotPage now REQUIRES at least one photo (guard + disabled submit +
  a required hint on the label).
- useNotASpotReport: one-tap NIP-56 (1984) report tagged 'not-a-skate-spot'
  (NIP-56 'other' + our L/l/t taxonomy), wired into the spot's More menu and
  added to the shared ReportDialog reasons.
- useAdminHide: the MISSING WRITE SIDE for admin moderation. useAdminModeration
  always read NIP-32 kind-1985 'hidden' labels but nothing published one, so the
  only working hide was device-local. Now publishes the label (+ optimistic
  local hide). Soft and reversible — the event stays on relays.
- /review (AdminReviewPage, admin-only → 404 otherwise): review queue over
  useSkateSpots with cheap flags (suspicious title, name <=3 chars, no photo, no
  description, no geohash), sorted by flag count, one-tap Hide or Not a skate
  spot. Entry via the admin-only AdminReviewCard in Settings → Advanced.
- /review added to the smoke-test route list.

Verified: the airport spot shows UNVERIFIED with 'Conditions · weather only' and
no 'Skateability'; the review queue lists it with flags + Hide/report actions;
create-spot blocks without a photo.
2026-09-27 23:41:49 +07:00
SkateSpots
4341653abe feat(app-shop): add free printable sticker + poster (vector, real QR)
- public/images/print/skatespots-sticker.svg (55 x 86 mm) and
  skatespots-poster.svg (A3) — the free artwork to print and stick up. Physical
  size is baked into the SVG (mm units), so 'Actual size' printing is exact.
- Self-contained: Outfit/Barlow fonts + the logo embedded as data URIs, and a
  real vector QR. VERIFIED both QRs decode to https://skate-spots.org (decoded
  with jsQR from a rendered PNG; logo centre, error-correction H).
- New 'Free to print' section in the App Shop: previews, FREE badge, sizes,
  cutting tip, 'Open & print' + 'Save file' buttons, and a 100%/Actual-size note.
- scripts/make-print-art.mjs regenerates the artwork (committed output).
- AGENTS.md documents the assets + the re-verify-the-QR rule.
2026-09-27 22:16:43 +07:00
SkateSpots
8dcc97fd81 feat(app-shop): add the App Shop section (empty on purpose)
New /app-shop page for SkateSpots merch, to be paid in sats only (Lightning or
Cashu) — no cards, no accounts, no middleman.

Design: gradient/glow hero ('Wear the scene' + Coming soon badge), an honest
stock strip (0 items / None pre-orders / Nothing for sale today), an 'In the
works' teaser grid (tees, decks, stickers, tools — dashed + 'soon', no prices),
a three-step 'How it'll work' (pick -> pay in sats -> ships), a newsletter
notify band (the drop lands there first) and a plain-English honesty footer.

Deliberately contains no items: no stock, no prices, no pre-orders, no buy
buttons. AGENTS.md documents that rule so nobody invents a catalogue later.

Wiring: registered in NAV_CATALOG as id 'app-shop' (appears in the desktop
'More' menu AND the mobile burger menu automatically), lazy route in AppRouter,
and added to the smoke-test route list so a broken page aborts deploys.

Verified: page renders desktop + mobile (all sections), listed in both navs,
smoke PASS with /app-shop.
2026-09-27 21:40:41 +07:00
SkateSpots
9d0c31f673 chore(deploy): skip production deploys for non-site commits
Netlify bills 15 credits per production deploy, and this cycle already spent
~315 on 21 deploys. auto-deploy.sh now skips when the change between the last
deployed SHA and origin/main touches only non-site files (AGENTS.md/*.md,
scripts/, cvm-server/, skills/) — it records the SHA without deploying, so doc
commits cost nothing.
2026-09-27 14:42:57 +07:00
SkateSpots
f7855d47e4 chore(cvm): one-command deploy to the single host + document the setup
- scripts/deploy-cvm.sh: backup remote server.ts -> upload -> restart -> confirm
  the heartbeat, with --rollback. Host/SSH values come from the never-committed
  ~/.config/skatespots/cvm.env (or CVM_HOST), so no host details ever enter this
  public repo.
- package.json: add `npm run deploy:cvm`.
- AGENTS.md: record that the CVM server runs on ONE external host (deploy via
  deploy:cvm) and that the discovery relay, not the server, is the SPOF.
2026-09-27 12:04:57 +07:00
SkateSpots
a881a87f4d feat(lab): add Barber / Hairstylist profile (kind 35014)
A full Lab role, mirroring how Event Organizer was added:
- kinds: SKATE_BARBER_KIND 35014, self-tag 'skatebarber', BARBER_*_LABELS,
  SkateBarber + parseSkateBarber (studio, books_open).
- craft config at /become/barber (cyan accent, Sparkles icon), Lab tab
  'Barbers', filter sections (services/styles/setting), empty state, FAB item.
- Lab pipeline: useLabProfiles kind/tag/parser, LabPage ServiceType
  'barbering' + ACCENT/maps/counts, CoachDetailPage per-role maps + studio &
  books-open block, LabLinkField label, growth stats + Community Stats card.
- No crew-scene strip for barbers (LabCrewsStrip returns null).
- Documented in NIP.md; smoke test now boots /become/barber.
2026-09-27 10:15:30 +07:00