feat(map): standalone ownship marker toggle, decoupled from Live View (#25)#104
Conversation
Records the user-approved split: decouple the existing GPS marker into a standalone opt-in Settings toggle now; defer AIS overlay with BYOK-aisstream recorded as the only compliant architecture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwEfjZyZiSy2NvrRhJHqUF
…View (#25) Ships the ownship-marker half of #25 per the 2026-07-22 spec addendum (AIS overlay deferred, no code). A GPS boat marker already existed but rendered only inside Live View when a plan with legs was loaded. This generalizes it: - Settings.showOwnship (default OFF/opt-in), persisted like other settings, with a "show my position" checkbox in OptionsPanel (mirrors motorEnabled). - useOwnshipGps.ts subscribes to geolocation.ts's watchPosition whenever the toggle is on, in ANY map context (planning, no plan, Live View) — reusing (not duplicating) the shared one-time GPS-denial hint, extracted to lib/gpsHint.ts so LiveView and the new hook share one claim. - OwnshipMarker.tsx renders BoatMarker (unchanged) from that fix; it is now the ONLY place BoatMarker is ever rendered — LiveView no longer renders its own, which is what structurally prevents a double marker when Live View is active and the toggle is on. - i18n: options.showOwnship.label/.help added to both dict.de.ts/dict.en.ts. geolocation.ts and BoatMarker.tsx are unchanged, per the addendum. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwEfjZyZiSy2NvrRhJHqUF
Self-review — approved (one deferred minor)Reviewed with independent re-verification ( Minor, non-blocking (deferred to a follow-up): when Everything else verified clean: single |
Summary
Implements the ownship-marker half of #25 only, per the user-approved
spec addendum "Addendum 2026-07-22: Ownship marker (standalone) + AIS overlay
(deferred) (#25)" in
docs/superpowers/specs/2026-07-14-sail-command-design.md.The AIS traffic overlay is explicitly DEFERRED — no AIS/WebSocket/MMSI
code is in this PR. That decision (BYOK-aisstream is the only compliant
architecture; gated on user go-ahead + a coverage spot-check) is recorded in
the spec addendum only.
services/geolocation.ts,components/BoatMarker.tsx,both unchanged) previously rendered only inside Live View, gated on
active && legs.length > 0. This generalizes it into a standalone, opt-infeature that renders in any map context (planning, no plan, or Live
View).
Settings.showOwnship: booleanfield, default OFF, persistedexactly like other settings (IndexedDB via
db.ts, generically — nodb.ts/AppState.tsxcode changes were needed).useOwnshipGps.tshook: subscribes towatchPositiononly while thetoggle is on; a denial/unavailable error claims the SAME one-time hint
LiveView already used (extracted to
lib/gpsHint.tsso both consumersshare one storage-key claim, never a second/duplicate hint).
OwnshipMarker.tsx: a thin presentational component that rendersBoatMarker(+ its accuracy circle) from the hook's fix. Mounted as analways-present sibling of
RouteLayer/LiveViewinsideMapView'ssubtree (mirrors
DataLayers.tsx's "always-mounted, plan-independent"pattern).
LiveView.tsxno longer rendersBoatMarkeritself at all —OwnshipMarkeris now the only render site for it. This makes "no doublemarker when Live View is active and the toggle is on" a structural
guarantee rather than a runtime race between two GPS watches. Live View's
own route-following (active-leg highlight, heading-to-steer,
distance-to-next-maneuver, ETA drift) is unchanged and still requires a
plan.
OptionsPanel.tsx(mirrors the existingmotorEnabledcheckbox pattern), with a help paragraph carrying the "aid,not a navigation device" caveat and consumer-GPS-accuracy framing.
options.showOwnship.label/options.showOwnship.helpadded toBOTH
dict.de.ts/dict.en.ts(MsgKeyparity viasatisfies).Test plan
npm --prefix app run typecheck— cleannpm --prefix app run lint— cleannpm --prefix app run test— full suite green: 66 files / 671 testspassed (includes
realmask.repro.test.ts, unaffected by this change)test):
useOwnshipGps.test.ts— does not subscribe while disabled; subscribesand exposes the emitted fix verbatim while enabled; unsubscribes +
clears the fix when disabled again; claims the shared hint on
denial/unavailable; does NOT re-show the hint if another consumer
already claimed it;
dismissHinthides without re-touching storage.OwnshipMarker.test.tsx(BoatMarker mocked) — renders nothing whenfixis null; renders BoatMarker with the fix's fields forwardedverbatim when present; falls back
headingToSteerDegto0absent adevice COG.
LiveView.test.tsx— new test assertsBoatMarker(mocked) is nevercalled, even with Live active and a fully steerable fix — the concrete
proof backing the "no duplicate marker" structural claim above.
OptionsPanel.test.tsx— checkbox unchecked againstDEFAULT_SETTINGS;toggling calls
onChangewithshowOwnship: trueimmediately; helptext carries the "not a navigation device" caveat.
AppState.test.tsx—showOwnshipdefaults to'false'; a patchpersists to IndexedDB and survives a provider remount.
gpsHint.test.ts—claimGpsHintOnce()pinned against the literalsc-gps-hint-shownstorage key LiveView's own tests already assertagainst (proves the two consumers share one claim, not two).
by default.
navigator.geolocation→.sc-boat-markerappears on the map with no plan loaded, correctly rotated to the
fake COG. Screenshot confirms visually.
querySelectorreturns null).fixture) and activated Live View with the toggle OFF: a fix arrives, but
confirmed zero
.sc-boat-markerelements (proves Live View alone nolonger renders one).
was not completed live: the shared Playwright MCP browser got
contended by another concurrent session partway through (page URL
silently jumped to a different session's
localhost:5173, twice — thedocumented shared-browser risk in CLAUDE.md). I did not force it further
to avoid polluting another session's state. The structural guarantee
(LiveView never renders
BoatMarker— proven above) plus the two cleanreal-browser checks already obtained cover this in spirit; a final
live double-check of that exact combination before merge would close
the gap if the browser is free.
unrelated to this change).
(
git statusclean before commit).Deviations / notes
geolocation.tsandBoatMarker.tsxarebyte-unchanged, as required.
db.ts/AppState.tsxrequired NO code changes —Settingspersistenceis fully generic (plain-object IndexedDB roundtrip +
Partial<Settings>merge), so the new field "just works" through existing machinery.
advancedSummary's one-line "Advanced" recap (PlannerPanel.tsx) was leftuntouched — it exists to recap routing-relevant settings, and
showOwnshipdoesn't affect route calculation.Part of #25.