Fix#802: ProviderPicker modal now uses dvh-based max-height, items-end
on mobile (bottom-sheet), flex-shrink-0 on all fixed sections, min-h-0
on the scrollable grid, and env(safe-area-inset-bottom) padding so the
Add button is always reachable above the iOS home indicator.
Fix#819: Gallery view now deduplicates photos by photo_id (underlying
trek_photos.id) so a photo linked from Gallery into an activity no longer
appears twice. Gallery delete cascades to all copies. EntryEditor From
Gallery grid and photo count also deduplicated. Server photo_count uses
COUNT(DISTINCT photo_id). Preserves #729 guarantee (removing from an
activity does not delete the Gallery copy).
- Track dirty state (title/subtitle changed from original)
- Intercept X button, backdrop click, and Cancel with handleClose
- Show ConfirmDialog when dirty; proceed with onClose only on confirm
- Add common.discardChanges and common.discard keys to all 15 locales
- sidebarMapItems now derives dayIdx from all timeline dates (not just
located-entry dates), so markers stay color-aligned with day headers
even when some days have no location
- scroll-sync no longer calls highlightMarker for unlocated entries,
preventing the map from clearing or misfiring when the scroll winner
has no corresponding marker
- same dayIdx fix applied to JourneyPublicPage desktop two-column view
formatDate() in both JourneyDetailPage and JourneyPublicPage passed
undefined/'en' as the locale to toLocaleDateString, so weekday/month
names always followed the browser's language instead of the app's
selected UI language. Thread the selected locale through from
useTranslation() in both pages.
Public view still falls back to 'en' when no settings locale is
available (shared links can be opened by anyone).
- Budget table column alignment: the NAME data cell had
`display: flex` directly on the <td>, which pulled it out of the
table-layout and desynced the column widths between data rows and the
AddItemRow. Moved the flex wrapper into a <div> inside the cell.
Closes#759
- Packing list: template-apply and bulk-import handlers called
`window.location.reload()` to refresh the list, which re-rendered the
whole trip loading screen. Both flows now merge the returned items
into the trip store instead. Closes#760
- Journey timeline: move-up / move-down arrows were rendered on
skeleton suggestions — skeletons are places from the linked trip and
don't participate in sort order. Skip canReorder when
entry.type === 'skeleton'. Closes#763
- Journey public view: the synthetic `[Trip Photos]` and `Gallery`
entries produced by syncTripPhotos were leaking into the public
timeline and map. The owner view already strips these in
JourneyDetailPage — apply the same filter on JourneyPublicPage.
Gallery photos still come from every entry so a shared gallery keeps
showing the trip-synced photos. Closes#764
- Journey thumbnails: public gallery grid was loading the original
asset for every tile. `photoUrl()` now takes an optional kind and the
grid requests `thumbnail`; the lightbox still opens the original.
Synology thumbnail default bumped from `sm` (240px) to `m` (320px)
because `sm` looked pixelated on retina. Closes#761
The 'From Gallery' picker on the journey entry editor used `aspect-square`
on grid items inside an overflow-scrolling container. Safari (desktop and
iOS) collapses the computed height of aspect-ratio boxes in this layout,
which stacked every thumbnail at y=0 — making selection impossible.
Swap to the classic padding-top spacer pattern (`paddingTop: '100%'` on
the cell + absolutely positioned image) which is bulletproof across
browsers and preserves the 5/6-column grid on mobile/desktop.
- 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.
- mobile: journey and gallery views both run as chromeless overlays now.
The hero card, backlink, stats row and inline tab-bar are hidden; the
floating top bar (back, Journey/Gallery toggle, settings) handles
branding for both views, and the gallery content gets a top padding
that matches the bar so nothing is occluded.
- the journey-title pill below the tab-toggle is removed — the toggle
itself is enough; the pill just duplicated information.
- JourneyMap tile layer: set updateWhenIdle:false and keepBuffer:4.
Leaflet defaults to "wait for pan to settle before loading tiles" on
mobile, which showed as a visible tile-lag when switching timeline
cards (flyTo moves the map). Eager updates plus a wider off-screen
ring keep the neighbouring tiles hot.
- mobile-shorten 'Alle Fotos' → 'Alle' in MemoriesPanel picker and the
Journey ProviderPicker filter tabs (four tabs no longer wrap)
- mobile-shorten 'Datum wählen' → 'Datum' in the entry-editor DatePicker
placeholder
- guard JourneyMap.tsx flyTo: getZoom() throws "Set map center and zoom
first" when activeMarkerId arrives before fitBounds has set a view —
wrap in try/catch and fall back to setView.
Mobile UI:
- #722 timeline carousel no longer cut off by BottomNav (uses --bottom-nav-h var)
- #723 scroll-snap-type relaxed to proximity so small swipes no longer skip entries
- #724 defensive padding-bottom fix in JourneySettingsDialog for iOS PWA
- #725 add back/settings buttons + journey title subtitle to mobile activity view
- #726 active entry re-centers after scroll settle; tap inactive card activates
it (does not jump straight into editor)
Entry editor flow:
- #727 photo uploads queue locally until Save for existing entries too
(previously fired upload immediately; Cancel silently kept the new photo)
- #728 Cancel/Close with unsaved changes now requires confirm (window.confirm)
- #729 linking a Gallery photo into an entry now copies the row (old MOVE
behavior meant Remove-from-Entry also nuked the Gallery original)
- #731 addPhoto / addProviderPhoto / linkPhotoToEntry promote skeleton
entries to concrete 'entry' type when content is added
Permissions:
- #732 updateJourney switched from canEdit to isOwner — editors can still
edit entries and photos, just not the journey shell (title, cover, status)
- #733 Contributors list gains a per-row remove (X) control with confirm
- #734 my_role is computed server-side and returned with the journey; UI
gates Settings/Add/Edit/Delete controls based on role
- #736 createOrUpdateJourneyShareLink + deleteJourneyShareLink now require
isOwner (previously NO permission check at all — anyone authenticated
could publish or unpublish a journey)
Immich upload (#730):
- migration 111: add users.immich_auto_upload (default 0)
- migration 112: seed provider_field for the toggle (idempotent, FK-safe)
- journey photo upload only mirrors to Immich when the user has opted in
- Settings UI gets a "Mirror journey photos to Immich on upload" checkbox
Test updates:
- JOURNEY-SVC-019 inverted to assert editor cannot update journey settings
- JOURNEY-SHARE-007 now passes userId (owner) to deleteJourneyShareLink
- FE-PAGE-JOURNEYDETAIL-148 inverted to assert photos stay pending until Save
- client/tests still green (2676/2676)
Also fixed en route: gallery entry title is now the literal 'Gallery' on the
wire (used to send the translated label, which broke server-side title === 'Gallery'
checks in non-English locales); confirm interpolation uses {username} single
braces matching the existing i18n runtime; Settings footer uses icon-only
delete/archive buttons on mobile so the row doesn't wrap.
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)
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).
- 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
- 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
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.
- Add --bottom-nav-h CSS token (84px + safe-area on mobile, 0px on desktop)
to give all fixes a single source of truth for the nav height
- Apply token to JourneySettingsDialog (fixes#650) and PlacesSidebar
day-picker sheet so bottom-anchored sheets clear the nav bar
- Add paddingBottom to TripPlannerPage Bookings, Lists, and Budget tab
scroll containers so content can be scrolled past the nav
- Bump Modal z-index from z-50 to z-[200] so modals render above the
bottom nav (both share z-50 with nav winning by DOM order)
- Add flex-wrap to settings footer so delete button stays visible when
translated labels (Dutch, German, French) overflow the single row
- Replace no-op pb-safe class with env(safe-area-inset-bottom) inline
style so dialog clears the iOS home indicator on iPhone
Fixes#648, #649
- Stop pagination on fetch error (set hasMore=false on non-ok response or catch)
- Set hasMore=false when loading album photos (albums load all at once)
- Hide ScrollTrigger when viewing album photos to prevent timeline photo leak
- Prevent background scroll-through with overscroll-contain and touch event handling
- Use bottom-sheet style on mobile (rounded-t, items-end) for better reachability
- Add extra bottom padding for mobile navbar safe area
- Close dialog when tapping overlay background
- Guard provider badge with truthy check to handle null/undefined provider
- Use explicit provider name matching instead of binary immich/synology fallback
- Show "Show more" button on both mobile and desktop when entry text is clamped
- Add "Show less" button when expanded to collapse back
- Add useTranslation hook to ExpandableStory component
- Add i18n keys common.showMore and common.showLess for all 14 languages
- Show spinner and "Uploading..." text on photo upload button in entry editor
- Show spinner on gallery view upload button during upload
- Disable upload buttons while upload is in progress
- Add i18n key journey.editor.uploading for all 14 languages
- Revert filled skeleton entries back to skeleton on delete instead of permanently removing them
- Add per-user hide_skeletons preference on journey_contributors (migration 99)
- Add PATCH /journeys/:id/preferences endpoint for toggling skeleton visibility
- Add Eye/EyeOff toggle button with custom tooltip in journey detail header
- Filter skeleton entries from timeline when hidden
- Add i18n keys for all 14 languages
- Enable attributionControl and add OSM attribution to JourneyMap TileLayer
- Memoize sidebar map entries array to prevent unnecessary map rebuilds
- Use stable callback reference for onMarkerClick
jsdom replaces globalThis.AbortController with its own implementation;
Node.js undici-based fetch validates signals via instanceof against the
native AbortSignal, causing fetch to throw before MSW could intercept.
Fix via custom Vitest environment (tests/environment/jsdom-native-abort.ts)
that captures native AbortController/AbortSignal before jsdom patches them
and restores them after jsdom setup.
Also updates JournalBody test 004 to match component behaviour (headings
rendered as <p>) and removes debug console.log statements.
- broadcastJourneyEvent now excludes by socket ID instead of user ID,
so other devices of the same user receive real-time updates (#615)
- Routes pass x-socket-id header through to broadcast functions
- loadJourney handles 404 gracefully — redirects to /journey with
toast instead of infinite spinner (#616)
- Gallery/timeline load thumbnails instead of originals (50-100KB vs 2-5MB)
- Batch endpoint for adding multiple provider photos in one request
- Optimistic photo deletion — no full page reload on delete
- Immich albums include shared albums
- Select-all button moved outside scroll container (always visible)
- Album tab loads actual album contents via /albums/:id/photos
Replace bulk-loading all Immich photos (up to 20k) with paginated
search: 50 photos per page, automatic infinite scroll via
IntersectionObserver. Prevents server blocking on large libraries.
- Backend: searchPhotos accepts page/size params, returns hasMore
- Frontend: loads 50 at a time, appends on scroll
- AbortController cancels in-flight requests on tab switch
Large Immich libraries (7k+ photos) caused timeouts and pending
requests when using "All Photos". Cap pagination at 5 pages (5000
photos) and abort in-flight requests when switching tabs.
- Load actual album photos instead of date-range search fallback
(new GET /albums/:id/photos for Immich + Synology)
- Add select all / deselect all toggle in photo picker
- Normalize Markdown headings to plain text in journal stories
- Fix setext headings (---) rendering as hr instead of h2
- Add remark-breaks for proper line break rendering
- Fix pros/cons dark mode gradient backgrounds
- i18n: selectAll/deselectAll in 14 languages
- Render h1/h2/h3 as plain paragraphs — journal stories are plain
text, not structured documents
- Preprocess text to insert blank line before --- and === so they
become horizontal rules instead of setext headings
Introduce trek_photos as central photo registry. Frontend uses
/api/photos/:id/:kind instead of provider-specific URLs. Adding
a new photo provider is now backend-only work.
- New trek_photos table (migration 98) with photo_id FK in
trip_photos and journey_photos
- Unified /api/photos/:id/thumbnail|original|info endpoint
- photoResolverService for central resolution and streaming
- ProviderPicker: add "All Photos" tab, rename tabs, fix i18n
- Localize all hardcoded strings in JourneyDetailPage (14 langs)
- Fix date formatting to use browser locale instead of hardcoded 'en'
- Journey stats as styled tile cards
- Map tooltips now respect light/dark mode via CSS variables
- Journey creation inherits cover image from first selected trip
- Only day-assigned places are synced to journey (no unplanned places)
- Place count in trip picker reflects assigned places only
- Contributor avatars shown in journey detail page
- Suggestion banner button visible in dark mode (!important override)
- Dashboard list view uses correct trips array and status label
- Replace all remaining hardcoded strings in JourneyDetailPage JourneySettingsDialog with t() calls
- Add 14 missing translation keys to all 13 non-English language files
(trips.member*, common.expand/collapse, inspector.remove, memories.*, journey.*)
- Fix common.loading and common.saving to use Unicode ellipsis (…) instead of three dots (...)
- Update 4 test files that expected three-dot ellipsis to use Unicode ellipsis
- All 2541 tests passing
- 5-table schema (journeys, entries, photos, trips, contributors) with migrations 87-91
- Trip-to-Journey sync engine with skeleton entries and photo sync
- Full CRUD API for journeys, entries, photos with Immich/Synology integration
- Timeline, Gallery and Map views with entry editor (markdown, mood, weather, pros/cons)
- Journey frontpage with hero card, stats and trip suggestions
- Public share links with token-based access and photo proxy
- PDF photo book export (Polarsteps-inspired)
- Dashboard redesign: mobile greeting, live trip hero, quick actions, unified card design
- BottomNav profile sheet with settings/admin/logout
- DayPlan mobile inline place picker
- TripFormModal members management
- Vacay calendar trip date indicator dots
- Fix contributor photo access (403) for journey Immich/Synology photos
- Trip deletion cleanup for journey skeleton entries
- i18n: 231 new keys across all 14 languages (native translations, no fallbacks)