- Full 8,703-key Hindi dictionary in src/i18n/hi (7 namespace batches),
placeholders verified, only 2% identical strings (brand/protocol names).
- Register hi in Language/LANGUAGE_LOADERS, detectLanguage (hi-IN),
LanguageToggle (🇮🇳, हिन्दी) and dateFnsI18n (hi-IN).
- Lazy-load Noto Sans Devanagari for hi via lib/localeFonts.ts (renamed from
cjkFonts.ts) and add it to the CSS font stack; other locales don't download it.
- check-i18n covers hi: 24 dictionaries / 25 codes at 100%; tsc/tests/build green.
- 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.
- 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.
- react-router-dom/react-router 7.18.4: the app only uses the v6-compatible API
(Link/NavLink/Routes/HashRouter + hooks), so zero code changes; tsc, tests and
the full-route boot smoke all pass. Clears the open-redirect advisory.
- vitest 5.0.5 (dev): clears vitest/@vitest/mocker/tinypool criticals. Setup now
imports @testing-library/jest-dom/vitest for the new expect types.
npm audit: 8 total (0 critical, 1 high = xlsx write-only, 7 moderate = build
tooling).
- aria-label on the language dropdown trigger (flag-only) and the header
settings icon (title-only) so screen readers announce them.
- AGENTS: final security scan (audit 35->13, zod 4/nostrify 0.50, APK manifest
and CSP verified) + menu/a11y conclusions.
The dismissal was stored in localStorage, so one early ✕ suppressed the prompt
forever. Switch to sessionStorage (key skatespots:desktop-welcome-snoozed): it
reappears in a new tab/session but respects 'keep browsing' for the current one.
- Fix the real bug: the dialog clipped its bottom links (flex column + max-h
calc(100dvh-2rem), content min-h-0 flex-1 overflow-y-auto).
- One dominant action per state: 'New here' is a compact row, not a second hero.
- Plain card labels (Browser extension / Signer app / Sign in with your phone)
with tiny NIP-07/46/55 hints; protocol jargon out of the main labels.
- Bunker: input + Connect with instructions collapsed under 'Where do I get a
bunker?'; no duplicate heading/divider.
- Secret key moved to a quiet bottom link; subtitle says 'or keep browsing'.
- 8px spacing grid (px-5 gutters, space-y-4 sections, gap-2 cards, p-2.5 cards,
448px max width). New strings backfilled into all 20 locales.
Verified with headless 1280x900 + 390x844 screenshots.
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.
Owner decision: the map's spots + shops are canonical (relay.skate-spots.org
only), not blended with the viewer's personal relays. useSkateSpots and useShops
now use queryCanonical with a stable 'canonical' query-key suffix. The spot
detail page (useSkateSpot) stays blended so shared links to a spot published only
on a user relay still resolve. Verified with a headless map screenshot showing
canonical clusters.
The anon max_limit (1000) was below the map's 2000-spot query, so the map
appeared empty; raised to 5000. Also raised the per-IP connection gate (429s).
A Chengdu spot had landed only on ditto/damus because the app's publish resolves
on the first relay; republished it to the community relay (signature intact).
Parity: 56 spots on our relay vs 26 on ditto/damus (only junk missing).
About page renders ShareAppCard on Android only: a QR of the Zapstore listing,
a share-sheet button for the install link, and NfcWriteButton to write it to a
tag. Single-sourced APP_INSTALL_URL in lib/platform.ts (also used by the About
download button). No peer-to-peer APK transfer (needs native Wi-Fi Direct/BLE +
PackageInstaller + device QA); Zapstore handles install/update.
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.
LoginDialog leads with a three-card chooser (Browser extension, Signer =
Bunker on desktop / Amber on Android, Sign in with your phone) and a quiet
Secret Key link; the NostrConnect QR moved to its own connect panel. The
desktop welcome modal opens on this. Verified with a headless 1280x900
screenshot. Also records why the full npm update was reverted (duplicate
transitive copies + TS bump) and that the remaining fixes are majors.
- 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.
Bump 1.6.6 -> 1.6.7. Native Amber intents now send compressionType=none and the JS
signer decodes Signer1 gzip results, so long encrypted payloads no longer fail.
APK built + published to Zapstore (verified on relay.zapstore.dev).
The web/PWA Amber path set compressionType=none; the native intents set neither
compressionType nor returnType, and the JS gzip decoder was never called. NIP-55
lets Amber gzip a large result (Signer1... base64), so a long nip44_decrypt (a
long DM) could arrive compressed and be parsed as garbage. Native intents now
send compressionType=none; the JS signer also gunzips any Signer1 result. Timeout
message no longer wrongly says Amber may not be installed.
Bump 1.6.5 -> 1.6.6, notes for the always-NIP-17 crew chat, instant sends +
persisted decrypt cache, the 20-locale i18n backfill, and the private-chat invite.
APK built + published to Zapstore (verified on relay.zapstore.dev).
245 keys had accumulated in en/es only (trustUi, labOptions, cordnUi, socialUi,
walletUi, marketTerms, …), leaving every other locale at 97%. Backfilled 245x20 =
4,900 strings into src/i18n/<lang>/drift.ts and merged them through a new shared
src/i18n/deepMerge.ts — a shallow ...drift spread would clobber existing
namespaces, so index.ts deep-merges instead. Verified: check-i18n all 20 at 100%,
tsc + eslint clean.
The last Amber pain: a fresh launch re-decrypted the whole inbox (2 signer ops
per wrap). Decrypted messages are now persisted to IndexedDB
(src/lib/dmDecryptCache.ts, keyed viewerPk:wrapId, bounded 4000/account, pruned
oldest-first) and hydrated once per session before the first lookup, so a wrap is
decrypted at most once ever. Cleared per-account on logout (plaintext at rest
otherwise readable by another account on the device).
Per the crew-chat audit recommendations:
- The crew Chat tab no longer silently switches transport on a coordinator tag.
It is always NIP-17; coordinator routing lives only in the Private chat (MLS).
useCordnChat / useCrewChatUnread / lib/cordn client+crypto are kept but unwired
(headers marked). Drops the tab's Cordn-only unread dot.
- Group reads (Groups list + crew thread) widened 200 -> 500 wraps; the shared
decrypt cache makes the extra reads affordable. 1:1 list stays 200.
Messages now appear on send; shared DM decrypt cache stops per-view re-decrypts;
double-send guarded. Bump 1.6.4 -> 1.6.5, APK built + published to Zapstore
(verified on relay.zapstore.dev).
Audit of the crew Chat tab (see CREW_CHAT_AUDIT.md):
- A sent crew message never appeared: useSendDirectMessage invalidated the list
keys but not ['group-messages', gid, ...] (the thread key). Now optimistically
inserts the sender's own plaintext (no refetch -> no Amber pop-ups).
- Every reader decrypted every wrap under its own query key, so each view
(Messages, then each crew thread) re-decrypted the whole inbox. Added a shared,
coalesced, bounded session decrypt cache (decryptGiftWrap), successes only.
- Threads dedup by rumor id; CrewChat guards double-send with a ref; the Cordn
path optimistically inserts and invalidates only for silent signers.
Fix: sending to a crew sequential-ised its per-recipient signer intents, so Amber
approvals land instead of timing out. Bump 1.6.3 -> 1.6.4, APK built + published
to Zapstore (verified on relay.zapstore.dev).
Sending to a crew fired every recipient's gift wrap in parallel (Promise.all),
and the sealed-relay crew chat encrypted to the whole roster in parallel. Amber
is a single foreground activity: concurrent nostrsigner: intents mean only one is
answered and the rest sit until the 60s native timeout rejects with 'Amber didn't
respond'. Both paths now go sequentially for an interactive signer (parallel kept
for silent signers). Also corrected the approval hint (2 per member: encrypt +
sign, not 1 per recipient).
Bump 1.6.2 -> 1.6.3, notes for the Amber navigation fix, the Messages Crews
section, and the crew encrypted-chat card rewrite. APK built + published to
Zapstore (verified on relay.zapstore.dev).
The sidebar card badged 'Private chat - On' while the MLS tab said 'not created
yet', and led with external apps. Rewrote CrewCoordinatorCard to show two
explicit rows (Chat = NIP-17 always on; Private chat = MLS with real status) and
make the in-app chat the primary CTA (?tab=mls); demoted Cordn web / Staircase
under 'Open in another app'. Panel now gives join feedback + an admin invite
hint. Adds CORDN_UX_AUDIT.md (findings, terminology model, deferred items).
- DirectMessagesPage gains a 'Crews' block listing the viewer's crews with an
Open/Set-up badge (from one local IndexedDB read via useCordnGroupGids),
linking into /crew/<pk>/<id>?tab=mls.
- Crew page tabs are now URL-addressable (?tab=events|chat|mls); an explicit
param wins over the auto-default. useUserCrews + cordnGroupGids added.
For an interactive signer (Amber) each nip44 decrypt opens the app. The DM/NIP-17
read hooks decrypt 2x per gift wrap and DirectMessagesPage mounts two of them, so
navigating to Messages stormed Amber. Strengthened amberSafeQueryOptions for
interactive signers (staleTime/gcTime Infinity, refetchOnMount/refetchOnReconnect
false) so decryption happens once per session, mirroring useStash; same guard on
usePrivateContacts + useCordnChat. Added a manual Refresh to the Messages header
(Amber gets no polling). Gated useSyncPendingPosts' legacy re-sign loop for
interactive signers.
Bump 1.6.1 -> 1.6.2, release notes for the first-run checklist + backup-reminder
fixes. APK built and published to Zapstore (verified on relay.zapstore.dev).
- FirstRunChecklist showed to any logged-in user until manually dismissed; it now
requires a recent signup_completed stamp AND first-run-seen (auto, once), so
established/imported accounts never see it.
- SignupDialog wrote the backup-confirmed flag under user?.pubkey while the login
was still settling, so it landed on 'backup-confirmed:anon' and the post-signup
BackupReminder nagged every session. Now markBackupConfirmed() is called with the
pubkey derived from the minted nsec.
- NIP-49 export password field autoComplete new-password -> off (fewer browser
save-password prompts).
- Crew page defaults to the Private-chat (MLS) tab when this device already has
the group (useCordnGroupExists reads the IndexedDB snapshot — local only, no
signer prompt); a manual tab click always wins.
- Founder/admin banner ('Set up') points at the tab when the crew has no group.
- Bump 1.6.0 -> 1.6.1 and add release notes for everything since 1.6.0 (private
crew chat/MLS, self-hosted relay + media, marketplace close-the-deal, trust
controls, map/weather, Clothing Designer, UI fixes).
- 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.
Milestone 2 for the native Cordn client.
- Session now runs on the viewer's own signer (NIP-07/Amber/nsec) instead of a
raw key, so the Cordn identity is the user's pubkey. Added idbStore.ts
(Dexie 'SkateSpotsCordnDB', one snapshot per account) so MLS state survives a
reload, plus membership delivery: requestJoin / fetchJoinRequests /
fetchWelcomes / acceptWelcome / addMemberByIdentifier, and addMember now also
stores the Welcome on the coordinator for the target.
- useCordnNative owns the session lifecycle and lazy-imports the native module,
keeping @contextvm/sdk + ts-mls in a separate ~678 kB async chunk (entry
bundle unchanged at 1.9 MB).
- CordnNativePanel: a third 'Private chat' tab on the crew page — founder/admin
create the group and approve join requests; members ask to join and check for
an invite; live message list + composer (polling only for silent signers).
- Strings in i18n/legacy/cordnUi.ts (en/es).
Verified live against our coordinator with a new scenario
(join-request -> approve -> welcome) plus the existing two; npm test + smoke
pass. Not yet exercised in a real browser (browser tooling was offline this
session) — the dynamic-import failure path degrades to an inline error.
Add src/lib/cordn/native/ — a browser-safe Cordn client doing real RFC 9420
MLS over a ContextVM coordinator, mirroring Cordn's wire contract (reimplemented,
not copied from @cordn/cli).
- mls.ts: identity, key packages, group create/add/join, application messages,
the ChaCha20-Poly1305 coordinator wrapper, causal prev tags.
- coordinator.ts: ContextVM/MCP-over-Nostr client on @contextvm/sdk, validated
with @cordn/core schemas.
- session.ts: CordnSession (key packages, createGroup, addMember->Welcome,
joinGroup, send, fetch) with persistence behind an injectable CordnStore
(MemoryCordnStore today; IndexedDB adapter TODO).
- Verified end-to-end against our own coordinator: a two-member round trip
(CORDN_LIVE=1 vitest run native.live.test.ts) — create, add, join, and
bidirectional encrypted messages with proven senders.
Deps: @cordn/core, @contextvm/mcp-sdk, @noble/ciphers pinned 2.2.0 (fixes the
previously invalid tree: ts-mls wants 2.2.0, a stale root copy was 0.5.3).
src/test/setup.ts is now guarded for node-environment test files. Not wired to
any UI yet; nothing imports the module, so the web bundle is unchanged.
Crews can carry a Cordn group ref (['cordn', <cordn1-ref-or-url>] -> SkateCrew.cordnGroup): CrewForm gains a 'Cordn group' field and CrewCoordinatorCard shows an 'Open crew chat' (cordn.net/chat/<ref>) + 'Copy for Staircase' row. Verified a real ContextVM client (@contextvm/sdk) connects + lists tools against our self-hosted coordinator.
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.
Bundles leaflet CSS + MarkerCluster CSS (main.tsx) and the default marker PNGs (SpotMap) instead of loading them from unpkg.com, so boot makes no third-party request; unpkg.com removed from the CSP (index.html meta + _headers + netlify.toml). Adds dns-prefetch/preconnect for relay.skate-spots.org and blossom.skate-spots.org.
Kanit (th), Noto Sans JP (ja) and Noto Sans KR (ko) were imported statically in main.tsx, so every visitor downloaded ~16MB of CJK font files. They're now loaded on demand once per locale (lib/cjkFonts.ts, wired in LanguageContext), so non-CJK users never fetch them and CJK users get their font after the first paint.
STATUS_META labels were 'coordinator.*' but keys live under 'socialUi.coordinator.*', so badges rendered raw keys (e.g. coordinator.statusStale). Prefixed all six. Also dropped relay announce_interval_secs from 86400 (24h) to 900 (15m) so the coordinator health probe reads Connected instead of stale.
Relabel the cordn.net add-link accurately as 'Cordn (web)' (real MLS in the browser) and add a 'Copy for Staircase' button that copies the coordinator nprofile for the native app. Our coordinator by default; a BYO coordinator's add-link when it has a pubkey.
CrewCoordinatorCard now offers a StaircaseLink in both branches: our coordinator (SKATESPOTS_COORDINATOR add-link) when a crew has none, and the crew's own coordinator add-link when it has a pubkey (falls back to the Cordn home otherwise). Lets crews jump to real MLS chat in the native Cordn client.
Newlay runs its embedded Cordn coordinator (identity cordn.key, uid 10001). SKATESPOTS_COORDINATOR is the provided default: Settings -> Coordinator shows a 'Use SkateSpots coordinator' block and CrewForm prefills it for new crews; BYO coordinator stays client-side. Disclosure copy (coordinators see ciphertext + metadata, never contents) in socialUi.coordinator.ourDefault* + existing privacyNote.
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.
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.
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.
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.
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.
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.
Full app audit (UX_AUDIT_2026-10-09.md), heavy on the marketplace flow
sequence. Headline verified findings: useMarketplaceItem does not filter
admin-hidden or exclude showcase-only listings (a hidden/not-for-sale
listing is reachable by URL); the buyer CTAs compete on fixed-price
listings and Make an offer is shown to logged-out users; the grid's Mark
as sold skips MarkSoldDialog so no trade attestation is recorded; the hub
tabs are not URL-addressable; nav.marketplace and nav.trades are
duplicate labels; and the nav customizer Reset hardcodes 3 tabs against
MIDDLE_SLOTS 4. Includes a recommended fix order. Audit only, no code
changes.
Capture the recommendation in AGENTS.md (and mirror it in TRUST_SCORE.md):
do not build the cvm-server per-observer GrapeRank scorer yet. The full
confidence model is provider-side and consumed via NIP-85; the client has
a local confidence gate; the blockers are the per-observer signing key and
8-hop compute plus lifecycle/deploy/monitoring, with ~zero users making
the ROI negative. A house-perspective scorer is the cheaper path; revisit
when there are users, no trustworthy provider, or skate-specific signals
to bake in.
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.
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.
Replace the license file with the unedited AGPL-3.0 text (copied in full
from the teambtcmap/btcmap-android LICENSE, byte-identical). No project
header and no 'or later' grant, so SPDX is AGPL-3.0-only. AGENTS.md records
the rationale and the section-13 source-offer implication.
Add the canonical AGPL-3.0 text at /LICENSE (with the project copyright
notice on top) and set package.json license to AGPL-3.0-or-later. Chosen
because the trust work reviews AGPL reference implementations and we
reimplement rather than copy; AGPL section 13 source-offer is covered by
the public repo + the License page's source link. Recorded in AGENTS.md.
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.
Ideas-only review of a personalized web-of-trust approach: a spec-correct
NIP-85 consumer (kind-10040 provider designation, house fallback, author +
signature verification, the rank/followers/reporters/muters/hops tags, and
the absence rules), a Verified/Unknown/Flagged display with a viewer mode,
counterparty alerts, confidence-gated scoring, presets and negative
signals. Notes what we already have, concrete file touch points, license
and privacy caveats, and a recommended first slice. Links it from the
parked identity/conflict section.
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 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.
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.