Editable in EditProfileForm and displayed on the profile:
- Bitcoin Lightning Address (lud16) — now editable in-form, shown with a
labeled Lightning row in the Info card.
- Bitcoin Silent Payment Address (BIP-352, custom `bitcoin_silent_payment`)
— validated (sp1…), shown as a copyable, truncated row.
- Sponsors & Support (custom `sponsors`) — freeform highlight, rendered in
a dedicated pink "Sponsors & Support" sidebar card.
- Cordn contact link (custom `cordn`) — private MLS messaging link
(cordn.net), shown as a "Message privately" row in the Info card.
Crews and the CLINK noffer tip button were already present, so they are
left as-is. Added isValidSilentPayment / isValidLightningAddress
validators and documented the custom kind-0 fields in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use the established "Stash" term everywhere instead of introducing a new
word. The profile section is now titled "Stash", visibility copy reads
"Shown in your public Stash on your profile", and internal identifiers
were renamed (useUserQuiver → useUserPublicStash, QuiverItem →
PublicStashItem, parseQuiverItem → parsePublicStashItem). NIP.md updated
to match.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Previously the "Public" visibility on Stash items did nothing — the
whole Stash is a single NIP-44 encrypted blob, so nobody could ever see
a "public" item. This makes Public actually mean public.
Model (hybrid A + marketplace):
- Public items are now ALSO published as separate unencrypted NIP-99
kind 30402 events tagged `t: stash-showcase`, keyed by `stash-<id>`.
- Public + not-for-sale → showcase-only (`showcase: true`, price 0):
shows on the profile "Quiver", filtered OUT of the sellable market.
- Public + for sale/trade → full listing: shows on the profile Quiver
(badged For Sale/Trade) AND flows into the marketplace.
- Going private or deleting an item publishes a NIP-09 deletion for its
public event, keeping everything consistent.
Changes:
- New hook useStashShowcase (sync/unpublish + useUserQuiver query).
- StashPage wires add/edit/remove through sync handlers; honest
visibility copy in add + edit dialogs.
- Profile page (NIP19Page) renders a new "The Quiver" grid with
For Sale/Trade badges + prices, and a Gear count in Contributions.
- Marketplace hooks skip showcase-only events so they don't clutter
buy/sell listings.
- NIP.md documents the showcase event schema.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The avatar "Profile" tab in the mobile bottom bar linked to /settings,
so tapping your own face took you to Settings instead of your public
profile. It now routes to /:npub (View Profile) when logged in, and
still falls back to /settings when logged out (where login lives).
Long-press to customize the nav is preserved.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Let the title badges (Encrypted · Syncs everywhere · sats) wrap
gracefully instead of colliding with the toolbar controls
- Hide the list/grid view toggle on the empty state (nothing to toggle)
so it no longer clutters the space next to the Add button
- Give the title row room to grow on mobile (min-h-14 + padding)
instead of forcing everything into a fixed 56px row
- Make the search input flex-fill and shrink the category select on
small screens so the filter bar no longer overflows
- Add aria-labels to the view-toggle buttons
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Panel now anchors md:bottom-16 md:left-4 (was bottom-right) so it opens on
the left as before, and sits ABOVE the desktop footer so it never covers the
"Made with..." row or footer links.
- Tightened max-height to clear the top nav too (never slides under the menu).
- Bubble stays bottom-right on desktop for discoverability (clear of footer).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- findCoaches now covers EVERY Lab craft (coach, filmer, editor, photographer,
builder, artist, graffiti, tattoo, trainer, musician, judge) via role param;
reads skills from `t` tags and strips self-tags.
- New findShops (kind 30017, t=skateshop) and findTours (kind 30823) tools.
- getCommunityStats counts all crafts individually + lab_total + shops + tours,
and filters spots to t=skatespot (drops foreign kind-30818 events).
- Wired new tools + expanded role vocabulary into the Sasquatch brain: system
prompt, intent parser, keyword detector, and both dispatch/format paths.
- Privacy preserved: the public CVM data layer never queries the encrypted
Stash (kind 30001) — verified no 30001/decrypt references. Personal 'my
stash' stays login+Pro gated and decrypts locally with the user's own key.
- Normalized useCVMClient.ts to clean UTF-8 (comment glyphs only; no code change)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Move bubble & panel to bottom-RIGHT on desktop (was bottom-left, colliding
with / hiding behind the desktop footer). Mobile stays bottom-left.
- Raise bubble/panel z-index to z-60 so it always sits above the footer.
- Re-clamp a stale saved drag position on mount: a bubble dragged off-screen
on one device no longer disappears on another.
- Stop showing the long welcome greeting + action button on every open once a
conversation has started (chatMessages > 0), so it no longer "says the same
thing all over again".
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Flesh out SkateJudge as a full craft-based Lab profile (parseCraftBase),
with JUDGE_FORMAT / SPECIALTY / LEVEL label sets covering Game of S.K.A.T.E.,
best trick, jams, contests, am series, world tour stops, etc.
- Wire judge into useLabProfiles (union type, self-tag, parse case)
- Add "judging" ServiceType to LabPage: tab, accent, filter sections, empty
state, counts, profile card icon, and ?craft=judging deep link
- New BecomeJudgePage + /become-judge route + FAB entry
- CoachDetailPage renders judge profiles (accent, icon, labels, noun, title)
- Community Stats "Judges" card now links to /lab?craft=judging
- Document kind 35012 (skatejudge) in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useGrowthStats now queries and exposes every Lab craft individually: artists,
graffiti, tattoo, trainers, musicians and judges (previously folded into an
opaque "other" bucket with no visible breakdown).
- CommunityStatsPage renders a card for all 11 Lab crafts, so the section matches
what /lab actually lists instead of only showing 5 categories.
- labTotal + the content-mix donut's "Lab" slice now sum every craft, keeping the
"total" badge and chart consistent.
Note on relay mismatch: the app dedupes addressable events (keeps newest per
pubkey+kind+d) and merges across the community relay + the user's relays, so
counts can legitimately differ from a raw single-relay browser that shows stale
duplicate versions.
Bump version to 1.2.1 (versionCode 10201).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. Map filter: renamed the "Bitcoin" toggle to "Spedn" (matches the toolbar
button); tooltip now says "Spedn your sats".
2. Community stats (/stats): useGrowthStats now queries the same relay set as the
rest of the app (community relay + user relays) via useSkateRelays instead of
only the single hardcoded SKATE_RELAY. Lab profiles / spots / etc. that live on
a user's own relay are now counted, so the numbers match what /lab shows.
3. Near Me on GrapheneOS: even after granting the OS location permission, the
WebView's navigator.geolocation stays blocked → "permission denied". Fixed by
fetching the position from the native Capacitor Geolocation plugin first
(LocationManager, honours the granted permission), falling back to
navigator.geolocation only if that fails. Also auto-inject
ACCESS_FINE/COARSE_LOCATION into AndroidManifest via the manifest script.
Bump version to 1.2.0 (versionCode 10200).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause found in the plugin's native getPublicKey (NostrSignerPlugin.java):
when a packageName is set, it FIRST tries Amber's ContentProvider (background,
no-UI). On GrapheneOS that cross-app ContentResolver query is blocked and never
returns, so it never falls through to startActivityForResult(nostrsigner:) —
the branch that actually opens Amber's approval UI. Result: eternal
"Waiting for Amber".
Fix: getAmberPublicKeyViaPlugin no longer calls setPackageName and no longer
passes packageName; it passes only the get_public_key permission. With no
package name the plugin skips the content-resolver branch and launches the
intent directly, opening Amber's UI. Amber returns both pubkey and its package,
so later signing still works. Paired with the auto-injected <queries> manifest
block so the OS can resolve the intent in the first place.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two concrete root causes for the eternal "Waiting for Amber…":
1. setPackageName could hang forever on GrapheneOS. getPlugin() awaited it with
no timeout, so getPublicKey() (and its 90s timeout) was never even reached.
Now setPackageName races a 3s timeout and continues regardless — the Amber
package is passed explicitly on every call anyway, so it's non-essential.
2. AndroidManifest.xml had no <queries> block. On Android 11+/GrapheneOS the OS
silently blocks the nostrsigner intent without it, so Amber never opens. Added
scripts/inject-amber-queries.mjs (idempotent) and wired it into android:init /
android:sync / android:build so the block is guaranteed present in every APK,
surviving cap sync regeneration.
Bump version to 1.1.9.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: LoginDialog and useCurrentUser detected the native app via
window.Capacitor, which is unreliable inside the WebView (false at mount / not
attached in some builds). So the ZapStore APK fell into the web intent:// redirect
flow and never opened Amber ("Waiting for Amber…" hang).
Fix: add isNativeAmberAvailable() in capacitorAmberSigner.ts using the imported
@capacitor/core Capacitor + native plugin presence, and use it (evaluated at
click time) in LoginDialog.handleAmberLogin and in useCurrentUser's signer
selection. The APK now calls getAmberPublicKeyViaPlugin() → native intent → Amber
opens directly, no redirects.
Bump version to 1.1.7.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Login (Amber tab):
- Emphasize the proper NIP-55 redirect flow: one tap → approve in Amber →
redirected back to /amber-callback → poll auto-completes login. No copy/paste.
- Replace the indefinite spinner with a clear "Waiting for Amber…" state that
auto-signs-in on return, with an "Open Amber again" retry.
- Demote manual pubkey entry to an explicit "Having trouble?" escape hatch that
the user must tap — it never hijacks the flow anymore.
- Removed the aggressive 2.5s auto-switch to paste; keep background polling on
both waiting and paste states so a late callback still completes.
Signup:
- Welcome step now recommends "Sign up with Amber" first on Android (key lives
inside Amber, SkateSpots never touches it), with a clearly-secondary "quick
account on this device" option. Wired onUseAmber through LoginArea to open the
Amber login flow.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- CrewCard: rebuilt to fit any screen width — clip overflow, truncate name/location, single wrapping badge row, consistent avatar sizing; responsive header
- Amber web login: after opening Amber, drop user on manual paste step after 2.5s so the spinner never hangs when the callback opens in a new tab or clipboard is blocked; keep polling on the paste step so a late callback still auto-completes
- Stash: hasNip44/hasNip04 now fall back to the active login when pubkey matching fails, so logged-in nsec/amber users are no longer wrongly told "Encryption Required"
- Signup: added an explicit welcome step that explains account creation and only generates the keypair when the user taps "Create my account" (no more silent auto-creation on dialog open)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The web manualChunks put react-dom/react-router in a vendor-react chunk
but left core `react` (useLayoutEffect etc.) in a different chunk, so
react-dom executed before React was defined:
"Cannot read properties of undefined (reading 'useLayoutEffect')".
Group react, react-dom, scheduler, react-router and jsx-runtime into the
same vendor-react chunk so the React runtime loads together and first.
Note: Capacitor APK builds must use CAPACITOR_BUILD=1 (single inlined
bundle) via `npm run android:build` — the plain `npm run build` emits the
chunked web output that breaks in the Android WebView.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- signerCapabilities: recognize x-amber login type as NIP-44/NIP-04
capable (Amber implements nip44_encrypt/decrypt via native intents and
URL redirects). Fixes "Encryption Required" wall on Stash for users
logged in with Amber on Android.
- CrewDetailPage: restructure hero header so actions wrap below the
title on mobile instead of squeezing it in a cramped 3-column flex;
smaller avatar/title on small screens, truncating location.
- MapPage: remove Quick Snap from the mobile Add speed-dial and all its
handlers, refs, web-fallback input, and now-unused imports.
- zapstore.yaml: explicit orange icon + metadata_sources: [] so the
correct logo is always used on the Zapstore listing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Swapped in the new brand logo for every place the app icon appears:
- Favicon, apple-touch-icon, and og:image / twitter:image (via index.html
references to /images/icon-1024.png)
- PWA manifest icons (any + maskable) and all app shortcuts
- In-app header logos (Layout, Index) and logo.png / logo-icon.png
- This same icon-1024.png is what the Zapstore listing pulls, so the store
entry updates too
Also corrected the manifest icon "sizes" declarations to match the actual
asset dimensions (180x180) so browsers don't reject the icons for a
size mismatch.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The guide's action buttons used window.location.href = '/onboarding', a hard
full-page navigation. In the SPA/native (Capacitor) context this reboots the
app at its entry point (home) rather than resolving the client-side route, so
tapping "Learn More" dumped users back on the home page.
Switched all Sasquatch guide navigation (Learn More, Login to begin, Login to
Offer Services / Create a Crew, Start exploring) to React Router's navigate(),
which performs proper in-app client-side routing and reliably renders the
target page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Since signup now defers the key backup, users who skip it MUST be able to get
their recovery code later — otherwise we've just moved the trap. This adds a
"Your Recovery Code" card at the top of Settings > Security that:
- For nsec logins: reveals the recovery code on demand (hidden by default),
with Copy, Download (.txt), and QR options, plus a Hide button and clear
"never share this" warnings.
- For bunker / browser-extension / Amber logins: explains that SkateSpots
never held the key and points the user to back it up in their signer app.
Also removed the old placeholder QR handler that only showed an error, and
updated the "Backup Not Confirmed" copy to direct users to the new reveal
card instead of falsely claiming lost keys are unrecoverable.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- "Sign Up" now goes straight into a one-tap flow: a key is generated silently
and the user is logged in immediately, landing on optional profile setup.
- Removed the method-choice dialog (mnemonic vs nsec) and the generate/download
steps from the default path — no crypto vocabulary, no gates.
- Removed the forced key-download gate (Continue was disabled until download).
Backup is now optional and deferred (BackupReminder already nudges post-login;
the "done" step offers a gentle, jargon-free recovery-code reveal).
- Softened copy throughout: "secret key" -> "recovery code / account backup",
"your identity on Nostr" -> plain reassurance, CTA -> "Save & Start Skating".
- Advanced signer-app path preserved under a subtle link.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- NofferField: link → https://clinkme.dev/specs.html (instead of just /)
- EditProfileForm: link → https://clinkme.dev/specs.html (instead of just /)
- SasquatchGuide: added [specs] and [apps] inline links next to the clink description, pointing to the new pages
The app already referenced clinkme.dev homepage; these add the two new deeper pages for discoverability.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Completes the admin moderation coverage from the previous commit.
Lab profiles (coaches, filmers, editors, photographers, builders, artists,
graffiti, tattoo, trainers, musicians):
- useLabProfiles / useLabProfile and useCoaches / useCoach now filter out
admin-hidden entries (list) and 404 hidden ones (detail) via the shared
moderation layer. Added filterAdminHiddenMixed() for the mixed-kind Lab
union (kind read from each item's event).
- CoachDetailPage (the shared Lab detail page) now shows the delete/takedown
card to admins too, not just the owner, with admin-specific copy.
DeleteEntityDialog already handles the owner-or-admin logic.
Gear / marketplace listings (NIP-99, kind 30402):
- useMarketplaceItems / useMarketplaceAtSpot filter out admin-hidden listings.
- GearSwapItemCard: admins get a "Remove (admin)" action in the card menu with
a confirmation dialog that routes through useDeleteEntity (NIP-09 delete +
hidden label). Report stays for non-owners.
SECURITY.md: updated the admin-moderation coverage list and auditor file map.
Build + lint pass, no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The two SkateSpots admin pubkeys (already in src/lib/admins.ts) can now remove
any listing, not just their own. Because a NIP-09 deletion signed by a
non-author isn't honored by most relays, admin takedowns are made reliable with
a client-side moderation layer:
- useDeleteEntity: owners still publish a plain NIP-09 kind:5. Admins acting on
someone else's content publish the kind:5 AND a NIP-32 kind:1985 `hidden`
label referencing the target coordinate/id, authored by the admin.
- useAdminModeration (new): fetches admin-authored `hidden` labels app-wide and
exposes a fast lookup + filterAdminHidden() helper. Only labels whose author
is in the hardcoded ADMIN_PUBKEYS are honored (author = trust boundary,
re-verified client-side; never derived from a d-tag).
- Wired the hide filter into the list + detail hooks for spots, crews (incl.
by-focus), tours, events, and shops via React Query `select`, so removed
content disappears from lists and 404s on its detail page.
UI:
- DeleteEntityDialog now renders for owner OR admin, with distinct
"Remove (admin)" framing and copy (used on Crew, Coach, Tour, Event pages).
- SpotDetailPage & ShopDetailPage: routed their inline delete through
useDeleteEntity, show the delete button for admins, and relabel the confirm
dialog for admin takedowns.
- Added the delete control to Tour and Event detail pages (previously none).
SECURITY.md: documented the admin moderation trust boundary and extended the
auditor file map. Verified build + no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Address the two genuinely exploitable findings from the OpenCode audit:
C1 (critical) — sasquatchBrain.ts shipped a hardcoded OpenRouter API key in
the client bundle, extractable and abusable by anyone. Removed it; OpenRouter
is now opt-in via VITE_OPENROUTER_API_KEY and skipped when unset. Sasquatch
still works for everyone via the existing keyless Pollinations fallback.
(The leaked key must also be rotated/revoked on openrouter.ai.)
H1 (high) — SpotMap popup interpolated external weather-API data directly into
innerHTML. Added an escapeHtml() helper and applied it to every weather/AQI/
forecast value so a compromised weather API can't inject HTML/JS.
Triage of the rest: C3 (crew agent derived key) is a documented, deliberate
tradeoff — agent holds no funds and only speaks for itself. H2 (chart.tsx
dangerouslySetInnerHTML) is the stock shadcn chart with app-controlled config,
not user input. H3/H4 (GitHub Actions) don't apply — no .github/ in this repo.
C2 (NWC in localStorage) is standard for web Nostr clients. npm audit fix +
key rotation are operator steps done outside the app.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Follow-up to the map-XSS work. The Capacitor app ships the same dist/ bundle
(webDir: 'dist') and renders the same untrusted relay content, so it shared
the same injection surface — but the CSP only existed as HTTP headers
(public/_headers, netlify.toml), which never reach a WebView loading from a
local scheme. The prior "OS sandbox is enough" comment was wrong: the sandbox
isolates the app from OTHER apps; it does not stop a same-origin injected
script from reading this app's own localStorage (nsec, NWC string, Cashu seed)
or IndexedDB (Cashu bearer ecash proofs, native-only wallet).
Changes:
- index.html: add a <meta http-equiv="Content-Security-Policy"> so the policy
ships inside the bundle and applies to BOTH web and native. script-src stays
'self' 'wasm-unsafe-eval' (no unsafe-inline/eval). Capacitor local origins
(https://localhost, capacitor://localhost, ionic://localhost) are allowlisted
on script/style/img/connect so asset loading & live-reload still work.
Replaces the old "CSP intentionally omitted" comment. Verified the app boots
and the map renders with no CSP violations.
- SECURITY.md: correct the CSP section (now three synced locations incl. the
meta tag; explain the local-scheme reason and the OS-sandbox misconception),
add Cashu ecash/seed to the threat model and residual-risks, note the shared
bundle, and extend the auditor file map (useCashu.ts, platform.ts,
capacitor.config.ts).
Note: the actual injection fix (createElement + textContent in map popups)
already shipped on mobile since it lives in the shared bundle; this commit
adds the defense-in-depth layer that was previously web-only.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add root SECURITY.md documenting the threat model (plaintext nsec / NWC in
localStorage inherent to client-side Nostr), reporting channels (Nostr DM /
NIP-05), in/out of scope, the CSP and its trade-offs, untrusted-data
rendering rules, URL/CSS sanitization, author-scoped queries, NWC budget
guidance, accepted residual risks, and an auditor file map.
- netlify.toml now serves the same Content-Security-Policy as public/_headers
and uses X-Frame-Options: DENY + Permissions-Policy, so the hardening holds
regardless of which static host serves the web build.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
An NWC connection string is a spend credential stored client-side. If a
connection is granted an unlimited budget, a compromise could drain the
wallet up to that limit. Add a prominent security notice in the "Connect
NWC Wallet" dialog instructing users to set a small daily budget and scope
the connection to pay_invoice only, and reminding them it can be revoked in
their wallet. This caps the blast radius of the credential the app must hold.
(The CSP referenced in the report was already added in the prior commit via
public/_headers; storage of nsec/NWC strings in localStorage is inherent to
client-side Nostr signing/payments and is mitigated by the XSS + CSP work.)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Critical: Leaflet marker popups built untrusted Nostr event fields (and
BTC Map API fields) into innerHTML, bypassing React's output encoding. Any
user could publish a spot/tour/crew/event with an <img onerror> payload in a
text field; it executed on /map render with no interaction, letting injected
script read the plaintext nsec from localStorage (nostr:login) and the Cashu
seed (cashu-mnemonic) — permanent, unrotatable account + wallet takeover.
Fixes in src/components/SpotMap.tsx:
- Rebuild all popup DOM with createElement + textContent instead of innerHTML
for every untrusted field: spot.location; tour.title/location/meetingPoint;
evt.title/location; crew.name/location; BTC Map name/address/opening_hours/
phone/description.
- Validate every event/API URL with sanitizeUrl() before it reaches an
href/src: crew.image (popup + marker icon) and BTC Map website. Drop
anything that isn't http(s). tel: links reduced to safe characters.
- Replace inline onerror handler on the crew marker image (would be blocked
by CSP anyway) with a scripted img.onerror fallback.
- Add popupRow() helper so untrusted detail rows can never be innerHTML'd.
Defense in depth (web only, native Capacitor build unaffected):
- Add public/_headers with a strict Content-Security-Policy: default-src
'none'; script-src 'self' 'wasm-unsafe-eval' (no unsafe-inline/eval);
object-src 'none'; base-uri 'none'; frame-ancestors 'none'; plus the
https:/wss: allowances a Nostr client genuinely needs for relays, media,
and mints. Also X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy.
- Move the inline boot-crash-guard script to public/boot-guard.js so
script-src can stay 'self' with no 'unsafe-inline'; index.html now loads it
externally. The guard also now paints errors via textContent, not innerHTML.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
useCrew previously relied on a single combined filter (kind + #d + authors,
limit 1). Some relays don't reliably match combined author + d-tag filters,
causing the crew detail page to render NotFound even when the crew exists.
Now, if the precise addressable lookup returns nothing and the author pubkey
is known, we fall back to fetching that author's crews (kind + authors) and
matching the identifier client-side. This survives relays with quirky tag
indexing while still preferring the exact match when available.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Latitude/longitude fields are now optional for local/national tours
- If provided, both must be filled and valid; otherwise geohash and
coords tags are omitted from the published event
- Updated input placeholders and added helper text clarifying coordinates
are optional
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>