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>
Switching between Fastest / Scenic / Cheapest used to require a fresh
Grok call every time (~90s). Now each variant's itinerary + OSRM
legs are cached the moment they're computed, so:
- Switching back to a previously-viewed variant is instant — no Grok
round-trip, just a state swap. A toast reports "(cached)" so the
user knows nothing was refetched.
- The variant strip gains a Compare (n) button that lights up once
at least two variants are cached. Toggling it overlays the other
cached variants on the map as dashed lines in their tone colour
(green for Scenic, blue for Cheapest), while the selected variant
stays solid red on top.
- Map legend swaps to a variant-key list while Compare is active so
the user can read which dashed line is which.
- The cache is also kept fresh as soon as legs[] finishes computing
for the active variant — so toggling Compare immediately after a
fresh plan still shows real routes, not stale ones.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the simple <select> vehicle chip with a proper anchored
glass panel matching Direction B.
- Full Tesla model catalogue (S / 3 / Y / X / Cybertruck) with all
trims (Standard / Long Range RWD / Long Range AWD / Performance /
Plaid / Cyberbeast) and per-trim range, peak kW, 0-100, top speed,
and badges (Most popular, Best range, Performance).
- Vehicle chip in the top bar now shows the model + abbreviated trim
("Model Y LR AWD") with range underneath, opens the panel anchored
to the chip's bounding rect.
- Panel header has a starting-charge slider (10-100%, snaps to 5).
Each trim shows "X km now" (= range × charge%) in green so the
user sees the live impact of picking a different trim or charge
level.
- Model groups collapse / expand independently; the currently-
selected model expands by default.
- Picking a trim updates the chip everywhere; closes the panel.
- Backdrop click and Esc both dismiss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new modals wired into the planner with real interactions:
CustomiseStopModal (4 tabs)
- Tab rail (Charger / Overnight / Duration / Things to do / Detour)
auto-hides tabs that don't apply (e.g. Charger only for chargers,
Overnight only for hotels, Things to do only when nearby[] exists).
- Charger tab: radio-style picker over stop.chargerOptions with
kW/stalls/€/network/detour badge per option.
- Overnight tab: lists the current hotel + stop.alternatives as
swappable options with Choose buttons.
- Duration tab: 48px figure + range slider (10–120 min, snaps to
presets) with arrive/leave battery % shown live; preset grid
(Quick top-up / Coffee / Sit-down lunch / Explore / Full charge /
Skip).
- Things to do tab: checkbox grid over stop.nearby[] for picking what
you want to do at the stop.
- Apply Changes commits chargeMinutes + durationMin back into the
itinerary via a new updateStop() handler.
AddDetourOverlay
- Spotlight-style search bar (autofocus, esc to close) with a
curated POPULAR_DETOURS list (York Minster, Lake District,
Hadrian's Wall, Beaune town centre, etc.). Insert button adds the
detour as a new stop in the current day via a new insertDetour()
handler. The map polyline / leg metrics recompute automatically.
GpxExportModal
- Real downloadable export — GPX (XML), KML (Google Earth), CSV.
- Live preview pane shows the generated file, with line count + KB
in the footer. Copy puts the file content on the clipboard;
Download .gpx writes it as a Blob with the right MIME type and
triggers a real browser download. Filename derived from the trip's
first/last stop.
- GPX include-toggles for stop notes and nearby places.
- Empty-trip state shows a friendly message instead of an empty pane.
Plumbing
- Modal state lifted into the planner as { kind, stopId? } | null.
- Top bar Export button, the rail's Add stop button, and the bottom
Add detour link all open the relevant modal.
- The Customise stop button inside the expanded stop card now opens
the Customise modal for that stop.
- ModalShell handles backdrop, escape-to-close, scroll-clamp, and
optional header/footer/subtitle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- NightBlock component: dashed-blue panel that appears under the last
stop of any day (when it's a hotel or supercharger) and before the
next day starts, showing "Sleep in car at services" or the hotel
name with a "Hotel options" button that opens the stop's expansion
to its overnight swap section.
- Drag-to-reorder: every stop card is draggable; dropping on another
card moves the dragged stop into that position (and into that
day if you drag across day boundaries). Order numbers are
renormalised after each drop; the legs + map polylines + day totals
recompute automatically.
- Origin/Destination editable replan: editing the origin or
destination input and pressing Enter or blurring sends a "Replan
the trip from {origin} to {destination}" chat message. Auto-
populating from the current itinerary does not trigger a replan;
only real user edits do.
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>
Implements the layout-shell phase of the Direction B redesign from the
Anthropic Design handoff bundle.
Visual layer:
- Geist font + new --gd-* CSS token palette (panel/border/text scales,
green/amber/blue/purple accents alongside the red).
- Top bar with brand chip, origin/destination inputs, chat composer
with chip-based refinements, vehicle selector chip, Export/Share.
- Body split: map-left flex-1, stops rail 540px right (replaces the
prior chat-sidebar + bottom-timeline layout).
- Stops rail: trip summary header (stops · days · km · drive · charge),
sticky day headers with date label + per-day km/drive/charge totals,
icon-led stop cards with colored type-icon tile (charge/sleep/hotel/
attraction/cafe/viewpoint colour-coded), combo pill, description,
charge/arrive meta inline.
- Leg row between cards (km · drive time), still computed from real
OSRM routes.
- Click stop to expand: 4-stat grid (Charge/Arrive/Leave/Cost),
amenity chip cloud, alternative-swap list (preserving the existing
delta swap mechanic), Customise/Remove actions.
- Map ↔ rail bridge: clicking a card flies the map to that pin,
hovering either highlights the other. Pins are custom Leaflet
divIcons coloured per stop type with active/hover scaling.
- Empty state + quick-prompt buttons in the rail.
Not in Phase 1 (deferred):
- Variant strip (Fastest/Scenic/Cheapest) — needs backend prompt work.
- While-here food/do/see tabs — needs backend nearby[] data.
- Customise / Add-detour / GPX modals.
- Vehicle selector trim panel (still a native select for now).
- Drag-to-reorder, in-trip/compare/shared screens.
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.