Legs:
- day.firstLeg (hotel -> first stop) is now a first-class leg object exactly
like retLeg/transferIn: own mode/dur/geometry, same leg-row rendering, same
click-to-edit L3 card (drag waypoint + walk/taxi toggle), same router +
sanity guards, same strip/budget/context accounting
- hotel rendered as a timeline NODE on every day: 'arrive HH:MM - drop bags'
on the arrival day (after the airport->hotel leg row), 'depart HH:MM'
(startMin minus the out-leg) on ordinary days; the arrival drop-off is a
stand-alone leg row instead of being embedded in the flight card
- arrival day: the first stop starts at hotel-arrival + out-leg; ordinary
days keep startMin = time AT the first stop (the out-leg happens before)
- out-leg suppressed when the day doesn't start at the base hotel: travel
days (city change, first stop > 25km away) and breakfast-at-hotel days
(< 120m); stale firstLeg objects self-heal
- set_start tool shifts arrLandingMin on arrival days so the whole chain
(hotel arrival, first stop, ...) follows the new landing time
Coordinates (verified on land via Nominatim + tile coastline scan):
- S14 Castillo San Diego -> Bodeguita promontory [10.4205,-75.5517]
- S16 Malecon de Bocagrande -> seawall [10.43545,-75.539]
- S18 Punta Canoa -> sandbar beach [10.4403,-75.530]
- S19 beachside lunch -> same sandbar [10.4402,-75.5294]
healCoordinates syncs saved plans from data.js on load
Transfers (airport<->hotel) were the app's weakest surface; this fixes the
long-standing cluster of bugs in one pass:
- saneRoute(): reject OSRM results implausible for the straight-line distance
(extract coverage holes once produced a 9h 'route' for a 5km taxi hop);
applied at every routePts call site
- healTransfers() on load: re-sync transfer endpoints against current hotel +
AIRPORTS table, re-sync stays[] against the day-base hotel objects (the
saved stays[] carried pre-coord-fix locations, and dayBase() resolves the
hotel FROM stays — every return leg was routed to the stale hotel), drop
implausible or endpoint-drifted stored routes so they re-route cleanly,
default a missing mode to car, and recover arrLandingMin from the booked
flight time
- the last-stop->hotel ride is now a first-class editable leg (retLeg): the
rail row opens the route card with drag + walk/taxi toggle, exactly like
stop-to-stop legs; previously auto-routed returns seed it
- startMin now actually tracks landing + drop-off (syncArrivalStart had a
null arrLandingMin guard that silently froze the day's start)
- applySavedPlan re-points day: days becomes a new object tree on restore,
and the current-day reference stayed on the stale base-plan day — the rail
rendered the base plan instead of the saved one until a day-tab click
- setDay enriches the opened day (idempotent), so a day opened from a saved
plan gets its legs (incl. the return leg) routed instead of sitting on
straight-line estimates
Also in this change set: correct LLM model name, keyword-router removal (chat
goes to the LLM), screenshot_map + relocate_stop tools for the UI LLM,
server-side plan store with localStorage migration, thinking toggle +
keep-alive warm-up, three-state LLM guard with proactive model load,
spatial /near /nearest /corridor SQL fixes, and the full data.js
coordinate correction for the Colombia demo.
Root cause: several Cartagena old-city landmarks in the data had latitudes
~4 km too high (≈10.46°N instead of ≈10.42°N), dropping them out into the
Caribbean bay — that's why Day 2 stops 2–5 rendered in the water. The chat
model couldn't fix them because its web search couldn't surface valid
coordinates for those specific buildings, and it (correctly) refused to move
them to arbitrary guessed locations.
Also found the CTG (Rafael Núñez) airport was misplaced ~8 km east onto the
Bocagrande area ([10.4622,-75.4385]); the real airport is [10.4425,-75.5131].
This inflated the Day 1 arrival drop-off to 313 min.
Changes:
- data.js: re-anchor 6 old-city stops (S5, S7–S10, S20) + the C2 candidate to
verified on-land coordinates (all snap <21 m to the OSRM extract); fix the
CTG airport in the CTG→SMR flight stations and the S22 stop.
- app.js: correct AIRPORTS.ctg (used by the arrival transfer) to the real
airport. Add healCoordinates() so a plan saved before this fix (still holding
the old in-the-bay points in the browser's localStorage) is re-anchored to
the corrected source on load — stops re-sync by id, arrival/departure airports
re-derived from the AIRPORTS table. Idempotent; user-swapped stops (candidate
ids) are left alone. Verified with a CDP test that corrupts the saved plan
(6 stops + airport) and confirms a reload self-heals them back.
All regression suites pass: coords 6/6, drop-off leg 17/17, smoke 0 errors,
rail structure 13/13, dates 7/7.
The mock can now work several trips at once: a trip picker in the
topbar, per-trip state in localStorage (edits + version history survive
switching), and a per-trip router key so each trip routes on its own
OSRM extract.
Colombia (Cartagena -> Santa Marta, 8 days) becomes the default working
trip, inside a new walking-profile OSRM instance:
- scripts/crop_pbf.py: streaming 3-pass PBF cropper (no PBF library;
the country file is too big for osmium extract -s complete_ways on
this box). Framing validated against osmium test fixtures and
osrm-extract. Drops relations (foot profile ignores restrictions).
- scripts/setup-osrm-colombia.sh: download -> crop (~4.6 MB) ->
extract/partition/customize with the official foot.lua -> serve
:5003. Verified: Getsemaní -> Castillo San Felipe 7.98 km / 96 min.
- server.js: ROUTERS map, ?router=colombia|northeast on /route,
/table, /router-status (per-router probe points).
- app.js: legs touching a transit stop are fixed (flight/taxi/bus via
M.multimodal), never re-routed; distance-0 router answers (both
points on one node) read as adjacent stops, not out-of-coverage.
- multi-hotel fix: day-scope map fit uses the stays covering THAT day,
so a two-city trip no longer fits the whole country per day.
Also: pre-cache-tiles.sh (Cartagena+Santa Marta z14-16, 2560 tiles),
tile cache write fix (Node rejects flags:'x'; buffer-then-write),
favicon, renderHotelMks load-order guard, tests updated for the new
default trip (all four puppeteer suites green, zero console errors).
- server.js: proxy /route, /table, /router-status to the local OSRM on :5000
- app.js: routeClient (polyline-5 decode, ordered route fetch); legs start as
straight-line estimates (conf 1) and are upgraded in the background to real
road geometry + duration (conf 3); out-of-coverage trips (Florence) fall
back gracefully with a 'estimate — outside router coverage' source
- L3 leg editor: dragging a waypoint now genuinely re-routes through the
router (was silently no-op — #app covered the map with pointer-events:auto,
so no map drag ever landed; #app is now pointer-events:none, children opt in)
- data.js: second demo city (Boston 14–16 Sep, inside the router's NE-US
extract) via ?city=boston; nudges + multimodal legs are now dataset-driven
- L0: Day/Trip scope toggle — per-day summary cards (pacing bar, stops,
end time, price, base) with click-to-jump; map fits the whole trip
- version history: commit() snapshots days+stays on every mutation; topbar
vN pill with undo/redo + dropdown timeline showing a diff label per version;
Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y; restore() re-enriches legs (snapshots may
predate route enrichment)
- offline: staged 'prepare for offline' card in the trip rail (itinerary →
POI details → vector tiles) that flips the topbar badge to '📦 offline ready'
- chat/scripts/nudges: Florence-specific strings generalized (stay name/dates,
vibes, pacing summary) so both demo cities read naturally