Skip to content

feat: AIS live traffic overlay on the Live tab (#25)#145

Merged
DocGerd merged 10 commits into
developfrom
feat/25-ais-overlay
Jul 23, 2026
Merged

feat: AIS live traffic overlay on the Live tab (#25)#145
DocGerd merged 10 commits into
developfrom
feat/25-ais-overlay

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Implements the reviewed plan docs/superpowers/plans/2026-07-23-ais-traffic-overlay.md (spec: docs/superpowers/specs/2026-07-23-ais-traffic-overlay-design.md): a live AIS traffic overlay on the Live tab, sourced browser-direct from aisstream.io over a BYOK WebSocket.

What shipped (one commit per plan task)

  • Settings — optional aisApiKey / ownMmsi on Settings (absent = feature off, no migration) + isValidMmsi (9-digit string, leading zeros preserved).
  • AisStreamClient (services/aisStream.ts) — pure, React-free connection state machine: subscription/bbox builders, sentinel-aware message parsing (heading 511, SOG 102.3, COG 360), full-jitter capped-exponential backoff, terminal keyError state; injectable socket factory + timers, only browserAisSocket touches a real WebSocket.
  • Target store (lib/aisTargets.ts) — MMSI-keyed merge of PositionReport + ShipStaticData, ownship dropped at ingest, fresh < 3 min / stale 3–10 min / dropped > 10 min; explicit-clock pure functions.
  • useAisTraffic — component-local hook (useOwnshipGps discipline): target Map in a ref, ≤1 Hz snapshot publish + sweep, key/online/visible/bbox gates, five-state UI status.
  • Rendering — shared lib/projectionVector.ts (reused by Live view: ownship 6-minute COG/SOG projection vector (parity with AIS target vectors) #141), lib/aisGeoJson.ts feature/popup builders, AisLayer (one GeoJSON source, three declutter-tiered layers anchored below the route stack, themed .ais-popup).
  • Options UI — AIS group (key + MMSI fields, inline MMSI validation, privacy help), all strings in both dicts.
  • Live-tab wiringAisTraffic controller (debounced padded-viewport subscription, status chip overlay), AIS_VESSEL_LAYER registered in INTERACTIVE_MAP_LAYER_IDS.
  • Live-verified auth semantics fix — see below.

Test counts

  • 880 unit tests passing (was 877 on develop; +36 new across mmsi 7, aisStream 23, aisTargets 8, useAisTraffic 10, projectionVector 3, aisGeoJson 9, AisTraffic chip 6, OptionsPanel +5, db.test +1; net +36 incl. shared-file counts). Full gate: lint ✓, typecheck ✓, npm test 86 files / 880 ✓, build ✓.
  • E2E smoke: plan.spec.ts, layout.spec.ts, offline.spec.ts all passing (no key configured in e2e → zero AIS network; wind fixture restored, not committed).

Live pass (owner key, real aisstream.io, dev server + Playwright)

  • 10+ real vessels in the Flensburg Fjord / Danish South Sea viewport: green arrows rotated to heading (COG fallback), neutral dot for a course-less target, COG vectors only on movers (6 of 10 — exact match to SOG>0 ∧ course known), name labels at zoom ≥ 11, MMSI label fallback for an unnamed target.
  • Status chip cycles connecting…live · N Schiffe; layer order verified from the live style: depth → harbors → seamarks → AIS (vectors/vessels/labels) → route stack; ownship is a DOM marker (always on top).
  • Popup verified in BOTH themes (light rgb(251,251,252) / dark rgb(13,14,16) chrome incl. both tip borders): name/MMSI/type/SOG/COG/age rows + disclaimer.
  • ownMmsi set to a live vessel's MMSI → that vessel never enters the overlay while others keep arriving.
  • Offline → chip AIS offline, socket closed, zero retries; back online → exactly one new socket, live again with no user action.
  • No key → chip off-hint, zero sockets created (feature fully inert).
  • Wrong key (live finding): aisstream sends NO {"error": …} frame — the socket accepts the subscription, then closes bare (1006, empty reason) on every reconnect, which the pinned error-frame rule alone would retry forever. Applied the plan's pre-authorized adjustment (plan §Task 2 note / §Notes): three consecutive subscribed-then-closed-with-zero-messages cycles are promoted to the existing terminal keyError state (unopened connect failures and post-message closes stay transient; error-frame path kept as belt-and-braces). Re-verified live: chip AIS: API-Schlüssel prüfen, no retry storm (socket count static over 20+ s), and a corrected key recovers to live.

Deviations from the plan (all minimal, reported)

  1. useAisTraffic: the plan's render-body ref write + setClientStatus('closed') inside the lifecycle effect trip the repo's react-hooks lint rules — replaced with an effect-synced ref and a render-time derived effectiveStatus (the useOwnshipGps derived-fix precedent); observable status mapping identical, all 10 plan tests pass unchanged.
  2. aisStream keyError promotion (above) — the plan's pinned live-adjustment path, executed.
  3. Plan's Task 8 named live.spec.ts; no such spec exists — ran layout.spec.ts + offline.spec.ts (the specs covering the Live tab/offline) plus plan.spec.ts instead.
  4. One pinned literal in the new backoff-reset test corrected after hand re-derivation (attempt counter resets only on a received message → 4th delay is 1000, not 500).

Closes #25

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXvHWVP7iALk2rScg3CtkT

Patrick Kuhn added 9 commits July 23, 2026 20:49
Live verification against aisstream.io showed a bad key produces NO error
frame: the socket accepts the subscription, then closes bare (1006, empty
reason) on every reconnect. Applies the plan's pinned adjustment: three
consecutive subscribed-then-closed cycles with zero inbound messages route
to the existing terminal keyError state (no retry storm); unopened connect
failures and post-message closes stay transient.
Comment thread app/src/services/aisStream.ts Outdated
…ch keyError (#25)

Review F1 (PR #145): earlyCloses previously counted ANY subscribed-but-silent
close and reset only on an inbound message, so a valid key over a vessel-empty
bbox (zero traffic) plus 3 network drops hit terminal keyError. Now each
connect arms a 30 s auth-stability timer (injected setTimer): if the socket is
still open when it fires, the key is proven (aisstream kills bad-key sockets
within ~a second) and earlyCloses/attempt reset; once proven, silent closes
are always transient. Timer cleared on close/stop/error-frame (leak-probed in
tests).
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.

AIS: live online traffic overlay with ownship recognition

1 participant