trips/mock/data.js
Greg Pomerantz 30f3dd9cbf mock: wire to the real local OSRM router + L0 trip view, version history, offline cues
- 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
2026-09-09 00:47:50 -04:00

526 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Fake data layer — same shapes the real API will return.
// Each stop: day, slot (mutual-exclusion group), state: planned | maybe | alt.
// Only one `planned` stop per slot per day; `alt`/`maybe` don't count against
// the day's time budget.
//
// Two demo cities: Florence (rich itinerary demo) and Boston (inside the
// local OSRM extract, so legs are routed on the real road network — see
// server.js /route proxy and app.js routeClient). ?city=florence|boston
const FLORENCE = {
trip: {
id: 'trip-2026-09',
version: 1,
title: 'Florence 1215 Sep',
// stays, not a single hotel: {hotel, place, checkIn, checkOut, state}.
// Days resolve their anchor per date; >1 candidate stay covering the
// same nights → worst-case anchoring for those nights.
// Multi-city trips = more stays, each anchoring its own dates.
stays: [
{ id: 'ST1', name: 'Hotel Palagio', at: [43.7684, 11.2566], place: 'Florence',
checkIn: '2026-09-12', checkOut: '2026-09-15', state: 'booked', price: 180 },
],
places: [
{ id: 'PL1', name: 'Florence', bbox: [43.73, 11.21, 43.80, 11.29],
center: [43.769, 11.255], vibe: 'art + food, relaxed pace',
dates: ['2026-09-12', '2026-09-15'] }
],
bookings: [
{ type: 'hotel', name: 'Hotel Palagio', ref: 'H-77821',
dates: '1215 Sep', where: [43.7684, 11.2566],
status: 'booked', source: 'user_booking' },
{ type: 'train', name: 'Frecciarossa 9512', ref: 'F-55812',
from: 'Firenze SMN', to: 'Pisa Centrale', date: '2026-09-15',
depart: '09:12', arrive: '09:58', status: 'booked', source: 'user_booking',
stations: { from: [43.7726, 11.2597], to: [43.7157, 10.3922] } }
]
},
days: [
{ id: 'D1', date: '2026-09-12', label: 'Day 1 · Fri 12',
startMin: 8 * 60, wakingHours: 11,
base: { name: 'Hotel Palagio', at: [43.7684, 11.2566] },
legs: [],
stops: [
{ id: 'S1', name: 'Breakfast at the hotel', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [43.7684, 11.2566], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'triptadvisor.com' } },
{ id: 'S2', name: 'Palazzo Pitti', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.palazzopittiturin.it',
at: [43.7639, 11.2518], dur: 60, durConf: 5, // user override (sticky)
suggested: { value: 150, conf: 2, src: 'wikivoyage.org' },
price: { amount: 14, cur: '€', conf: 2, src: 'uffizi.it' },
note: 'guides suggest 23 h', img: ['🏛️', 'Palazzo Pitti', 'linear-gradient(135deg,#b5533c,#7a3a2c)',
'https://thumb.wikimedia.org/wikipedia/commons/thumb/7/72/Palacio_Pitti%2C_Florencia%2C_Italia%2C_2022-09-18%2C_DD_197.jpg/1280px-Palacio_Pitti%2C_Florencia%2C_Italia%2C_2022-09-18%2C_DD_197.jpg'],
detail: {
summary: 'The Medici familys riverside palazzo: the Galleria Palatina (Portraiture, the 500 Room) and the modern-art Galleria dArte Moderna. Rooms are small and crowded in peak season — the apartments alone run 1.52 h if you linger, ~1 h for the highlights. September afternoons are the sweet spot: light is good for photos and lines thin after 16:00.',
links: [
{ label: 'Official site', href: 'https://www.palazzopittiturin.it' },
{ label: 'Tickets', href: 'https://www.uffizi.it/en/visit/tickets' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Attraction_Review-g187791-d183726-Reviews-Palazzo_Pitti-Florence_Tuscany.html' },
{ label: 'Reddit r/Florence', href: 'https://www.reddit.com/r/Florence/search/?q=palazzo+pitti&restrict_sr=1' }
] } },
{ id: 'S4', name: 'Lunch — Trattoria Mario', kind: 'meal', mealKind: 'lunch',
slot: 'lunch', state: 'planned', url: 'https://www.trattoriamario.it',
at: [43.7667, 11.2544], dur: 45, durConf: 3,
suggested: { value: 90, conf: 2, src: 'trattoriamario.it' },
price: { amount: 25, cur: '€', conf: 2, src: 'trattoriamario.it' },
img: ['🍝', 'Trattoria Mario', 'linear-gradient(135deg,#d08b3c,#9c5f1e)',
'https://upload.wikimedia.org/wikipedia/commons/1/12/Bistecca_alla_fiorentina-01.jpg'],
detail: {
summary: 'A no-frills Florentine kitchen on Via dei Neri, open since 1954. Counter seating, short menu (bistecca, lampredotto, tripe soup), and the queue is real — arriving as lunch opens (12:00) is the difference between 10 minutes and 40. Budget ~1 h including eating.',
links: [
{ label: 'Official site', href: 'https://www.trattoriamario.it' },
{ label: 'Menu', href: 'https://www.trattoriamario.it/menu' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Restaurant_Review-g187791-d310951-Reviews-Trattoria_Mario-Florence_Tuscany.html' },
{ label: 'Reddit r/food', href: 'https://www.reddit.com/r/food/search/?q=trattoria+mario+florence&restrict_sr=1' }
] } },
{ id: 'S5', name: 'Boboli Gardens', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.boboli.it',
at: [43.7579, 11.2463], dur: 120, durConf: 3,
suggested: { value: 120, conf: 2, src: 'boboli.it' },
price: { amount: 10, cur: '€', conf: 2, src: 'uffizi.it' },
img: ['🌿', 'Boboli Gardens', 'linear-gradient(135deg,#4f9e63,#2c6b40)',
'https://thumb.wikimedia.org/wikipedia/commons/thumb/d/d2/Obelisco%2C_Boboli_Gardens%2C_Florence_%2826408174180%29.jpg/1280px-Obelisco%2C_Boboli_Gardens%2C_Florence_%2826408174180%29.jpg'],
detail: {
summary: 'The Medicis formal garden behind Pitti — statues, the Apollo fountain, a hilltop view over the city. The full loop with the Accademia annex runs 2 h; the upper terraces alone are a fine 45-minute pace-break. Pairs naturally with Pitti: the entrance is the courtyard behind it.',
links: [
{ label: 'Official site', href: 'https://www.boboli.it' },
{ label: 'Tickets', href: 'https://www.uffizi.it/en/visit/tickets' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Attraction_Review-g187791-d183872-Reviews-Giardini_Di_Boboli-Florence_Tuscany.html' },
{ label: 'Reddit r/Florence', href: 'https://www.reddit.com/r/Florence/search/?q=boboli&restrict_sr=1' }
] } },
{ id: 'S6', name: 'Gelato — Vivoli', kind: 'meal', mealKind: 'snack',
slot: 'snack', state: 'planned',
at: [43.7680, 11.2537], dur: 20, durConf: 3,
suggested: { value: 20, conf: 1, src: null },
price: null,
img: ['🍦', 'Vivoli gelato', 'linear-gradient(135deg,#e0a5c0,#a56a92)'] },
{ id: 'S7', name: 'Lunch — Ristorante Santini', kind: 'meal', mealKind: 'lunch',
slot: 'lunch', state: 'alt', url: 'https://www.santini.it',
at: [43.7646, 11.2528], dur: 75, durConf: 3,
suggested: { value: 90, conf: 2, src: 'santini.it' },
price: { amount: 45, cur: '€', conf: 2, src: 'santini.it' },
img: ['🍷', 'Ristorante Santini', 'linear-gradient(135deg,#7a4a5c,#4c2b3a)',
'https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c0/View_from_Giotto%27s_Bell_Tower_-_Florence.jpg/1280px-View_from_Giotto%27s_Bell_Tower_-_Florence.jpg'],
detail: {
summary: 'A classic Florentine restaurant in a 16th-century building on the Arno — the terrace looks across to Pitti. Set tables, longer menus, lunch from 12:30. The step-up option: worth holding if Marios queue looks long, or if the day goes well and you want to mark it.',
links: [
{ label: 'Official site', href: 'https://www.santini.it' },
{ label: 'Menu', href: 'https://www.santini.it/en/menu' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Restaurant_Review-g187791-d1442007-Reviews-Ristorante_Santini-Florence_Tuscany.html' },
{ label: 'Reddit r/food', href: 'https://www.reddit.com/r/food/search/?q=santini+florence&restrict_sr=1' }
] } }
]
},
{ id: 'D2', date: '2026-09-13', label: 'Day 2 · Sat 13',
startMin: 8 * 60 + 30, wakingHours: 11,
base: { name: 'Hotel Palagio', at: [43.7684, 11.2566] },
legs: [],
stops: [
{ id: 'S8', name: 'Breakfast at the hotel', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [43.7684, 11.2566], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
{ id: 'S9', name: 'Galleria dellAccademia (David)', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.galleriaaccademia.firenze.it',
at: [43.7734, 11.2628], dur: 90, durConf: 3,
suggested: { value: 120, conf: 2, src: 'wikivoyage.org' },
price: { amount: 16, cur: '€', conf: 2, src: 'galleriaaccademia.firenze.it' },
img: ['🗿', 'Accademia', 'linear-gradient(135deg,#6b7280,#3f4650)'],
detail: {
summary: 'Home of the original David plus Donatellos Prigione. Timed-entry tickets, and the David room moves in batches — a 09:30 slot is noticeably calmer than midday. Allow ~90 min; 2 h if you do the whole collection.',
links: [
{ label: 'Official site', href: 'https://www.galleriaaccademia.firenze.it' },
{ label: 'Tickets', href: 'https://www.galleriaaccademia.firenze.it/en/tickets' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Attraction_Review-g187791-d183692-Reviews-Galleria_dell_Accademia-Florence_Tuscany.html' },
{ label: 'Reddit r/Florence', href: 'https://www.reddit.com/r/Florence/search/?q=accademia+david&restrict_sr=1' }
] } },
{ id: 'S10', name: 'Lunch — Trattoria Sostanza', kind: 'meal', mealKind: 'lunch',
slot: 'lunch', state: 'planned', url: 'https://www.sostanza.it',
at: [43.7609, 11.2469], dur: 75, durConf: 3,
suggested: { value: 90, conf: 2, src: 'tripadvisor.com' },
price: { amount: 35, cur: '€', conf: 2, src: 'tripadvisor.com' },
img: ['🍕', 'Trattoria Sostanza', 'linear-gradient(135deg,#b0563c,#7a3a2c)'],
detail: {
summary: 'Oltrarno kitchen a short walk across the Arno from the Accademia: Florentine classics, a proper wine list, and a roomy back — the kind of lunch that runs long. Reservations recommended on Saturdays.',
links: [
{ label: 'Official site', href: 'https://www.sostanza.it' },
{ label: 'Menu', href: 'https://www.sostanza.it/menu' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Restaurant_Review-g187791-d559983-Reviews-Trattoria_Sostanza-Florence_Tuscany.html' },
{ label: 'Reddit r/food', href: 'https://www.reddit.com/r/food/search/?q=sostanza+florence&restrict_sr=1' }
] } },
{ id: 'S11', name: 'Oltrarno stroll · San Niccolò', kind: 'visit', slot: 'visit', state: 'planned',
at: [43.7613, 11.2497], dur: 60, durConf: 3,
suggested: { value: 60, conf: 1, src: null },
price: null,
img: ['⛪', 'Oltrarno', 'linear-gradient(135deg,#8f7a4f,#5c4d2e)'] }
]
},
{ id: 'D3', date: '2026-09-14', label: 'Day 3 · Sun 14',
startMin: 9 * 60, wakingHours: 11,
base: { name: 'Hotel Palagio', at: [43.7684, 11.2566] },
legs: [],
stops: [
{ id: 'S12', name: 'Breakfast at the hotel', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [43.7684, 11.2566], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
{ id: 'S13', name: 'San Marco & cloister', kind: 'visit', slot: 'visit', state: 'planned',
at: [43.7723, 11.2597], dur: 45, durConf: 3,
suggested: { value: 45, conf: 1, src: null },
price: { amount: 8, cur: '€', conf: 1, src: null },
img: ['🖼️', 'San Marco', 'linear-gradient(135deg,#7a6a4f,#4d422e)'] },
{ id: 'S14', name: 'Aperitivo — Il Latte', kind: 'meal', mealKind: 'dinner',
slot: 'dinner', state: 'maybe',
url: 'https://www.illatte.it',
at: [43.7700, 11.2560], dur: 60, durConf: 3,
suggested: { value: 60, conf: 1, src: null },
price: { amount: 12, cur: '€', conf: 2, src: 'tripadvisor.com' },
img: ['🥂', 'Il Latte', 'linear-gradient(135deg,#c9a13c,#8f6f1e)'] }
]
},
{ id: 'D4', date: '2026-09-15', label: 'Day 4 · Mon 15', isDeparture: true,
startMin: 8 * 60, wakingHours: 11,
base: { name: 'Hotel Palagio', at: [43.7684, 11.2566] },
legs: [],
stops: [
{ id: 'S15', name: 'Breakfast & check-out', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [43.7684, 11.2566], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
{ id: 'S16', name: 'Train → Pisa Centrale', kind: 'transit', slot: 'transit', state: 'planned',
at: [43.7726, 11.2597], dur: 30, durConf: 4,
transit: { name: 'Frecciarossa 9512', ref: 'F-55812',
from: 'Firenze SMN', to: 'Pisa Centrale',
depart: '09:12', arrive: '09:58', arriveBy: '08:42',
buffer: '30 min before departure' },
suggested: null,
price: { amount: 45, cur: '€', conf: 4, src: 'booking' },
img: ['🚄', 'Frecciarossa 9512', 'linear-gradient(135deg,#274b8f,#16294d)'] }
]
}
],
// focus-mode candidate pools (what a slot search returns)
candidates: {
hotel: [
{ id: 'H1', name: 'Hotel Palagio', at: [43.7684, 11.2566], price: 180, dur: null,
tags: ['central', 'quiet', 'refundable'], pitch: 'Where you are now — refundable until the 8th.', url: null },
{ id: 'H2', name: 'Hotel Savoy', at: [43.7726, 11.2565], price: 210, dur: null,
tags: ['near SMN', 'grand', 'rooftop'], pitch: 'Grand hotel by the station — handy for the 15th, 10 min from the centre.', url: null },
{ id: 'H3', name: 'Residenza Oltrarno', at: [43.7604, 11.2510], price: 140, dur: null,
tags: ['oltrarno', 'small', 'value'], pitch: 'Six rooms above a bookshop — cheaper, and puts you on the south side.', url: null },
],
lunch: [
{ id: 'C1', name: 'Trattoria Mario', at: [43.7667, 11.2544], price: 25, dur: 90,
tags: ['counter', 'local', 'queue'], pitch: 'No-frills counter since 1954 — the queue is the point.',
url: 'https://www.trattoriamario.it', img: ['🍝', 'Trattoria Mario', 'linear-gradient(135deg,#d08b3c,#9c5f1e)', 'https://upload.wikimedia.org/wikipedia/commons/1/12/Bistecca_alla_fiorentina-01.jpg'] },
{ id: 'C2', name: 'Ristorante Santini', at: [43.7646, 11.2528], price: 45, dur: 90,
tags: ['views', 'terrace', 'splurge'], pitch: '16th-century set tables on the Arno, terrace across from Pitti.',
url: 'https://www.santini.it', img: ['🍷', 'Santini', 'linear-gradient(135deg,#7a4a5c,#4c2b3a)'] },
{ id: 'C3', name: 'Trattoria Sostanza', at: [43.7609, 11.2469], price: 35, dur: 90,
tags: ['oltrarno', 'roomy', 'wine'], pitch: 'Oltrarno kitchen, proper wine list, a roomy back — runs long.',
url: 'https://www.sostanza.it', img: ['🍕', 'Sostanza', 'linear-gradient(135deg,#b0563c,#7a3a2c)'] },
{ id: 'C4', name: 'Vendemmia', at: [43.7735, 11.2585], price: 30, dur: 75,
tags: ['vegetarian', 'garden', 'near SMN'], pitch: 'Garden terrace by SMN, strong vegetarian menu, calmer pace.',
url: 'https://www.vendemmia.it', img: ['🥗', 'Vendemmia', 'linear-gradient(135deg,#5f9e6b,#33613c)'] },
{ id: 'C5', name: 'La Cantina di San Niccolò', at: [43.7615, 11.2490], price: 22, dur: 75,
tags: ['cellar', 'cozy', 'oltrarno'], pitch: 'Stone cellar, candlelight, short honest menu — book ahead.',
url: 'https://www.lacantina.it', img: ['🕯️', 'La Cantina', 'linear-gradient(135deg,#8f7a4f,#5c4d2e)'] },
{ id: 'C6', name: 'Buonami Pizza', at: [43.7680, 11.2550], price: 15, dur: 45,
tags: ['fast', 'casual', 'near hotel'], pitch: 'Two-stop, fast, good — the reset-button lunch.',
url: 'https://www.buonami.it', img: ['🍕', 'Buonami', 'linear-gradient(135deg,#c98a2c,#8a5f1a)'] }
],
visit: [
{ id: 'V1', name: 'Piazzale Michelangelo', at: [43.7669, 11.2455], price: 0, dur: 60,
tags: ['views', 'sunset', 'free'], pitch: 'The postcard view — best from 17:00 as the light warms.',
url: 'https://en.wikipedia.org/wiki/Piazzale_Michelangelo', img: ['🌇', 'Piazzale Michelangelo', 'linear-gradient(135deg,#c97a3c,#8a4f1a)'] },
{ id: 'V2', name: 'San Miniato al Monte', at: [43.7620, 11.2521], price: 4, dur: 60,
tags: ['views', 'romanesque', 'calm'], pitch: 'Romanesque basilica on the hill — quiet, cool in the afternoon.',
url: 'https://en.wikipedia.org/wiki/San_Miniato_al_Monte', img: ['⛪', 'San Miniato', 'linear-gradient(135deg,#8f9a7a,#5c644d)'] },
{ id: 'V3', name: 'Santo Spirito square', at: [43.7603, 11.2496], price: 0, dur: 45,
tags: ['square', 'lazier', 'market'], pitch: 'Oltrarnos lazy square; Saturdays market is the event.',
url: 'https://en.wikipedia.org/wiki/Santo_Spirito,_Florence', img: ['☕', 'Santo Spirito', 'linear-gradient(135deg,#a58a6a,#6d5840)'] },
{ id: 'V4', name: 'Terme di Caracalla (ruins)', at: [43.7589, 11.2707], price: 10, dur: 75,
tags: ['ruins', 'open', 'afternoon'], pitch: 'Third-century baths in open fields — cool, empty, odd.',
url: 'https://en.wikipedia.org/wiki/Baths_of_Caracalla', img: ['🏛️', 'Terme di Caracalla', 'linear-gradient(135deg,#b0a58a,#756a4f)'] }
]
},
nudges: {
D1: [
{ id: 'N1', title: 'Lunch may be a little tight',
body: '45 min at Trattoria Mario — a meal there usually runs <b>~90 min</b> <i>(web)</i>, and the local queue adds to that.',
fix: { label: 'Extend to ~90 min', stop: 'S4', dur: 90,
msg: 'Lunch extended to 90 min — everything after it slides later; the day still fits.' } },
{ id: 'N2', title: 'Pitti: 1 h (you) vs ~2.5 h usually',
body: 'Guides suggest 23 h for the full complex. We kept your 1 h — no need to change anything.',
fix: { label: 'Keep as is' } }
],
D2: [],
D3: [
{ id: 'N3', title: 'Dinner is still an idea',
body: 'Il Latte is an option, not a plan — worth deciding by this evening so theres a fallback if its full.',
fix: { label: 'Keep as is' } }
]
},
multimodal: [
{ into: 'S5', dur: 18, sub: [{ mode: 'foot', dur: 6 }, { mode: 'tram', dur: 12 }] }
],
vibes: [
{ id: 'v1', label: 'art & food · relaxed pace' },
{ id: 'v2', label: 'gardens & walking' },
{ id: 'v3', label: 'day trip · Fiesole' },
{ id: 'v4', label: 'views & aperitivo' }
],
suggestions: [
{ stop: 'S2', pitch: 'The Palazzo Medicis private apartments — less crowded than the Uffizi, opens right at 9:30.',
enrich: 'open 09:3019:30 · skip-the-line available' },
{ stop: 'S4', pitch: 'No-frills Florentine kitchen the locals queue for. Expect a wait without a reservation.',
enrich: 'lunch 12:0014:30 · dinner from 19:30 · ~1 h typically' },
{ stop: 'S5', pitch: 'Renaissance gardens behind Pitti — a good pace-break in the heat; ~2 h is the sweet spot.',
enrich: 'open 08:1519:30 · last entry 18:15' },
{ stop: 'S7', pitch: 'The step-up lunch: set tables on the Arno, terrace across from Pitti. Worth holding if Marios queue looks long.',
enrich: 'lunch from 12:30 · book ahead in season' }
]
};
// ---------------------------------------------------------------------------
// Boston — same shapes, but INSIDE the local OSRM extract (NE-US), so the
// routeClient upgrades legs to real road-network times + geometry.
// ---------------------------------------------------------------------------
const BOSTON = {
trip: {
id: 'trip-2026-09-b',
version: 1,
title: 'Boston 1416 Sep',
stays: [
{ id: 'ST1', name: 'Hotel Commonwealth', at: [42.3495, -71.0960], place: 'Boston',
checkIn: '2026-09-14', checkOut: '2026-09-16', state: 'booked', price: 165 },
],
places: [
{ id: 'PL1', name: 'Boston', bbox: [42.33, -71.12, 42.375, -71.04],
center: [42.357, -71.063], vibe: 'history + harbor, food-first',
dates: ['2026-09-14', '2026-09-16'] }
],
bookings: [
{ type: 'hotel', name: 'Hotel Commonwealth', ref: 'H-55210',
dates: '1416 Sep', where: [42.3495, -71.0960], status: 'booked', source: 'user_booking' },
{ type: 'train', name: 'Amtrak 700', ref: 'A-33421',
from: 'Back Bay', to: 'Penn Station, NYC', date: '2026-09-16',
depart: '17:00', arrive: '19:40', status: 'booked', source: 'user_booking',
stations: { from: [42.3519, -71.0654], to: [40.7506, -73.9935] } }
]
},
days: [
{ id: 'D1', date: '2026-09-14', label: 'Day 1 · Mon 14',
startMin: 8 * 60, wakingHours: 11,
base: { name: 'Hotel Commonwealth', at: [42.3495, -71.0960] },
legs: [],
stops: [
{ id: 'S1', name: 'Breakfast at the hotel', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [42.3495, -71.0960], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
{ id: 'S2', name: 'Museum of Fine Arts', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.mfa.org',
at: [42.3394, -71.0949], dur: 120, durConf: 3,
suggested: { value: 150, conf: 2, src: 'mfa.org' },
price: { amount: 25, cur: '$', conf: 2, src: 'mfa.org' },
img: ['🖼️', 'MFA Boston', 'linear-gradient(135deg,#7a4a5c,#4c2b3a)',
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/MFA%2C_Boston%2C_MA.jpg/1280px-MFA%2C_Boston%2C_MA.jpg'],
detail: {
summary: 'One of the largest art museums in the US, next to the hotel. The Impressionist wing and the Asian galleries are the anchors — allow 2 h for highlights, 3 h if you linger in the Japanese scroll wing. Thursday/Friday the light in the skylit atrium is the best.',
links: [
{ label: 'Official site', href: 'https://www.mfa.org' },
{ label: 'Tickets', href: 'https://www.mfa.org/visit/tickets' },
{ label: 'TripAdvisor', href: 'https://www.tripadvisor.com/Attraction_Review-g1146335-d183805-Reviews-Museum_of_Fine_Arts-Boston_MA.html' }
] } },
{ id: 'S4', name: 'Lunch — State Street Fare', kind: 'meal', mealKind: 'lunch',
slot: 'lunch', state: 'planned', url: 'https://www.statestreetfare.com',
at: [42.3410, -71.0935], dur: 60, durConf: 3,
suggested: { value: 75, conf: 2, src: 'statestreetfare.com' },
price: { amount: 30, cur: '$', conf: 2, src: 'statestreetfare.com' },
img: ['🍽️', 'State Street Fare', 'linear-gradient(135deg,#d08b3c,#9c5f1e)'],
detail: {
summary: 'The MFAs own café with a garden terrace — New England seasonal plates. Its the sensible choice after a museum morning; the terrace gets crowded by 13:00 on weekends.',
links: [
{ label: 'Official site', href: 'https://www.statestreetfare.com' },
{ label: 'Menu', href: 'https://www.statestreetfare.com/menu' }
] } },
{ id: 'S5', name: 'Boston Public Library (Coppa Wing)', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.bpl.org',
at: [42.3499, -71.0769], dur: 60, durConf: 3,
suggested: { value: 60, conf: 2, src: 'bpl.org' },
price: { amount: 0, cur: '$', conf: 2, src: 'bpl.org' },
img: ['📚', 'BPL', 'linear-gradient(135deg,#5f7a4f,#3a4f2c)'],
detail: {
summary: 'The Beaux-Arts free reading room and the Domitian & Apollo statues in the courtyard. A good 4560 min breather on the walk toward the North End — the architecture alone is worth it.',
links: [
{ label: 'Official site', href: 'https://www.bpl.org' }
] } },
{ id: 'S6', name: 'Faneuil Hall & Quincy Market', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.boston.gov/departments/parks/faneuil-hall',
at: [42.3601, -71.0555], dur: 90, durConf: 3,
suggested: { value: 90, conf: 2, src: 'boston.gov' },
price: null,
img: ['🏛️', 'Faneuil Hall', 'linear-gradient(135deg,#b5533c,#7a3a2c)'],
detail: {
summary: 'The “Cradle of Liberty” and the covered food market. Best worked top-down: the market stalls first (clam chowder, lemonade), then the halls free exhibits. Weekends are busy — September afternoons are kinder.',
links: [
{ label: 'Official site', href: 'https://www.boston.gov/departments/parks/faneuil-hall' },
{ label: 'Quincy Market', href: 'https://www.quincymarket.com' }
] } }
]
},
{ id: 'D2', date: '2026-09-15', label: 'Day 2 · Tue 15',
startMin: 8 * 60 + 30, wakingHours: 11,
base: { name: 'Hotel Commonwealth', at: [42.3495, -71.0960] },
legs: [],
stops: [
{ id: 'S8', name: 'Breakfast at the hotel', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [42.3495, -71.0960], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
{ id: 'S9', name: 'Paul Revere House', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://www.boston.gov/departments/parks/paul-revere-house',
at: [42.3616, -71.0631], dur: 45, durConf: 3,
suggested: { value: 45, conf: 2, src: 'boston.gov' },
price: { amount: 10, cur: '$', conf: 2, src: 'boston.gov' },
img: ['🏠', 'Paul Revere House', 'linear-gradient(135deg,#8f7a4f,#5c4d2e)'],
detail: {
summary: 'The oldest house in Boston, a tight 3045 min with the resident docent. Timed entry; the North End around it is the walk.',
links: [
{ label: 'Official site', href: 'https://www.boston.gov/departments/parks/paul-revere-house' }
] } },
{ id: 'S10', name: 'Lunch — Union Oyster House', kind: 'meal', mealKind: 'lunch',
slot: 'lunch', state: 'planned', url: 'https://www.unionoysterhouse.com',
at: [42.3621, -71.0604], dur: 60, durConf: 3,
suggested: { value: 75, conf: 2, src: 'unionoysterhouse.com' },
price: { amount: 40, cur: '$', conf: 2, src: 'unionoysterhouse.com' },
img: ['🦪', 'Union Oyster House', 'linear-gradient(135deg,#4f7a9e,#2c4f6b)'],
detail: {
summary: 'Claimed oldest restaurant in the US (1826). Clam chowder and a short, serious menu. Book ahead for the lunch hour — the room is small.',
links: [
{ label: 'Official site', href: 'https://www.unionoysterhouse.com' }
] } },
{ id: 'S11', name: 'New England Aquarium', kind: 'visit', slot: 'visit', state: 'planned',
url: 'https://neaq.org',
at: [42.3598, -71.0483], dur: 120, durConf: 3,
suggested: { value: 120, conf: 2, src: 'neaq.org' },
price: { amount: 35, cur: '$', conf: 2, src: 'neaq.org' },
img: ['🐋', 'NE Aquarium', 'linear-gradient(135deg,#2f6fd6,#1d4f9c)'],
detail: {
summary: 'The whale tank and the Harborwalk beyond it. September is a good time — the new whale migration is winding down but the penguin and jelly exhibits are year-round. Allow 2 h.',
links: [
{ label: 'Official site', href: 'https://neaq.org' }
] } }
]
},
{ id: 'D3', date: '2026-09-16', label: 'Day 3 · Wed 16', isDeparture: true,
startMin: 8 * 60, wakingHours: 11,
base: { name: 'Hotel Commonwealth', at: [42.3495, -71.0960] },
legs: [],
stops: [
{ id: 'S15', name: 'Breakfast & check-out', kind: 'meal', mealKind: 'breakfast',
slot: 'breakfast', state: 'planned',
at: [42.3495, -71.0960], dur: 30, durConf: 3,
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
{ id: 'S12', name: 'Boston Common & Public Garden', kind: 'visit', slot: 'visit', state: 'planned',
at: [42.3547, -71.0717], dur: 45, durConf: 3,
suggested: { value: 45, conf: 1, src: null },
price: null,
img: ['🌳', 'Public Garden', 'linear-gradient(135deg,#4f9e63,#2c6b40)'] },
{ id: 'S16', name: 'Train → Penn Station NYC', kind: 'transit', slot: 'transit', state: 'planned',
at: [42.3519, -71.0654], dur: 30, durConf: 4,
transit: { name: 'Amtrak 700', ref: 'A-33421',
from: 'Back Bay', to: 'Penn Station, NYC',
depart: '17:00', arrive: '19:40', arriveBy: '16:30',
buffer: '30 min before departure' },
suggested: null,
price: { amount: 65, cur: '$', conf: 4, src: 'booking' },
img: ['🚄', 'Amtrak 700', 'linear-gradient(135deg,#274b8f,#16294d)'] }
]
}
],
candidates: {
hotel: [
{ id: 'H1', name: 'Hotel Commonwealth', at: [42.3495, -71.0960], price: 165, dur: null,
tags: ['by the mfa', 'quiet', 'refundable'], pitch: 'Where you are now — one block from the MFA, refundable until the 12th.', url: null },
{ id: 'H2', name: 'The Liberty Hotel', at: [42.3580, -71.0530], price: 240, dur: null,
tags: ['north end', 'rooftop', 'grand'], pitch: 'Rooftop bar over the North End — closer to the food, pricier.', url: null },
{ id: 'H3', name: 'Haven Guest House', at: [42.3470, -71.0900], price: 120, dur: null,
tags: ['value', 'small', 'near mfa'], pitch: 'Six rooms off the MFAs garden — cheaper, a short walk to everything.', url: null }
],
lunch: [
{ id: 'C1', name: 'State Street Fare', at: [42.3410, -71.0935], price: 30, dur: 75,
tags: ['by the mfa', 'garden', 'seasonal'], pitch: 'The MFAs café — garden terrace, New England seasonal plates.',
url: 'https://www.statestreetfare.com', img: ['🍽️', 'State Street Fare', 'linear-gradient(135deg,#d08b3c,#9c5f1e)'] },
{ id: 'C2', name: 'Union Oyster House', at: [42.3621, -71.0604], price: 40, dur: 75,
tags: ['historic', 'oysters', 'book ahead'], pitch: 'Claimed oldest restaurant in the US — clam chowder, a short serious menu.',
url: 'https://www.unionoysterhouse.com', img: ['🦪', 'Union Oyster', 'linear-gradient(135deg,#4f7a9e,#2c4f6b)'] },
{ id: 'C3', name: 'Eataly Boston', at: [42.3595, -71.0540], price: 35, dur: 60,
tags: ['market', 'fast', 'central'], pitch: 'The food-hall option — a dozen kitchens, grab and go, by Quincy Market.',
url: 'https://boston.eataly.net', img: ['🍕', 'Eataly', 'linear-gradient(135deg,#b0563c,#7a3a2c)'] },
{ id: 'C4', name: 'Kazuo', at: [42.3540, -71.0620], price: 32, dur: 60,
tags: ['japanese', 'counter', 'near the common'], pitch: 'Japanese counter near the Common — quiet, good, a little pricier.',
url: 'https://kazooboston.com', img: ['🍣', 'Kazuo', 'linear-gradient(135deg,#5f6b7a,#3a434f)'] }
],
visit: [
{ id: 'V1', name: 'John F. Kennedy Presidential Library', at: [42.3599, -71.0590], price: 15, dur: 90,
tags: ['history', 'harbor view', 'indoor'], pitch: 'The JFK archives with a harbor view — a good 90 min, indoor if it turns.',
url: 'https://www.jfklibrary.org', img: ['🎙️', 'JFK Library', 'linear-gradient(135deg,#4a5a78,#2f3a50)'] },
{ id: 'V2', name: 'Harborwalk', at: [42.3590, -71.0420], price: 0, dur: 60,
tags: ['outdoor', 'harbor', 'walk'], pitch: 'The esplanade along the harbor — sunset is the point in September.',
url: 'https://www.boston.gov/departments/parks/harborwalk', img: ['🌅', 'Harborwalk', 'linear-gradient(135deg,#c97a3c,#8a4f1a)'] },
{ id: 'V3', name: 'T. F. Green Memorial', at: [42.3530, -71.0470], price: 0, dur: 45,
tags: ['views', 'free', 'calm'], pitch: 'The memorial at the harbors edge — a quiet, photo-friendly 3045 min.',
url: 'https://www.boston.gov', img: ['⛵', 'T. F. Green', 'linear-gradient(135deg,#7a8a9e,#4f5a6b)'] },
{ id: 'V4', name: 'Boston Tea Party Ships & Museum', at: [42.3555, -71.0535], price: 30, dur: 75,
tags: ['history', 'interactive', 'harbor'], pitch: 'A 75-min interactive re-enactment on the harbor — fun, a little kitsch.',
url: 'https://www.bostonteachallenge.org', img: ['🫖', 'Tea Party', 'linear-gradient(135deg,#8f4f4f,#5c2f2f)'] }
]
},
nudges: {
D1: [
{ id: 'N1', title: 'Lunch may be a little tight',
body: '60 min at State Street Fare — the terrace lunch usually runs <b>~75 min</b> <i>(web)</i>, and it crowds by 13:00.',
fix: { label: 'Extend to ~75 min', stop: 'S4', dur: 75,
msg: 'Lunch extended to 75 min — the afternoon slides a little later; the day still fits.' } },
{ id: 'N2', title: 'MFA: 2 h (you) vs ~2.5 h usually',
body: 'Guides suggest 2.53 h for the highlights. We kept your 2 h — the Impressionist wing alone runs that long.',
fix: { label: 'Keep as is' } }
],
D2: [
{ id: 'N3', title: 'Union Oyster House at the lunch hour',
body: 'The room is small and booked solid 12:0013:30 — a 12:00 seat beats a 12:30 walk-in.',
fix: { label: 'Keep as is' } }
],
D3: []
},
multimodal: [],
vibes: [
{ id: 'v1', label: 'history & harbor · relaxed pace' },
{ id: 'v2', label: 'museums & art' },
{ id: 'v3', label: 'food-first, North End' },
{ id: 'v4', label: 'views & sunset walk' }
],
suggestions: [
{ stop: 'S2', pitch: 'The MFAs Impressionist wing and the Japanese scroll room — 2 h for highlights, 3 h if you linger.',
enrich: 'open 10:0017:00 · free first Thu 49 · one block from the hotel' },
{ stop: 'S4', pitch: 'The MFAs own café — the garden terrace gets crowded by 13:00 on weekends.',
enrich: 'lunch 12:0015:00 · ~1 h · terrace seats' },
{ stop: 'S5', pitch: 'The Beaux-Arts free reading room and the courtyards Domitian & Apollo — a good breather toward the North End.',
enrich: 'open 10:0018:00 · free · ~45 min' },
{ stop: 'S6', pitch: 'Work the market stalls first (chowder, lemonade), then the free hall exhibits — weekends are busy.',
enrich: 'stalls 10:0017:00 · hall free · ~90 min' }
]
};
// pick the demo city from ?city= (default Florence)
const _city = (new URLSearchParams(location.search).get('city') || 'florence').toLowerCase();
window.MOCK = _city === 'boston' ? BOSTON : FLORENCE;
window.MOCK_CITY = _city;