**#541 — File downloads broken in PWA standalone mode**
Replace getAuthUrl + window.open pattern with blob-based fetch using
credentials:include. The old approach minted a 60s single-use ephemeral
token then called window.open, which handed the URL to the system browser
on Android/iOS — losing the PWA cookie jar and producing "invalid or
expired token". The new approach fetches the file directly inside the
PWA WebView as a blob URL, so no auth handoff occurs.
New helper client/src/utils/fileDownload.ts with downloadFile and openFile.
Updated FileManager, ReservationsPanel, ReservationModal, PlaceInspector,
CollabNotes.
Security hardening in fileDownload.ts:
- assertRelativeUrl() guard prevents credentials being sent to external hosts
- openFile() checks blob.type against a safe-inline allowlist; HTML, SVG and
other script-capable MIME types are forced to download instead of being
opened inline, preventing same-origin XSS via blob URLs
- resp.ok check covers all non-2xx responses, not just 401
**#505 — PWA offline session lost on reload**
Wrap authStore with Zustand persist middleware, serializing only
{user, isAuthenticated} to localStorage key trek_auth_snapshot.
maps_api_key is intentionally excluded from the snapshot.
On cold start with no network: persist hydrates isAuthenticated:true,
App.tsx clears isLoading and calls loadUser({silent:true}), ProtectedRoute
renders the dashboard immediately. The network error from loadUser leaves
isAuthenticated intact so no login redirect occurs.
On 401 or logout: store state is cleared, persist writes
{isAuthenticated:false} — stale snapshot does not grant offline access
after session expiry.
- 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
- Remove memories providers from trip addons section
- Show Immich/Synology as sub-items under the Journey global addon
- Same pattern as bag tracking under packing list
- 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
- testSmtp now surfaces real nodemailer error instead of generic 'SMTP not configured' on send failure
- admin webhook test button uses correct i18n key (was showing 'Test-E-Mail senden' in all languages)
- backup created_at uses stat.mtime instead of unreliable stat.birthtime on Linux
- testSmtp now surfaces real nodemailer error instead of generic 'SMTP not configured' on send failure
- admin webhook test button uses correct i18n key (was showing 'Test-E-Mail senden' in all languages)
- backup created_at uses stat.mtime instead of unreliable stat.birthtime on Linux
Add missing notes (and other fields) to client Place type so the field
is correctly typed when hydrating the edit form. Fix PlaceInspector to
show description and notes as separate blocks so notes are no longer
hidden when a place also has a description.
Resolves#595. The PDF builder filtered reservations through a transport-only
allow-list, silently dropping all non-transport types. Replace the allow-list
with a single hotel exclusion (hotel is already covered by the accommodations
block) so every other reservation type now appears in the daily itinerary.
Add per-type icon and accent colour matching the existing ReservationsPanel
palette, and per-type subtitle builders (party size, venue, operator) plus a
generic location line for future use.
- 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.
IMMICH-057: use two-step trek_photos/trip_photos insert (same fix
as SYNO-035) to avoid missing asset_id column error.
IMMICH-061: mock regex /\/api\/albums$/ did not match the ?shared=true
variant; updated to /\/api\/albums(\?.*)?$/ so both owned and shared
album requests resolve correctly.
IMMICH-090: /search route only fetched a single page; implement
internal pagination loop (max 20 pages) accumulating all assets
before responding, which is what the test and the feature require.
trek_photos is now the central registry; trip_photos and journey_photos
reference it via photo_id FK. Updated all affected test helpers and
direct-SQL assertions to join trek_photos instead of querying stale
columns (asset_id, provider, owner_id) on the leaf tables.
Also fix ATLAS-UNIT-019: getVisitedRegions now fires background geocoding
and returns immediately, so the test must call it twice — once to trigger
the fill, once after advancing fake timers to read cached results.
Both trip_photos (alias tp) and trek_photos (alias tkp) have a provider
column. Using the bare identifier 'provider' in the JOIN condition was
ambiguous and caused SQLite to throw SQLITE_ERROR, failing migration 98
and taking down the entire test suite setup.
Fix: introduce providerJoinExpr = 'tp.provider' when the legacy
trip_photos table already carries a provider column, used only in the
two-table JOIN. The single-table INSERT keeps the unqualified form.
- Extract throttleNominatim() so reverseGeocodeCountry and
reverseGeocodeRegion share the same lastNominatimCall state.
Concurrent /stats + /regions no longer interleave requests
faster than 1 req/s, closing the remaining 429 path from #576.
- getVisitedRegions now returns cached data immediately and fills
uncached places in a fire-and-forget background loop. Eliminates
the N×1.1s response time that caused 504s behind reverse proxies
(likely root cause of #493). geocodingInFlight set prevents
double-enqueuing on concurrent page loads.
- Add AbortSignal.timeout(10_000) to both Nominatim fetch calls so
a hung upstream no longer stalls the endpoint indefinitely.
- Unify User-Agent header in reverseGeocodeRegion to match policy.
- 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.
Migration 98 assumed trip_photos already had asset_id + provider
columns, but older DBs still have the original immich_asset_id
column. Now detects schema variant and adapts accordingly.
- 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