Commit Graph

12 Commits

Author SHA1 Message Date
tony 9f489d1fb6 fix(security): tighten pair-code defences per follow-up review
- Per-code failure threshold dropped 5 → 3. After 3 wrong guesses
  during a code's 5-min TTL, the code is burned and the owner must
  re-mint. Still tolerates the occasional typo while collapsing the
  brute-force window further.
- Cap concurrent active codes to 1: minting a new code clears any
  prior code. Single-user app, no need to juggle multiple.
- Add a global brute-force lockout: after 100 failed attempts in
  any 5-minute window, the redeem endpoint returns 429 endpoint_locked
  for the next 10 minutes, regardless of source IP. Logged at error
  level so the operator notices.
2026-06-01 10:21:03 +01:00
tony 36118b4a4f fix(security): harden owner pair-code against brute-force
- Set express trust proxy=1 so req.ip is the real client (one Dokku
  nginx hop). Drop the X-Forwarded-For fallback — it was client-spoofable.
- Add a global redeem rate limit (30/min across all IPs) so rotating
  proxies can't outpace the per-IP limiter.
- Every failed redeem now decrements life from all active pair codes
  via a shared failures counter. After 5 wrong guesses during a code's
  5-minute TTL, the owner's real code gets burned and they must re-mint.
  This makes the 20-bit entropy of a 6-digit code defensible: an
  attacker doesn't know which code is real and can't probe long enough
  to find it before triggering the burn.
2026-06-01 10:16:30 +01:00
tony 90a4459e63 feat: 6-digit pair code login + responsive layout for narrow car browsers
Pair code:
- POST /api/auth/owner/pair-code (owner-gated) mints a single-use,
  5-minute, 6-digit code held in memory.
- POST /api/auth/owner/pair-redeem swaps the code for an owner cookie,
  with basic IP-based rate limiting (max 8 attempts/min).
- OwnerLoginModal gains a tab: "Use 6-digit code" (default) vs the
  long secret. The PIN input is big, monospaced, numeric-only.
- Small smartphone icon next to the connected Tesla chip opens
  OwnerPairCodeDisplay, which shows the code in 56px type with a
  live countdown so it's tappable on the touchscreen of the other
  device while you stare at it.

Responsive layout:
- TopBar wraps when narrower than the chip row. minWidth on OD strip
  drops 380→280 and chat composer drops 360→240 so they stack cleanly.
- Main body switches to flex-col below the lg breakpoint (Tailwind
  1024px), so the map sits on top of the stops rail on Tesla 3/Y
  portrait and any window not maximised. Map gets 40vh min-height
  so it never disappears.
2026-06-01 10:10:30 +01:00
tony 381eb18cd3 feat(tesla): collapse OD strip when connected, sync with in-car nav destination
- Server: include active_route_destination (lat/lng/eta) from drive_state.
- When Tesla is connected, the Origin → Destination strip collapses to a
  single ConnectedTripStrip: "From car · Where to?". The origin is implicit
  (the car's GPS), the manual From input + crosshair button disappear.
- If Tesla nav already has a destination, it auto-fills as the trip
  destination; if the user has typed something else, an inline "Use Tesla
  nav" button offers a one-tap swap.
- Mocks: driving + charging scenarios include an activeRoute so the flow
  is testable end-to-end via ?mockTesla=driving / ?mockTesla=charging.
2026-05-31 22:58:42 +01:00
tony f793b526aa fix(security): owner auth gate, OAuth state cookie binding, 0600 token perms
- Add OWNER_SECRET-based session: signed HMAC cookie, /api/auth/owner login,
  requireOwner middleware. All Tesla routes refuse 401 without it.
- Bind OAuth state to a SameSite=Lax httpOnly cookie at /start, validate
  match in /callback with constant-time compare. Refuses unmatched callbacks.
- Token store now mkdir 0700, writeFile + rename atomic, mode 0600 with
  defensive chmod. Owner-only on disk.
- VIN masked to last 4 in responses; partner-register no longer echoes raw
  Tesla body to clients; coord bounds checked on send-to-nav.
- Client: useTesla also tracks owner status; Connect Tesla button opens an
  OwnerLoginModal when not authenticated, then continues to Tesla OAuth.

Conscious deferrals:
- Explicit CSRF tokens on POST routes: mitigated by SameSite=Lax cookies
  + same-origin CORS. Will revisit if cross-origin clients land.
- At-rest token encryption: deferred for single-user app; tokens are on a
  0700 Dokku volume readable only by the app uid. Will add AES-GCM if we
  multi-tenant.
2026-05-31 22:32:22 +01:00
tony d705669dda feat(tesla): real Fleet API integration — OAuth, vehicle state, send-to-nav
Server:
- teslaTokenStore: file-backed token store at /app/data/tesla-tokens.json
- teslaClient: OAuth (authorize/code-exchange/refresh), Fleet API GET/POST,
  listVehicles, getVehicleData, wake, sendNavigationRequest, getAppToken,
  registerPartnerAccount; auto-rotates refresh tokens 60s before expiry
- /api/tesla/status, /api/auth/tesla/start, /api/auth/tesla/callback,
  /api/tesla/state, /api/tesla/wake, /api/tesla/send-to-nav,
  /api/tesla/disconnect, /api/tesla/register-partner
- State includes battery, range (mi→km), charging power/eta, GPS,
  shift_state, model/trim auto-detected from vehicle_config

Client:
- useTesla hook: auto-fetches status, polls live state every 60s when connected
- Connect Tesla chip in TopBar; on connect shows battery% + range
- Per-stop "Send to Tesla nav" button (only when Tesla connected)
- "Use my location" button prefers vehicle GPS over browser geolocation
- Auto-detects model/trim from Tesla and updates the vehicle picker
- When in-car AND Tesla connected: auto-fills origin from car's GPS,
  hides the vehicle chip (we know the car), hides GPX export and Share
2026-05-31 22:23:38 +01:00
tony cff52b4b9e feat: travel dates + sea-crossing chooser, Tesla in-car polish, Fleet API stub
- Travel dates: TopBar chip + popover (outbound/return/travellers); sent to
  Grok prompt; itinerary.needsTravelDates drives a nudge banner; cache and
  prefetch ledger invalidate when dates change
- Sea crossings: CrossingOption schema (Eurotunnel, DFDS, P&O, Brittany,
  Stena Line); CrossingSwapBlock under tunnel/ferry/crossing stops with
  trip-impact deltas and Book links; prompt requires 3-5 real options for
  every UK ↔ mainland route; picking a crossing triggers silent re-plan
- Tesla in-car polish: UA + heuristic detection sets <html class="incar">;
  CSS overrides kill backdrop-filter, scale fonts, enforce 44px tap targets,
  disable hover flicker; geolocation + reverse geocode + crosshair button
  inside the From input; up/down arrow reorder buttons replace touch-broken
  HTML5 drag-and-drop
- Tesla Fleet API stub: /.well-known/appspecific/com.tesla.3p.public-key.pem
  served from TESLA_FLEET_PUBLIC_KEY for partner domain verification;
  OAuth callback + vehicle_data stub return 503 until partner approval
- Dockerfile + .dockerignore for Dokku deployment; server now serves
  client/dist in production
2026-05-31 21:38:27 +01:00
tony 88fc86dc32 feat: faster variant switching, live Grok thoughts, accurate trip endpoints
- Bump z-index on vehicle selector and modals (z-50 sat below Leaflet panes)
- Prefetch the other route variants in the background as soon as the first
  trip lands; switching now hits the cache and is near-instant
- Surface Grok's streaming thoughts to the UI: glassy overlay on the empty
  map + sidebar callout, with skeleton shimmer until the first thought
- Thread explicit origin/destination from the TopBar through to the prompt
  as a ground-truth block; harden rules so the first/last stops match the
  user's actual endpoints and cross-Channel trips include both sides
2026-05-31 17:14:04 +01:00
tony ed64712525 feat: Phase 5 — live streaming trip building via SSE
Grok now drives the trip rendering in real time instead of dumping
the full result after ~90 seconds.

Backend
- GrokHeadlessClient gains a chatStream() async generator that spawns
  grok with --output-format streaming-json (NDJSON of {type,data}
  events), buffers the "text" tokens, and emits partial events as the
  buffer becomes parseable.
- tryPartialJsonParse — lenient JSON repair: walks the buffer once,
  closes structures in stack order, drops in-progress strings and
  dangling keys, returns whatever object is currently consistent.
  Hard-tested with progressive slicing of a multi-stop itinerary.
- New SSE endpoint POST /api/chat/stream with events: open / thinking
  / partial / done / error. Uses res.on('close') + writableEnded as a
  reliable client-disconnect signal (req.on('close') fires in Express
  5 once the body is consumed, which was killing the grok child).

Frontend
- sendMessage swaps to fetch+ReadableStream against /api/chat/stream
  and parses SSE blocks. Each partial event runs a fast synchronous
  normalizePartialItinerary (no Nominatim — drops stops missing
  lat/lng so partial render doesn't block on geocoding).
- The done event runs the full async normalizer for the final pass
  and caches the result per variant.
- Stops, day cards, map markers, polylines, the variant strip, and
  the trip summary all update progressively as Grok writes each stop.

Verified with a London → Edinburgh prompt: 6 partial events landed
across the 76-second stream, with the rail filling in
"Baldock Services" → "+Grantham A1" → "+Premier Inn Newcastle"
→ "+Fort Kinnaird" before the final done event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:01:00 +01:00
tony f63af36451 feat: Phase 2 — variant strip, while-here, charger swap block
Adds the three big "options" UX wins from Direction B:

1. Route variants (Fastest / Scenic / Cheapest)
   - Grok prompt now returns a top-level variants[] summary with
     drive/charge/cost/distance/pros for each variant, plus a
     selectedVariant indicating which one the stops reflect.
   - VariantStrip renders under the top bar with selected-state
     styling, tone-coloured highlight (red/green/blue) on the most
     relevant stat, and 3-5 pros pills.
   - Clicking a variant fires /api/chat with selectedVariant=<id> so
     Grok re-plans with that variant's bias. A "switching" state
     disables the strip while the request is in flight.
   - The chat route accepts selectedVariant ('fast'|'scenic'|'cheap')
     and the GrokHeadlessClient threads it through both the local CLI
     and xAI API paths.

2. While here (food / do / see / shop / rest)
   - Every Supercharger, destination-charger and hotel stop now
     returns a nearby[] array with category/icon/name/detail.
   - Expanded stop card has tabs (All / Food / Do / See) — tabs
     auto-hide when no items in that category. Two-column grid of
     named places with walk-time + rating, e.g. "M&S Foodhall · 1 min
     walk · 4.3★ · sandwiches".

3. Charger swap block
   - Every charging stop now returns chargerOptions[] with the
     current pick + 1-3 alternatives at the same location, each with
     network (Tesla/Ionity/Allego/Fastned/BP Pulse), stalls, kW,
     pricePerKwh, detourMin and an optional badge (Faster/Cheaper/
     More stalls/Newer).
   - ChargerSwapBlock shows the current charger as a red-tinted
     header row that expands to reveal alternatives with stats and a
     Use button per row.

Renamed the existing AlternativeStop UI label from "alternative(s)"
to "location alternative(s)" so it's clear when the user is swapping
the stop *location* vs swapping the *charger at the same location*.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:14:15 +01:00
tony 89b24d4c34 feat: wire build/test infra, trips API, and enriched journey stops
- Add tsconfig.json (server) + client/tsconfig.{json,app.json,node.json}
  so typecheck and tsc -b actually work.
- Fix npm test to run Playwright (was running vitest on Playwright specs);
  typecheck now covers both server and client.
- Mount routes before app.listen, add error handler, mount optional
  @tonycodes/auth-express middleware when AUTH_SECRET is set.
- Add /api/trips (GET/POST/PATCH/DELETE) backed by an in-memory store
  that gracefully degrades when DATABASE_URL is unset.
- Add prisma/seed.ts skeleton and server/types/express.d.ts for req.auth.
- Rewrite Grok prompt for combo-aware planning: charge+eat,
  stay+destination-charging, eat+viewpoint, etc., with amenities,
  cuisine, priceLevel, duration, day titles and trip highlights.
- Extend Stop schema + normalization to preserve all enrichment fields.
- New StopCard component renders combo pill, description, meta row
  (charge / stop / battery / cuisine / £-level) and amenity icons;
  map popups show the same enriched detail; timeline gains day titles
  and a HIGHLIGHTS sidebar.
- Fix server TS errors (vehicle accepted as string | {name,rangeKm},
  JSON parse results typed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:32:53 +01:00
tony d516e93323 chore: initial checkpoint - Tesla Roadtrip planner
- Proactive Grok integration (xAI API + local CLI fallback)
- Real road routing via OSRM (no more bird's-eye lines)
- Heavy structured logging for fast iteration
- Strong sanitization + geocoding + ErrorBoundary (no black screens)
- Playwright E2E tests (API diagnostic + full UI flow)
- scripts/dev.sh for one-command startup
- Clean .env.example + documentation

This is a stable checkpoint before further prompt/UI refinement.
2026-05-15 19:24:35 +01:00