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.
- 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.
- 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.
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
- 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
- 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
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>
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>
Timeline now shows the driving distance and duration between every
consecutive pair of stops, taken from real OSRM road routes (not Grok
estimates). Top stat bar and per-day headers also use the live totals
so they update immediately as the itinerary changes.
Grok now returns 1-3 alternative picks for each Supercharger and
hotel stop, each with deltaKm/deltaMin vs the chosen pick and a short
reason explaining the trade-off. The Swap (n) button on each card
expands an inline list of alternatives; clicking one swaps the stop
in place. The previously-chosen stop is kept in the alternatives
list with inverted deltas so the user can swap back. The map +
polylines + stat bar all recompute automatically.
Other tweaks:
- haversine fallback when OSRM is unreachable so legs still show
approximate metrics in offline / degraded mode.
- Leg geometry storage moves from raw polyline[] to typed Leg[] with
per-leg distance/duration/fromId/toId.
- Stop schema gains alternatives[]; client normalization filters out
alternatives missing lat/lng.
- Day cards widened (340 → 360px); timeline pane grew 280 → 340px
with vertical scroll so swap panels don't clip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- 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.