Skip to content

feat: map view with live vehicles (prototype) - #31

Draft
i11v wants to merge 15 commits into
mainfrom
worktree-map-view
Draft

i11v wants to merge 15 commits into
mainfrom
worktree-map-view

Conversation

@i11v

@i11v i11v commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Map view with live vehicles (prototype)

A new /map view showing vehicles moving along their lines around the closest stop to the user.

What it does

  • Centers on the closest stop to the user's location with margin so approaching transport is visible; city overview fallback when geolocation is unavailable, with auto-switch as location changes plus a recenter button.
  • Renders only the lines available at the focused stop (route shapes drawn from build-time geometry assets).
  • Live vehicle positions pushed over the existing WebSocket/Durable Object channel — one shared city-wide upstream fetch per 5s tick, filtered per-subscriber by route, rAF-interpolated marker movement.
  • Tap a platform → bottom sheet over the map with a pin-to-board action (opens the platform view).
  • Kind filters (tram / bus / train / metro …) persisted in localStorage; metro unchecked by default.

Architecture

  • Contract: VehiclePosition wire schema, SubscribeVehicles/VehiclesUpdate protocol messages, stop index bumped to v2 (platform coords + per-node routes) deploying atomically with the bundle.
  • Build: GTFS join emits hashed routes-*.json and per-route shapes/*.json assets (Douglas–Peucker simplified). Verified: 8490 stops, 837 routes.
  • Worker: VehicleGateway (4.5s cache, single-flight, rate-limit cooldown, never-fails-stale) alongside the departure gateway on one shared Golemio client + rate limiter; DO cadence split into a pure, fully-tested planTick.
  • Web: MapLibre + keyless OpenFreeMap tiles, isolated in a lazy map-*.js chunk (278 KB gz) separate from the 300 KB entry. Presentation kept behind stateless layers.ts helpers for a later design pass.

Prototype notes (accepted gaps)

Per the agreed prototype framing (UI + architecture), the following are intentionally deferred:

  • UI is unstyled/minimal — a separate Claude Design pass is planned after playing with the prototype.
  • No service-worker caching for /data/routes-*.json and /data/shapes/*.json — map assets are network-only for now.
  • No browser/visual verification was possible in this environment; interaction paths are covered by unit tests only.
  • Remaining deferred Minors are listed in the SDD progress ledger.

Verification

  • bun run typecheckbun run lintbun run test ✅ (130/130) bun run build

Spec: docs/superpowers/specs/2026-07-20-map-view-design.md
Plan: docs/superpowers/plans/2026-07-20-map-view.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ

i11v and others added 15 commits July 20, 2026 22:59
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
…route asset schemas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
Add a Schema subset for GET /v2/vehiclepositions and toVehicles() to
map Golemio's vehicle-position features to the contract's
VehiclePosition shape, dropping canceled runs. Add fetchVehicles to
GolemioClient, mirroring fetchBoards (token header, 10s timeout,
429/non-2xx handling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
Sibling fetchBoards had test coverage exercising URL/query params and
429/401 error mapping through the mocked HttpClient; fetchVehicles had
none. Mirror the same fixtures and idioms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
Extract a pure planTick cadence helper (packages/worker/src/do/cadence.ts)
and rework the session DO's poll loop to drive both departures (15s) and
vehicles (5s) from it, storing subscribed routes and the next boards-due
timestamp. The message handler gains SubscribeVehicles/UnsubscribeVehicles,
broadcasting VehiclesUpdate filtered to the subscribed route set. Each
fetch keeps its own mid-flight-change frame-drop guard and broad catch so
one failing fetch can't suppress the other or freeze the alarm cadence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
…cker

Extend useDepartures with a second vehicleRoutes param, applying
VehiclesUpdate frames into vehicles/vehiclesAt and subscribing/
unsubscribing over the socket with the same encoded-frame dedupe as
the board selectors. Add loadMapKinds/saveMapKinds (default: all
kinds but metro) and mapKindsStore/toggleMapKind for the map's kind
filters, plus closestEntry (nearest StopIndexEntry by haversine) and
lerp/lerpAngle (shortest-arc bearing interpolation) for the map view.
Also fixes StopIndexEntry/StopPlatform test fixtures for the v2
routes/lat/lon fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
…open

The board subscription's ws.onopen handler always sends Subscribe or
Unsubscribe explicitly, since the session DO outlives individual sockets
and silence would leave a stale subscription polling server-side. The
vehicles frame added in the prior commit only sent SubscribeVehicles when
vehicleRoutes was non-empty, never UnsubscribeVehicles when empty — so a
tab that left the map view and reconnected (same session id, same DO with
persisted vehicleRoutes) would leave a stale vehicles subscription polling
every 5s. Make it symmetric with the board subscription.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
DeparturesUpdate and VehiclesUpdate each overwrote the shared status/reason
from their own message, so a healthy message on one stream could flip a
degraded other stream back to "live". Track boardsDegraded/vehiclesDegraded
separately and derive status/reason from both. Also extract a pure
decodeMapKinds codec from loadMapKinds so round-trip and unknown-value
filtering can be unit tested under vitest's node environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
…ess gate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGdH46ajdt2QkihjSHUdmZ
@github-actions

Copy link
Copy Markdown

🚀 Preview deployed: https://preview-31.tablo.run
fallback: https://tablo-pr-31.i11v.workers.dev

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