Wraps the Vacay and Journey desktop headers in the shared rounded
bg-tertiary bar (title + divider + subtitle, actions grouped on the
right, border and light shadow for contrast). Vacay keeps its filters
sidebar-toggle inside the bar on tablet widths; Journey keeps the
search-toggle and the primary "Create journey" action. Mobile headers
are unchanged.
Brings the dashboard header in line with the Bookings/Lists/Budget/Files
toolbars: a single rounded bg-tertiary bar that groups the title, the
active/archived trip counters, and the view-toggle + widgets + new-trip
actions. Added a border and light shadow so the bar stands out against
the dashboard background in both light and dark mode. Mobile header is
untouched.
The "Add new task..." button moved from the panel into the shared
toolbar and is triggered via addItemSignal. Rewrite the three affected
tests to drive that signal through a rerender instead of clicking the
removed in-panel button.
- Tier cards (Hostel Bunkmate through No Return Ticket) with gradient
icons and placeholder state for empty tiers
- Animated shimmer badge and subtle radial glow behind the card
- Mobile-responsive layout, name chips show just the month on small
screens to avoid overflow
- Copy + translations for all 15 supported languages
- PackingListPanel accepts inlineHeader prop (default true) to keep its
legacy title and inline import button; ListsContainer passes
inlineHeader={false} since the toolbar now owns those controls
- ReservationModal tests look for the renamed 'Car' button (was 'Rental Car')
- Budget total-budget test asserts against the split integer/decimal
spans that replaced the single text node
Trip planner now has a consistent rounded toolbar across bookings, lists,
budget and files. Each panel shows title, inline filter pills (with
counts where useful) and an accent action button on the right. Moved
per-tab controls into the toolbar — lists import, todo add, budget
currency/add-category, files trash/filters — and dropped the redundant
in-panel headers.
Budget sidebar redesigned: total-budget card with indigo-ringed avatars
and coloured split bar; settlement flows as paired avatar cards;
by-category donut rebuilt in SVG with per-category gradients. Both cards
now follow dark/light mode via a widgetTheme helper.
Todo: add-new-task is a portalled modal on desktop, the add-task input
bar is gone; new SORT BY section in the sidebar; inline category
creation in the task editor.
Reservations: pending / confirmed sections remember their collapsed
state per trip (localStorage).
Misc: per-trip connections toggle moved into the day-plan sidebar,
booking endpoints fixed to show on map for trains/cruises/cars as well,
label localStorage persistence, RESMODAL test updated to the new
airport-select flow.
i18n: the new booking / map / todo / budget strings are translated into
all 15 supported languages.
Tabs are now always mounted (visibility toggled via hidden class), so
the same entry title can appear in multiple tab views simultaneously.
Replace getByText with getAllByText for presence checks; scope the
FE-086 click target to the cursor-pointer container.
Closes#686
- Add trekPhotoCache service: SHA1-keyed disk cache under uploads/photos/trek/,
1h TTL, in-flight dedup map to prevent stampedes on concurrent requests
- Add migration 108: trek_photo_cache_meta table
- Hook cache into streamPhoto for Immich/Synology thumbnail path;
originals bypass cache
- Add fetchImmichThumbnailBytes / fetchSynologyThumbnailBytes returning
Buffer instead of piping, used by the cache layer
- Add scheduler entry (every 2h + startup sweep) to evict expired disk
files and DB rows via sweepExpired()
- Client: convert journey tab conditional-mount to hidden-toggle so
img elements stay in DOM across tab switches, preventing redundant
thumbnail requests on rapid tab changes
- Expose invalidateSize() on JourneyMapHandle; call it on map tab
activation to fix Leaflet rendering in previously-hidden container
- ProviderPicker now tracks per-asset album passphrase in a Map; on confirm,
assets are grouped by passphrase and submitted as separate batches so each
asset receives its own album's passphrase instead of the last-selected one
- getOrCreateTrekPhoto unconditionally overwrites the stored passphrase when
a fresh one is supplied, allowing re-adds to heal a stuck bad passphrase
- deleteTrekPhotoIfOrphan purges the trek_photos row for provider assets when
no trip_photos or journey_photos reference it anymore; wired into
removeTripPhoto, removeAlbumLink, and deletePhoto so remove + re-add is a
clean slate
- Three new integration tests: SYNO-090 (passphrase overwrite), SYNO-091
(orphan cleanup), SYNO-092 (remove + re-add restores correct passphrase)
Add admin toggles for places_autocomplete_enabled and places_details_enabled
alongside the existing places_photos_enabled, all default ON.
- adminService: getPlacesAutocomplete/updatePlacesAutocomplete, getPlacesDetails/updatePlacesDetails
- admin routes: GET/PUT /admin/places-autocomplete, /admin/places-details
- maps routes: autocomplete returns { suggestions: [], source: 'disabled' } when off;
details returns { place: null, disabled: true } when off
- authService: both flags included in getAppConfig() response
- authStore: placesAutocompleteEnabled + placesDetailsEnabled state and setters
- App.tsx: wire both flags from app-config on load
- AdminPage: two new toggle rows using var(--text-primary)/var(--border-primary) consistent with rest of UI
- i18n: all 15 locales (en, de, ar, br, cs, es, fr, hu, id, it, nl, pl, ru, zh, zhTw)
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)
Thread selectedAlbumPassphrase from ProviderPicker through onAdd →
journeyApi.addProviderPhotos → POST /entries/:entryId/provider-photos →
addProviderPhoto service → getOrCreateTrekPhoto so shared-album photos
have their passphrase encrypted and persisted on trek_photos at add-time,
enabling streamPhoto to forward it to Synology correctly (#689).
- Replace "Next Notice >" CTA with proper < > pager buttons
- Fix shared scroll container: each slot now scrolls independently
- Sheet uses fixed h-[85dvh] so height is consistent across all notices
- Sticky footer (pager + CTA) always anchored at bottom of each slot
- Content area vertically centered when shorter than available space
- Dismiss-drag suppressed when slot is scrolled down (pan-up to scroll back)
- Scroll position resets on navigation via per-slot refs
- Adjacent slot scroll cleared on horizontal gesture classification
- OK button navigates to next notice on non-last pages, dismisses on last
- OK button only shown when dismissible or on last notice
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.
- SystemNoticeModal tests: navigate to last page before testing
X button, ESC, and CTA dismiss (matches new last-page-only behavior)
- EntryEditor: use z-[9999] instead of portal (fixes iOS stacking
without breaking test DOM queries)
- Pros/cons inputs: remove colored backgrounds in dark mode
Merge the separate Timeline and Map tabs into a single fullscreen
combined view on mobile (<1024px). A Leaflet map fills the background
while a horizontal snap-scroll carousel of entry cards sits at the
bottom. Scrolling the carousel auto-focuses the corresponding map
marker; tapping a marker scrolls to the card. Tapping a card opens
a new fullscreen entry view with edit/delete actions.
- New: MobileMapTimeline, MobileEntryCard, MobileEntryView components
- New: useIsMobile hook (matchMedia < 1024px)
- JourneyMap: fullScreen + paddingBottom props, focusMarker guard
- Desktop layout completely unchanged
- Public share page gets the same combined view (read-only)
- Fix: entry editor now portaled to body (iOS stacking context)
- Fix: pros/cons dark mode input backgrounds
- Fix: mood button borders in dark mode
- Fix: location icon color (neutral instead of green/indigo)
- Add paddingBottom: var(--bottom-nav-h) to all mobile overlays that were
clipping content behind the bottom navbar: EntryEditor, SystemNoticeModal,
JourneyPage create modal, TodoListPanel sheets, TripPlannerPage
PlaceInspector, PackingListPanel bag modal, both PhotoLightboxes,
FileManager viewer, and shared Modal primitive
- Replace single-notice mobile bottom sheet with a 3-slot horizontal strip
so adjacent notices are physically present during drag
- Add live-follow swipe left/right to navigate between notices with
spring-back when under threshold and flushSync to eliminate blink on commit
- Add live-follow swipe down to dismiss all notices with spring-back;
backdrop tap also triggers the slide-down animation
- Normalize notice height with useLayoutEffect minHeight on strip and
align-items: stretch so all slots are always the tallest notice height
- Pin CTA button at consistent Y across notices via flex-1 + mt-auto;
always render invisible Not now placeholder to equalise CTA section height
- Move pager dots/counter below CTA buttons
Personal note from the creator shown as the first page in the 3.0
upgrade modal. Includes community links (Discord, Ko-fi) and a
special shout-out to jubnl. Modal UX improved: users must click
through all pages before dismissing, wider layout, enhanced
markdown rendering with styled links, signature, and HR separator.
i18n coverage across all 15 languages.
- Extract _fetchAllSynologyAlbums helper that loops until the source is
exhausted; listSynologyAlbums now uses it for personal, shared-out,
and shared-with-me instead of a hard-capped single request of 100
- Make getSynologyAssetInfo targetUserId required (number, not number|undefined)
to match every call site and eliminate an implicit any at the _requestSynologyApi
boundary
Scrollbars on mobile caused layout shift (content pushed left).
Hidden via media query on mobile; desktop retains thin styled scrollbars.
Also removes inline scrollbarWidth override in DayPlanSidebar that bypassed the CSS rule.
Pre-seed the store into loading state before render instead of relying on
timing. RTL's render() flushes all microtasks via act(), so the MSW response
lands before render() returns, leaving no observable loading window.
Rewrites downloadJourneyBookPDF to render the preview in an in-page srcdoc
iframe overlay instead of calling window.open(), which Safari iOS PWA blocks
in async callbacks. Matches the existing TripPDF pattern. Fixes#679.
backdrop-filter: blur() on position:fixed elements is a known Safari iOS
compositing failure in standalone (PWA) mode. When the GPU layer behind
a fixed overlay is uninitialized, the blur samples white instead of the
actual content, overriding the semi-transparent background and rendering
a fully white screen that requires a force-close to escape.
The JourneySettingsDialog (bottom-sheet on mobile) was most affected due
to its items-end layout, but all five modal overlays in JourneyDetailPage
had the same pattern. Removed backdropFilter from all five and bumped
opacity from 0.6 to 0.75 to maintain visual separation. Closes#678.
The /search route was looping up to 20 pages server-side, returning a
blob of up to 1000 photos with no hasMore flag, which prevented the
client's existing ScrollTrigger infinite scroll from ever firing.
Now the route proxies the client's page param directly to Immich and
returns a single page plus hasMore, enabling full library browsing.
The photo picker grid now groups photos by takenAt date (already
present in every asset response) with a date label above each group,
restoring the date-oriented browsing from V2. Closes#674.
loadJourney previously set loading=true unconditionally, causing the
JourneyDetailPage guard (if loading || !current) to unmount the entire
page tree on every background refetch — entry saves, settings saves,
trip link/unlink, contributor invite, delete, and WS realtime events
all triggered the full-page spinner flash.
Now loading is only toggled on cold loads (current?.id !== id).
Warm refreshes replace current silently so the hero, sidebar, map,
and timeline stay mounted throughout. Closes#673.
Adds a warn-severity modal notice targeting existing users who have the
MCP addon enabled. Communicates that OAuth 2.1 is now the recommended
auth method, static trek_ tokens are deprecated, and the toolset has
been significantly expanded. Priority 75 — slots between v3-journey and
v3-features in the upgrade modal sequence. Translations for all 15 languages.
Server-side notice registry with per-user condition evaluation (firstLogin,
existingUserBeforeVersion, addonEnabled, dateWindow, role, custom).
Notices are sorted by priority then severity, filtered against dismissals
stored in a new user_notice_dismissals table, and served via
GET /api/system-notices/active + POST /api/system-notices/:id/dismiss.
Client renders notices through a host component that partitions by
display type (modal / banner / toast). The modal renderer supports
multi-page pagination with directional slide transitions, keyboard
navigation, and correct dismiss-all semantics on CTA / X / ESC.
Dismissals are optimistic with a single background retry.
Includes 3.0.0 upgrade notices (v3-photos, v3-journey, v3-features),
onboarding welcome modal, and full i18n coverage across 15 languages.
The /journey route is addon-gated on both client and server.
Also includes: unit + integration test suites, registry integrity test
that validates action CTA IDs against client source, and technical
documentation in docs/system-notices.md.
- Add check_in_end column to day_accommodations (Migration 102)
- Server: create/update accommodation accepts check_in_end
- Bidirectional sync: check_in_end synced between accommodation
and linked reservation metadata (check_in_end_time)
- DayDetailPanel: shows check-in range (e.g. "14:00 – 22:00"),
new "Until" time picker in hotel form
- ReservationModal: new check-in-until field for hotel bookings
- ReservationsPanel: displays check-in range in metadata cells
- i18n: checkInUntil keys in all 15 languages
Closes#366
- Hide type filter pills on mobile (< md breakpoint)
- Move add button right-aligned on mobile
- Separate booking code into its own row below date/time
- Hide weekday in date on mobile for space
- Reduce padding on mobile
- Unified toolbar with title, type filter pills (with count badges),
and add button in one row
- Cards redesigned: labeled fields in rounded boxes, status/type in
header, edit/delete actions right-aligned
- Responsive grid with max 3 columns, auto-filling full width
- Type filters persist in sessionStorage per trip
- Widen reservations tab container to match other tabs (1800px)
Add a "No Category" option to the category filter dropdown in the
places sidebar, allowing users to filter for places without an
assigned category. The filter is synced with the map view.
Closes#607