navigator.onLine returns false transiently during service worker activation
(skipWaiting + clientsClaim), causing all repo refresh IIFEs to return null
immediately on first page load — leaving the UI with empty data until F5.
Fixes applied across all list repos (trip, day, place, packing, todo, budget,
reservation, accommodation, file):
- Drop navigator.onLine guard; let fetch fail naturally when truly offline
- Await all upsert calls (some were fire-and-forget, risking race conditions
against subsequent reads and silent swallowed failures)
- Return Promise.resolve(null) instead of Promise.resolve(fresh) in the
IDB-empty network path, so loadTrip's background refresh Promise.all
resolves null and skips set({trip}), preventing a spurious reference change
that was resetting the 1500ms splash timer
Tests updated: placeRepo and packingRepo "empty cache" tests now simulate
genuine network failure (HttpResponse.error) instead of relying on the
navigator.onLine guard that no longer exists; DashboardPage tests clear IDB
before each test and use a query-safe assertion after background refresh.
All create/update/delete repo methods now write to IndexedDB optimistically
and fire mutationQueue.flush() as fire-and-forget, returning immediately
without waiting for the network. This eliminates the 8-second UX freeze
previously seen when the API was unreachable but navigator.onLine was true.
- Repos rewritten: trip, day, place, packing, todo, budget, accommodation,
reservation, file — write methods never throw, always return optimistic data
- mutationQueue.flush() changed to iterative (one item per loop iteration)
so mutations enqueued mid-flush (e.g. bulk check-all) are picked up
- fileRepo.toggleStar skips the IDB put when the file is not cached locally
- DayDetailPanel passes place_name into accommodationRepo.create so the
optimistic accommodation renders the correct hotel label immediately
- Test suite updated throughout to reflect optimistic-first semantics:
no more rollback assertions, IDB cleared in component test beforeEach hooks,
FileManager tests switched from filesApi spy to MSW endpoint assertions
Add navigator.onLine guard to SWR refresh IIFEs so background
network calls don't fire in offline mode (prevents fake-IDB leakage
in tests via MSW default handlers).
Fix IDB isolation in affected test files by flushing pending macro
tasks then clearing IDB tables in beforeEach, so stale IDB writes
from previous tests' background IIFEs don't bleed into the next test.
Restore loadBudgetItems and refreshPlaces to apply background refresh
results to store state.
Move tags/categories API calls before the main Promise.all in
loadTrip so MSW handlers resolve during the await window.
window.open with noreferrer returns null, which triggered the popup-blocked download fallback in addition to the new-tab open. Use a target=_blank anchor click instead.
Replaced loose includes()/startsWith() path checks with exact equality
for static routes and strict prefix matching for dynamic-token routes.
Added /forgot-password and /reset-password to the allowlist so the
password-reset flow is usable without auth. Extracted isAuthPublicPath
as a pure testable function with 14 unit tests covering regressions.
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/.
- Translate all 1941 keys to Bahasa Indonesia (up from ~426)
- Add 437 keys missing since PR was opened (journey.*, oauth.scope.*,
dashboard.mobile.*, settings.oauth.*, admin.oauthSessions.*, etc.)
- Remove 2 stale keys superseded by unified file-import flow
- Fix duplicate packing.assignUser entry
- Rename const en → const id, update export default
- Update SUPPORTED_LANGUAGES length assertion in i18n unit test (14→15)
**#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.
jsdom's FormData is incompatible with undici's ReadableStream serialisation
used by MSW 2.x — requests hang under CI resource constraints but pass locally.
Replace server.use() + implicit HTTP roundtrip with vi.spyOn().mockResolvedValueOnce()
for all five FormData POST tests (uploadAvatar, uploadRestore, addFile, importGpx).
Node 22 URL.createObjectURL strictly requires a native node:buffer Blob
and throws ERR_INVALID_ARG_TYPE when given a jsdom Blob (caught by
fetchImageAsBlob, returning ''). Node 24 relaxed this check, masking the
failure locally.
Tests 007, 011: replace MSW/Response-based fetch mocks with direct
vi.spyOn(fetch) mocks returning node:buffer Blobs via a duck-typed
response object. The real URL.createObjectURL now handles the correct
Blob type and returns a genuine blob: URL on all Node versions.
Test 012: URL.createObjectURL identity varies across Node versions
making it impossible to spy on reliably. Replace createObjectURLSpy
assertion with a completedFetches counter in the fetch mock, which
proves the same semantic guarantee (6 requests ran, 7th was cleared).
setup.ts: restore the original conditional guard so the vi.fn fallback
only applies when URL.createObjectURL is completely absent, not
overwriting a working real implementation.
In jsdom, source modules resolve bare 'URL' identifiers through
window.URL (the jsdom window object), not through globalThis.URL (Node's
URL class). On GitHub Actions these are distinct objects, so all prior
attempts (Object.defineProperty, direct assignment, vi.stubGlobal) were
patching the wrong object and failing silently.
Changes:
- setup.ts: Object.defineProperty targets window.URL so the vi.fn mock
is visible to authUrl.ts at call time
- authUrl.test.ts: drop vi.stubGlobal approach; add vi.clearAllMocks()
to reset accumulated call counts on the setup.ts vi.fn between tests;
fix vi.spyOn target to window.URL in test 012
Direct property assignment and Object.defineProperty both fail
silently on CI when jsdom marks URL.createObjectURL as non-writable
and non-configurable. vi.stubGlobal('URL', ...) replaces globalThis.URL
entirely — which always succeeds — while extending the real URL class
so all URL parsing behaviour is preserved. vi.unstubAllGlobals() is
called at the start of beforeEach to reset cleanly between tests.
Two root causes:
1. authUrl.test.ts (007, 011, 012): Object.defineProperty in setup.ts
fails silently on CI when jsdom's URL.createObjectURL is
non-configurable. vi.restoreAllMocks() in beforeEach then restores
the property to jsdom's native implementation (returns '').
Fix: assign URL.createObjectURL = vi.fn(() => 'blob:mock') directly
in authUrl.test.ts's beforeEach, after restoreAllMocks(), so every
test in the file gets a fresh, reliable mock. Remove the now-
unnecessary mockClear() from test 012.
2. client.test.ts (013): MSW patches the global Response constructor and
calls blob.stream() on the body — a method not implemented by jsdom's
Blob. Fix: replace new Response(blob) with a plain-object duck-type
({ ok: true, blob: () => Promise.resolve(blob) }) to bypass the
patched constructor entirely.
Three interrelated issues caused 4 tests to pass locally but fail on CI:
1. setup.ts only applied the URL.createObjectURL stub when it was
undefined, but jsdom already defines it (returning ''). Changed to
always override with configurable:true so the predictable 'blob:mock'
value is set in every environment.
2. FE-API-013 used Object.defineProperty (non-configurable in jsdom) and
MSW to handle a native fetch call. Replaced with vi.spyOn for both
URL.createObjectURL/revokeObjectURL and a direct fetch mock, which is
more reliable across environments.
3. FE-COMP-AUTHURL-012's vi.spyOn(URL, 'createObjectURL') returned the
same vi.fn() instance set in setup.ts, accumulating calls from all
prior tests in the file (1+8+7+6=22 instead of 6). Added mockClear()
immediately after the spy setup to reset the count.