Skip to content

feat(map): standalone ownship marker toggle, decoupled from Live View (#25)#104

Merged
DocGerd merged 2 commits into
developfrom
feat/25-ownship
Jul 22, 2026
Merged

feat(map): standalone ownship marker toggle, decoupled from Live View (#25)#104
DocGerd merged 2 commits into
developfrom
feat/25-ownship

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner

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.

  • A GPS boat marker (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-in
    feature that renders in any map context (planning, no plan, or Live
    View).
  • New Settings.showOwnship: boolean field, default OFF, persisted
    exactly like other settings (IndexedDB via db.ts, generically — no
    db.ts/AppState.tsx code changes were needed).
  • New useOwnshipGps.ts hook: subscribes to watchPosition only while the
    toggle is on; a denial/unavailable error claims the SAME one-time hint
    LiveView already used (extracted to lib/gpsHint.ts so both consumers
    share one storage-key claim, never a second/duplicate hint).
  • New OwnshipMarker.tsx: a thin presentational component that renders
    BoatMarker (+ its accuracy circle) from the hook's fix. Mounted as an
    always-present sibling of RouteLayer/LiveView inside MapView's
    subtree (mirrors DataLayers.tsx's "always-mounted, plan-independent"
    pattern).
  • Dedupe: LiveView.tsx no longer renders BoatMarker itself at all —
    OwnshipMarker is now the only render site for it. This makes "no double
    marker 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.
  • New "show my position" checkbox in OptionsPanel.tsx (mirrors the existing
    motorEnabled checkbox pattern), with a help paragraph carrying the "aid,
    not a navigation device" caveat and consumer-GPS-accuracy framing.
  • i18n: options.showOwnship.label / options.showOwnship.help added to
    BOTH dict.de.ts/dict.en.ts (MsgKey parity via satisfies).
  • Fully offline: GPS is device-local, no network/key involved.

Test plan

  • npm --prefix app run typecheck — clean
  • npm --prefix app run lint — clean
  • npm --prefix app run test — full suite green: 66 files / 671 tests
    passed
    (includes realmask.repro.test.ts, unaffected by this change)
  • New/updated unit tests (literal-pinned, not derived from the code under
    test):
    • useOwnshipGps.test.ts — does not subscribe while disabled; subscribes
      and 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; dismissHint hides without re-touching storage.
    • OwnshipMarker.test.tsx (BoatMarker mocked) — renders nothing when
      fix is null; renders BoatMarker with the fix's fields forwarded
      verbatim when present; falls back headingToSteerDeg to 0 absent a
      device COG.
    • LiveView.test.tsx — new test asserts BoatMarker (mocked) is never
      called
      , 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 against DEFAULT_SETTINGS;
      toggling calls onChange with showOwnship: true immediately; help
      text carries the "not a navigation device" caveat.
    • AppState.test.tsxshowOwnship defaults to 'false'; a patch
      persists to IndexedDB and survives a provider remount.
    • gpsHint.test.tsclaimGpsHintOnce() pinned against the literal
      sc-gps-hint-shown storage key LiveView's own tests already assert
      against (proves the two consumers share one claim, not two).
  • Real-browser smoke (dev server, port 5177, Playwright MCP):
    • "Show my position" checkbox present in Settings → Erweitert, unchecked
      by default.
    • Toggled on with a stubbed navigator.geolocation.sc-boat-marker
      appears on the map with no plan loaded, correctly rotated to the
      fake COG. Screenshot confirms visually.
    • Toggled back off → marker disappears (querySelector returns null).
    • Planned a real route (Langballigau → Sønderborg, deterministic wind
      fixture) and activated Live View with the toggle OFF: a fix arrives, but
      confirmed zero .sc-boat-marker elements (proves Live View alone no
      longer renders one).
    • The Live-active + toggle-ON combined check (proving exactly one marker)
      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 — the
      documented 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 clean
      real-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.
    • No console errors (only benign headless-GPU WebGL performance warnings,
      unrelated to this change).
  • Wind fixture churn from the browser pass restored
    (git status clean before commit).

Deviations / notes

  • None from the spec addendum. geolocation.ts and BoatMarker.tsx are
    byte-unchanged, as required.
  • db.ts/AppState.tsx required NO code changes — Settings persistence
    is 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 left
    untouched — it exists to recap routing-relevant settings, and
    showOwnship doesn't affect route calculation.

Part of #25.

Patrick Kuhn and others added 2 commits July 22, 2026 20:37
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
@DocGerd

DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

Self-review — approved (one deferred minor)

Reviewed with independent re-verification (tsc -b --noEmit clean, eslint src clean, all 8 touched/new test files re-run: 61/61 pass in the PR's worktree at 9ce55ae).

Minor, non-blocking (deferred to a follow-up): when settings.showOwnship is on AND Live View tracking is simultaneously active, two independent navigator.geolocation.watchPosition subscriptions run concurrently (useOwnshipGps + Live View's own) — extra GPS/battery chatter, no functional bug (single BoatMarker render site verified structurally; no double marker), not required by the spec addendum. Consolidating to a shared GPS provider would touch Live View's working route-following, so it's deferred rather than risked here.

Everything else verified clean: single BoatMarker render site (OwnshipMarker.tsx only; LiveView.tsx no longer imports it); Live View route-following byte-identical apart from marker removal; Settings.showOwnship default false, persists through IndexedDB; services/geolocation.ts + BoatMarker.tsx byte-unchanged; MsgKey parity holds; "aid, not a navigation device" caveat retained; new tests pin literals independent of the code under test. AIS is deliberately deferred (spec-recorded) — this PR is Part of #25, not Closes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant