Migration 107 only rewrote image_url rows matching /places/%/photos/%; URLs using
the /place-photos/ or /places/<opaque> paths survived the upgrade and were passed
verbatim to the Places API, producing a malformed request whose empty/HTML response
body threw SyntaxError before detailsRes.ok was checked. The resulting rejection was
leaked by placePhotoCache.setInFlight via an unhandled .finally() chain, triggering
Node 22's default unhandledRejection=throw and terminating the process.
- placePhotoCache: add .catch() after .finally() to prevent unhandled rejection crash
- mapsService: reject URL-shaped placeIds early; read response as text before JSON.parse
- migrations: add migration to rewrite remaining googleusercontent/places.googleapis URLs
- MapView/MapViewGL: prefer stable proxy URL form of image_url before google_place_id
Fixes#770
ReservationOverlay was Leaflet-only: react-leaflet components, L.divIcon,
panes, useMap/useMapEvents. When the user switched the planner map to
Mapbox GL, the entire feature disappeared — no polylines, no endpoint
badges, no clickable IATA labels.
Add a matching overlay for the Mapbox renderer:
- New reservationsMapbox.ts with an imperative `ReservationMapboxOverlay`
class — mapbox-gl is imperative, so a React component wrapper would
fight its own lifecycle every render. The manager owns one GeoJSON
source + line layer for the arcs, one HTML `mapboxgl.Marker` per
endpoint badge, and one per flight stats label. It cleans itself up
when the map is rebuilt (style/token/3d toggle) or unmounted.
- Geometry helpers (great-circle arc, antimeridian split, haversine,
tz-aware duration math, label formatting) are copied from the Leaflet
overlay so both renderers produce the same lines. Great-circle is
useful even on the Mapbox globe because the mercator projection mode
still draws the short-way line, and the antimeridian split prevents
a NYC↔Tokyo flight from wrapping halfway around the planet.
- Flights / cruises get geodesic arcs; trains / cars get straight
lines. All four types get clickable endpoint badges with the
matching lucide icon; only flights render the rotating mid-arc stats
label (IATA → IATA · distance · duration) — same rule as the Leaflet
overlay.
- The stats label's rotation is recomputed on every `render` event by
projecting two points straddling the arc midpoint, which keeps it
parallel to the arc as the camera rotates/zooms on the globe.
- Visibility thresholds mirror the Leaflet overlay (per-type min pixel
distance before a line / endpoint label is worth drawing).
- MapViewGL now accepts the `reservations`, `visibleConnectionIds`,
`showReservationStats`, `onReservationClick` props that the Leaflet
MapView already took. `visibleConnectionIds` is honoured the same way
— the per-booking toggle in DayPlanSidebar controls which routes
appear, so switching the renderer doesn't lose that UX.
- Added a `mapReady` gate so the overlay can only add its source/layer
once the map's `load` handler has attached the other trip sources;
the gate resets on every style rebuild.
- Mapbox GL provider alongside Leaflet for trip and journey maps (opt-in in
settings with token, style presets incl. 3D on satellite, quality mode,
experimental badge).
- GPS "blue dot" with heading cone on mobile; three-state FAB (off / show /
follow), geodesic accuracy circle, desktop-hidden since browser IP geo is
too coarse for navigation.
- Marker drift fix: outer wrap no longer carries inline position/transform,
so mapbox's translate keeps the pin pinned at every zoom and pitch.
- Journey map popup (mapbox-gl): Apple-Maps-style tooltip on marker
highlight/click showing entry title + location / date subline.
- Journey feed reorder: up/down controls to the left of each entry reorder
sort_order within a day. Server endpoint, optimistic store update, rollback
on failure.
- Journey entry editor: desktop modal now centers over the feed column only,
backdrop still blurs the whole page (map included).
- Scroll-sync guard on journey: marker click locks the sync so smooth-scroll
can't steer the highlight to a neighbouring entry mid-animation.
- Misc: map top-padding aligned with hero, live/synced badges replaced by a
compact back-button in the hero, skeleton entries no longer pollute the
journey map, journey detail no longer shows map on mobile path when
combined view is active.
Introduces a TransportModal for creating/editing flight, train, car, and cruise
reservations that span multiple days. Transport entries now break the map route
into disconnected segments so the polyline reflects actual travel legs.
- Add TransportModal with airport/location pickers, multi-day date range, and all transport types
- Extend DB schema with end_day_id on reservations (migration 110) and backfill from existing dates
- Refactor useRouteCalculation to emit [][][number,number] segments split at transport boundaries
- Update MapView, DayPlanSidebar, ReservationsPanel, TripPlannerPage to wire up transport flow
- Add transport i18n keys across all 15 languages
Add type-selector UI in the file import modal letting users choose which
GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points,
paths) to import. KML LineString placemarks are now imported as path
places with route_geometry.
Performance improvements:
- Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut
unnecessary re-renders in PlacesSidebar
- Add weatherQueue to cap concurrent weather fetches at 3
- Replace sequential per-place deletes with a single bulkDelete API call
(new DELETE /places/bulk endpoint + deletePlacesMany service)
- Memoize atlas/photo/weather service calls to avoid redundant requests
- Add multi-select mode to PlacesSidebar for bulk operations
Add large GPX/KML/KMZ fixtures for integration/perf testing and two
profiler analysis scripts under scripts/.
P0 — stop the bleeding:
- Honor place.image_url in MapView and TripPlannerPage to skip redundant fetchPhoto calls
- Trim Place Details field mask (drop reviews/editorialSummary from default; new getPlaceDetailsExpanded for inspector)
- Admin toggle places_photos_enabled (default ON) to kill Google photo fetches under quota pressure; Wikimedia unaffected
- Return { photoUrl: null } instead of 204 so client handles disabled state cleanly
P1 — structural fix:
- New placePhotoCache service: persistent disk cache at uploads/photos/google/<sha1>.jpg, atomic writes, stampede dedup via in-flight Map
- Migrations 105-107: google_place_photo_meta table, place_details_cache table, backfill signed Google URLs to stable proxy URLs
- getPlacePhoto rewrites to fetch image bytes directly, store on disk, return /api/maps/place-photo/:id/bytes proxy URL
- Stable proxy URLs written to places.image_url — survive container restarts, no expiry
- New GET /api/maps/place-photo/:placeId/bytes route serving cached files with long-lived Cache-Control
- Place Details DB row cache with 7-day TTL; ?refresh=1 escape hatch
- photoService fast-path: proxy URLs bypass the mapsApi round-trip and go straight to urlToBase64
Bug fixes:
- MapView now requests base64 thumbs for places with proxy image_url (markers were showing color fallback)
- createPlaceIcon accepts /api/maps/place-photo/ URLs as interim fallback while thumb generates
- setSelectedAssignmentId ReferenceError in mobile day-detail handler (use selectAssignment)
- Remove redundant decodeURIComponent on already-decoded Express route param
- Use SHA1 hash for disk filenames to prevent coords:lat:lng pseudo-ID collisions
- Add checkSsrf guard to Wikimedia byte fetch
- Tighten migration 107 LIKE filter to avoid rewriting manually-pasted Google image URLs
- Validate enabled is boolean on PUT /admin/places-photos
- Drop aggressive iconCache.clear() on every thumb arrival
Observability:
- googleFetch() wrapper counts and debug-logs every outbound Google API call with running total
- Make useEndpointPane tolerant when map mock lacks getPane/createPane
- Add useMapEvents to react-leaflet mock in MapView.test
- Rewrite RESMODAL-042 to use the new AirportSelect flow (airline and
flight number only; airport codes are now saved as endpoints, not
metadata)
Adds from/to endpoints to flight/train/cruise/car reservations with
live map rendering. Flights use geodesic arcs and a curved duration +
distance badge; train/car/cruise render as straight or geodesic lines
with endpoint markers. Airports come from an embedded OurAirports
database (~3200 airports, offline-capable); train/cruise/car locations
via Nominatim. Per-trip connection toggle sits in the day plan
sidebar, persisted in localStorage. Clicking a map endpoint opens the
existing transport detail popup. New display setting toggles endpoint
labels on the map. Migration 105 adds the reservation_endpoints table
plus needs_review flag; existing flights are backfilled from their
IATA metadata on server startup.
- Map pans up when DayDetailPanel is open so route markers aren't hidden
- Files lightbox: clicking dark background closes lightbox again
- Memories lightbox: clicking dark background closes lightbox again
Store & re-render optimization:
- TripPlannerPage uses selective Zustand selectors instead of full store
- placesSlice only updates affected days on place update/delete
- Route calculation only reacts to selected day's assignments
- DayPlanSidebar uses stable action refs instead of full store
Map marker performance:
- Shared photoService for PlaceAvatar and MapView (single cache, no duplicate requests)
- Client-side base64 thumbnail generation via canvas (CORS-safe for Wikimedia)
- Map markers use base64 data URL <img> tags for smooth zoom (no external image decode)
- Sidebar uses same base64 thumbnails with IntersectionObserver for visible-first loading
- Icon cache prevents duplicate L.divIcon creation
- MarkerClusterGroup with animate:false and optimized chunk settings
- Photo fetch deduplication and batched state updates
Server optimizations:
- Wikimedia image size reduced to 400px (from 600px)
- Photo cache: 5min TTL for errors (was 12h), prevents stale 404 caching
- Removed unused image-proxy endpoint
UX improvements:
- Splash screen with plane animation during initial photo preload
- Markdown rendering in DayPlanSidebar place descriptions
- Missing i18n keys added, all 12 languages synced to 1376 keys
On mobile/touch devices, Leaflet tooltips disappear immediately on tap
since there is no hover state. This makes the info bubble permanent for
the selected marker on touch devices so it stays readable.
Fixes#249
Escape HTML entities before dangerouslySetInnerHTML in release notes
renderer to prevent stored XSS via malicious GitHub release bodies.
Fix RouteCalculator ignoring the profile parameter (was hardcoded to
'driving').
https://claude.ai/code/session_01SoQKcF5Rz9Y8Nzo4PzkxY8
- Category filter is now a multi-select dropdown with checkboxes
- PlaceAvatar: replace 200ms polling intervals with event-based
notification + React.memo for major performance improvement
- Map photo fetches: concurrency limited to 3 + lazy loading on images
- PlacesSidebar: content-visibility + useMemo for smooth scrolling
- Accommodation labels: check-out now appears before check-in on same day
- Timed places auto-sort chronologically when time is added
Live location:
- Crosshair button on the map toggles GPS tracking
- Blue dot shows live position with accuracy circle (<500m)
- Uses watchPosition for continuous updates
- Button turns blue when active, click again to stop
Auto-sort:
- Places with a time now auto-sort chronologically among other
timed items (transports, other timed places)
- Adding a time to a place immediately moves it to the correct
position in the timeline
- Untimed places keep their manual order_index
- Add reservation_end_time field (DB migration, API, UI)
- Split reservation form: separate date, start time, end time, status fields
- Fix DateTimePicker forcing 00:00 when no time selected
- Show end time across all reservation displays
- Link-to-assignment and date on same row (50/50 layout)
- Assignment search now shows day headers for filtered results
- Auto-fill date when selecting a day assignment
- Route segments: single OSRM request instead of N separate calls (~6s → ~1s)
- Route labels visible from zoom level 12 (was 16)
- Fix stale route labels after place deletion (useEffect triggers recalc)
- AbortController cancels outdated route calculations
## Collab — Complete Redesign
- iMessage-style live chat with blue bubbles, grouped messages, date separators
- Emoji reactions via right-click (desktop) or double-tap (mobile)
- Twemoji (Apple-style) emoji picker with categories
- Link previews with OG image/title/description
- Soft-delete messages with "deleted a message" placeholder
- Message reactions with real-time WebSocket sync
- Chat timestamps respect 12h/24h setting and timezone
## Collab Notes
- Redesigned note cards with colored header bar (booking-card style)
- 2-column grid layout (desktop), 1-column (mobile)
- Category settings modal for managing categories with colors
- File/image attachments on notes with mini-preview thumbnails
- Website links with OG image preview on note cards
- File preview portal (lightbox for images, inline viewer for PDF/TXT)
- Note files appear in Files tab with "From Collab Notes" badge
- Pin highlighting with tinted background
- Author avatar chip in header bar with custom tooltip
## Collab Polls
- Complete rewrite — clean Apple-style poll cards
- Animated progress bars with vote percentages
- Blue check circles for own votes, voter avatars
- Create poll modal with multi-choice toggle
- Active/closed poll sections
- Custom tooltips on voter chips
## What's Next Widget
- New widget showing upcoming trip activities
- Time display with "until" separator
- Participant chips per activity
- Day grouping (Today, Tomorrow, dates)
- Respects 12h/24h and locale settings
## Route Travel Times
- Auto-calculated walking + driving times via OSRM (free, no API key)
- Floating badge on each route segment between places
- Walking person icon + car icon with times
- Hides when zoomed out (< zoom 16)
- Toggle in Settings > Display to enable/disable
## Other Improvements
- Collab addon enabled by default for new installations
- Coming Soon removed from Collab in admin settings
- Tab state persisted across page reloads (sessionStorage)
- Day sidebar expanded/collapsed state persisted
- File preview with extension badges (PDF, TXT, etc.) in Files tab
- Collab Notes filter tab in Files
- Reservations section in Day Detail view
- Dark mode fix for invite button text color
- Chat scroll hidden (no visible scrollbar)
- Mobile: tab icons removed for space, touch-friendly UI
- Fixed 6 backend data structure bugs in Collab (polls, chat, notes)
- Soft-delete for chat messages (persists in history)
- Message reactions table (migration 28)
- Note attachments via trip_files with note_id (migration 30)
## Database Migrations
- Migration 27: budget_item_members table
- Migration 28: collab_message_reactions table
- Migration 29: soft-delete column on collab_messages
- Migration 30: note_id on trip_files, website on collab_notes
BREAKING: Reservations have been completely rebuilt. Existing place-level
reservations are no longer used. All reservations must be re-created via
the Bookings tab. Your trips, places, and other data are unaffected.
Reservation System (rebuilt from scratch):
- Reservations now link to specific day assignments instead of places
- Same place on different days can have independent reservations
- New assignment picker in booking modal (grouped by day, searchable)
- Removed day/place dropdowns from booking form
- Reservation badges in day plan sidebar with type-specific icons
- Reservation details in place inspector (only for selected assignment)
- Reservation summary in day detail panel
Day Detail Panel (new):
- Opens on day click in the sidebar
- Detailed weather: hourly forecast, precipitation, wind, sunrise/sunset
- Historical climate averages for dates beyond 16 days
- Accommodation management with check-in/check-out, confirmation number
- Hotel assignment across multiple days with day range picker
- Reservation overview for the day
Places:
- Places can now be assigned to the same day multiple times
- Start time + end time fields (replaces single time field)
- Map badges show multiple position numbers (e.g. "1 · 4")
- Route optimization fixed for duplicate places
- File attachments during place editing (not just creation)
- Cover image upload during trip creation (not just editing)
- Paste support (Ctrl+V) for images in trip, place, and file forms
Internationalization:
- 200+ hardcoded German strings translated to i18n (EN + DE)
- Server error messages in English
- Category seeds in English for new installations
- All planner, register, photo, packing components translated
UI/UX:
- Auto dark mode (follows system preference, configurable in settings)
- Navbar toggle switches light/dark (overrides auto)
- Sidebar minimize buttons z-index fixed
- Transport mode selector removed from day plan
- CustomSelect supports grouped headers (isHeader option)
- Optimistic updates for day notes (instant feedback)
- Booking cards redesigned with type-colored headers and structured details
Weather:
- Wind speed in mph when using Fahrenheit setting
- Weather description language matches app language
Admin:
- Weather info panel replaces OpenWeatherMap key input
- "Recommended" badge styling updated
- Replace OpenWeatherMap with Open-Meteo (no API key needed)
- 16-day forecast (up from 5 days)
- Historical climate averages as fallback beyond 16 days
- Auto-upgrade from climate to real forecast when available
- Fix Vacay WebSocket sync across devices (socket-ID exclusion instead of user-ID)
- Add GitHub release history tab in admin panel
- Show cluster count "1" for single map markers when zoomed out
- Add weather info panel in admin settings (replaces OpenWeatherMap key input)
- Update i18n translations (DE + EN)
- Map auto-fits to day places when selecting a day or place
- Dynamic padding accounts for sidebars and place inspector overlay
- Place-based reservations now show linked files in the bookings tab
- Increased max zoom to 16 for closer detail on nearby places
- Route line auto-updates on day select, reorder, assign, remove (no manual button)
- Remove manual route calculation button (keep optimize + Google Maps)
- Lock places at their position during route optimization (click avatar to toggle)
- Locked places shown with red background, border and lock overlay
- Custom tooltip for lock feature (DE/EN, dark mode)
- Fix map zoom: panTo instead of setView keeps current zoom level
- Fix fitBounds only on actual day change, not on place click
- Missing translations: needTwoPlaces, routeOptimized, noGeoPlaces
- Fix backup restore: try/finally ensures DB always reopens after closeDb
- Fix EBUSY on uploads during restore (in-place overwrite instead of rmSync)
- Add DB proxy null guard for clearer errors during restore window
- Add red warning modal before backup restore (DE/EN, dark mode support)
- JWT secret: empty docker-compose default so auto-generation kicks in
- OIDC: pass token via URL fragment instead of query param (no server logs)
- Block SVG uploads on photos, files and covers (stored XSS prevention)
- Add helmet for security headers (HSTS, X-Frame, nosniff, etc.)
- Explicit express.json body size limit (100kb)
- Fix XSS in Leaflet map markers (escape image_url in HTML)
- Remove verbose WebSocket debug logging from client
Real-Time Collaboration (WebSocket):
- WebSocket server with JWT auth and trip-based rooms
- Live sync for all CRUD operations (places, assignments, days, notes, budget, packing, reservations, files)
- Socket-based exclusion to prevent duplicate updates
- Auto-reconnect with exponential backoff
- Assignment move sync between days
Performance:
- 16 database indexes on all foreign key columns
- N+1 query fix in places, assignments and days endpoints
- Marker clustering (react-leaflet-cluster) with configurable radius
- List virtualization (react-window) for places sidebar
- useMemo for filtered places
- SQLite WAL mode + busy_timeout for concurrent writes
- Weather API: server-side cache (1h forecast, 15min current) + client sessionStorage
- Google Places photos: persisted to DB after first fetch
- Google Details: 3-tier cache (memory → sessionStorage → API)
Security:
- CORS auto-configuration (production: same-origin, dev: open)
- API keys removed from /auth/me response
- Admin-only endpoint for reading API keys
- Path traversal prevention in cover image deletion
- JWT secret persisted to file (survives restarts)
- Avatar upload file extension whitelist
- API key fallback: normal users use admin's key without exposure
- Case-insensitive email login
Dark Mode:
- Fixed hardcoded colors across PackingList, Budget, ReservationModal, ReservationsPanel
- Mobile map buttons and sidebar sheets respect dark mode
- Cluster markers always dark
UI/UX:
- Redesigned login page with animated planes, stars and feature cards
- Admin: create user functionality with CustomSelect
- Mobile: day-picker popup for assigning places to days
- Mobile: touch-friendly reorder buttons (32px targets)
- Mobile: responsive text (shorter labels on small screens)
- Packing list: index-based category colors
- i18n: translated date picker placeholder, fixed German labels
- Default map tile: CartoDB Light