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
The booking-labels toggle from the transport-routes-on-map change was
reading and writing settings.map_booking_labels without the key being
declared on the Settings type, so the store typing was inconsistent.
Adds it as an optional boolean to match the other display toggles.
Closes the annoyance from discussion #510 — the planner opened every
trip centered on the global default, even when the trip's places were
on the other side of the world. We already have a BoundsController
that fits the map on the current places when fitKey changes, so
nudging fitKey once per trip (after the places have loaded) gives each
trip its own starting view without any new settings or UI. If a trip
has no places with coordinates yet, the global default still applies.
The unified toolbar gives the gear button a title attribute for a11y,
which broke the previous "no title, no text" matcher. Matching on the
lucide-settings icon plus an empty text node is enough to identify it
uniquely on this page.
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/.
Vacay: remove the filter-sidebar toggle from the desktop bar and shift
the breakpoint so the pre-existing mobile/tablet header (which still
has the toggle) handles everything below the lg threshold where the
sidebar is always visible anyway.
Journey: drop the desktop search toggle and inline search input from
the bar. Mobile search UI is untouched.
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
Allow admins to set instance-wide defaults for temperature unit, color
mode, time format, route calculation, blur booking codes, and map tile
URL via a new Admin > User Defaults tab. Defaults are stored in
app_settings (prefixed default_user_setting_*) and applied at read time
as a fallback — user's own explicit values always take priority.
Translations added for all 16 supported languages.
- Add missing admin.ntfy.hint translation key in all 15 languages
- Add admin ntfy server hint clarifying it is the default for users
- Expose admin_ntfy_server via PreferencesMatrix so user settings
placeholder reflects the admin-configured default
- Add clear token button to admin ntfy panel (same pattern as user settings)
- Extract common.clear from settings.ntfyUrl.clearToken across all 15 languages
Properly translate all ntfy-related UI strings added in the previous
commit for ar, br, cs, de, es, fr, hu, id, it, nl, pl, ru, zh, zhTw.
Product name 'Ntfy' and placeholder values kept as-is.
Adds ntfy.sh (and self-hosted instances) as a new push notification
channel with full parity to the existing webhook channel.
- Backend: NtfyConfig type, getUserNtfyConfig, getAdminNtfyConfig,
resolveNtfyUrl, sendNtfy (header-based API with Title/Priority/Tags/
Click headers), testNtfy, NTFY_EVENT_META (priority + emoji tags per
event), SSRF guard via existing checkSsrf + createPinnedDispatcher
- notificationPreferencesService: ntfy added to NotifChannel union,
IMPLEMENTED_COMBOS, getActiveChannels parser, getAvailableChannels,
ADMIN_GLOBAL_CHANNELS, and AvailableChannels interface
- notificationService: per-user ntfy dispatch after webhook block;
admin-scoped ntfy via getAdminGlobalPref for version_available events
- Routes: POST /api/notifications/test-ntfy with saved-token fallback
- authService: admin_ntfy_server/topic/token in ADMIN_SETTINGS_KEYS,
masked + encrypted on read/write
- settingsService: ntfy_token added to ENCRYPTED_SETTING_KEYS
- Frontend: ntfy topic/server/token inputs + Save/Test/Clear buttons in
NotificationsTab; admin Ntfy panel in AdminPage; testNtfy API method
- i18n: full English strings; English placeholders in 14 other locales
- Tests: resolveNtfyUrl, sendNtfy, dispatch integration, UI tests,
MSW handler for test-ntfy endpoint
- 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)
- Replace separate GPX and KML/KMZ import buttons with a single "Import
file" modal accepting all three formats, with a drag-and-drop drop zone
- Support dragging files directly onto the Places sidebar panel; overlay
appears on hover and pre-loads the file into the modal on drop
- Fix [object Object] description bug in KML imports caused by
fast-xml-parser returning mixed-content nodes as objects; add stopNodes
config and object guard in asTrimmedString
- Fix CDATA sections leaking into descriptions (e.g. "text.]]>") by
unwrapping CDATA markers before tag stripping
- Add import deduplication across all import paths (GPX, KML/KMZ, Google
list, Naver list): reimporting skips places already in the trip by name
(case-insensitive) or by coordinates (within ~11 m tolerance), with
intra-batch dedup so duplicate placemarks within the same file are
also collapsed
- Fix KML route returning 400 "No valid Placemarks found" when all
placemarks were valid but deduplicated; 400 now only fires when the
file contains zero placemarks
- Show a warning toast "All places were already in the trip" instead of
a misleading success toast when a reimport produces zero new places
(GPX, KML/KMZ, Google list, Naver list)
- Add 8 new i18n keys across all 14 locales; remove 11 keys made unused
by the modal consolidation
- Strip BOM (U+FEFF) from 14 translation files injected by editor
- Guard KMZ unpack against zip-bomb: check entry.uncompressedSize against
50 MB cap (KMZ_DECOMPRESSED_SIZE_LIMIT) before calling .buffer();
limit is an exported constant so tests can override it
- Fix non-BMP HTML entity decoding: replace String.fromCharCode with
String.fromCodePoint + 0x10FFFF bounds check so emoji like 😀
round-trip correctly
- Switch KML namespace stripping from regex to fast-xml-parser's
removeNSPrefix option; XMLValidator accepts namespaced XML natively,
making the pre-strip step unnecessary
- Remove dead skippedCount overwrite after transaction; per-loop
increment already tracks it alongside per-item error messages
- Type multer req.file as Express.Multer.File on both /import/gpx
and /import/map routes instead of (req as any).file
- Add unit tests: emoji entity decoding (decimal + hex), KMZ zip-bomb
rejection, KMZ-with-no-KML rejection
Resolves conflicts with Naver list import (PR #662) — kept both unified
list-import dialog and new KMZ/KML dialog. Dropped duplicate react-dom
import and unused CustomSelect import from PlacesSidebar.
- SSRF: validate user-supplied URLs with checkSsrf() before fetch in
both importNaverList and importGoogleList; upgrade naver.me substring
check to exact hostname comparison to prevent bypass
- i18n: add missing places.importNaverList key to de.ts and es.ts
- migration: switch Naver addon seed to INSERT OR IGNORE to preserve
admin customizations on re-runs; restore budget_category_order
CREATE TABLE to its original formatting
- route: remove redundant cast after type-narrowing guard in naver-list handler
- component: hoist provider ternary above try/catch in handleListImport
- tests: add four new Naver import cases (502, empty list, no-coords,
canonical URL skipping redirect fetch)
- Fix race condition: AbortController cancels in-flight autocomplete
requests on each keystroke; stale responses no longer overwrite fresh ones
- Remove acTrigger state hack; onFocus calls fetchSuggestions directly
- Cap autocomplete input at 200 chars server-side (400 on violation)
- Filter Nominatim suggestions with empty osm_id segments
- Revert getPlaceDetails OSM branch from unconditional parallel fetch to
conditional serial: Nominatim called only when Overpass lacks coords/address
- Wire places.loadingDetails i18n key to Loader2 spinner via aria-label/role
- Add tests: MAPS-017, MAPS-040c, MAPS-093, FE-MAPS-004
- Collapse A2_TO_A3_BASE + let A2_TO_A3 into a single const declaration;
the _BASE copy was vestigial (never read after the clone)
- Add a comment explaining the table's two sources and the load-bearing
invariant: countries whose Natural Earth record has ISO_A2='-99'
(France, Norway) must be listed here since the runtime augmentation
loop skips those features
- Refactor the France-only A3-fallback test fixture into a factory helper
and extend FE-PAGE-ATLAS-041 with a Norway (NOR) case via it.each
- Improve atlas_country_options useMemo: rename a2 → resolvedA2 for
clarity, precompute the A3→A2 reverse-lookup Map once per geoData
change instead of O(n) Object.entries().find() per feature
Removes the client-side guard that blocked toggling vacation entries on
public holiday dates, so users who work on holidays can still book leave.
Also adds a filled blue circle on today's date in the Vacay calendar for
quick orientation.
Closes#651
- 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
**#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
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.
- 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
Add remark-breaks plugin so single newlines in note content render
as <br> instead of being collapsed by Markdown. Applies to both
the card preview and the expanded view.
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
- Integrate a loading spinner for "Name" input field during place search.
- Enhance OpenStreetMap place detail retrieval with Nominatim lookup.
- Update `authStore` to track Google Maps API key presence.
The category filter bridge was collapsing Set<string> to a single
string, emitting '' (no filter) whenever more than one category was
selected. Map now uses the same Set-based membership predicate as the
sidebar list filter.
Closes#602
LOCALES is now built via Object.fromEntries from SUPPORTED_LANGUAGES,
so adding a new language only requires one change in supportedLanguages.ts.
Also types translations as Record<SupportedLanguageCode, ...> so TypeScript
enforces that every supported language has a translation entry.
Move language list to supportedLanguages.ts so TranslationContext and
settingsStore can import from a single source of truth, eliminating
the hardcoded array in setLanguageTransient.
- Use apiClient instead of raw fetch() in configApi.getPublicConfig
- Validate DEFAULT_LANGUAGE against supported codes on server startup
- Log warning instead of silently swallowing fetch errors in LoginPage
- Case-insensitive browser language matching in detectBrowserLanguage
- Guard against undefined navigator in detectBrowserLanguage
- Validate language code in setLanguageTransient before applying
- Import directly from TranslationContext instead of barrel index
Replace the language cycling button on the login page with a dropdown
showing all 14 supported languages. Add automatic browser/OS language
detection via navigator.languages, falling back to a configurable
DEFAULT_LANGUAGE env var, then 'en' as last resort.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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
Revert common.loading and common.saving from Unicode ellipsis (…) back to
three dots (...) to match the rest of the project (e.g. "Optional caption...").
Update 4 test files that were incorrectly using the Unicode ellipsis character.
- 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
Replace 6 hardcoded German strings in PhotoUpload.tsx with t() calls:
- 'Tag verknüpfen' → t('photos.linkDay')
- 'Kein Tag' / 'Tag N' → t('photos.noDay') / t('photos.dayLabel')
- '{N} Foto(s) ausgewählt' → t('photos.photoSelected/photosSelected')
- 'bis zu 30 Fotos' hint → t('photos.fileTypeHint')
- 'Wird hochgeladen...' → t('common.uploading')
Add all 6 new keys to all 14 language files and update test
assertions from German strings to English equivalents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests were asserting against hardcoded German strings that were replaced
with t() calls. Updated to match the English translation values rendered
by TranslationProvider in the test environment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix critical bug: Photos and Files pages had German text hardcoded in JSX,
now use t() keys visible correctly in all languages
- Add 16 new translation keys (photos/files UI, login validation, common errors,
rate limit message) across all 14 language files
- Add missing keys in packing, memories, and budget sections for br, de, it, es,
fr, nl, pl, cs, hu, ru, zh, zh-TW, ar
- Add 152+ missing keys for zh-TW (entire sections were absent)
- Change Vacay addon name to 'Férias' in pt-BR only
- Add client-side HTTP 429 interceptor that shows translated rate limit message
- Replace hardcoded English fallbacks in TripPlannerPage, DayPlanSidebar,
DisplaySettingsTab, MapSettingsTab, AccountTab, and TodoListPanel with t()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Export __clearVersionCacheForTests() from adminService; call in
versionNotification beforeEach to reset module-scoped cache between
tests (VNOTIF-002..006 failed because VNOTIF-001 cached
update_available:false, short-circuiting all subsequent test fetches)
- Seed appVersion:'2.9.10' in Navbar test authStore; appVersion moved
from local useEffect state to authStore in last commit so the test
render no longer fetches it independently (FE-COMP-NAVBAR-016)
- Add data-testid="weekend-days" to VacaySettings weekend-days
container; use within() in tests to scope button count to that
section, fixing false positives from the week-start buttons which
share the same inline styles (FE-COMP-VACAYSETTINGS-003/004)
- Pass isPrerelease={true} in GitHubPanel FE-ADMIN-GH-007; component
filters out prerelease releases when isPrerelease=false so the badge
was never rendered (pre-existing, unrelated to last commit)
- Type checkVersion() with VersionInfo interface; fixes TS errors in
checkAndNotifyVersion() where object type blocked property access
- Don't cache fallback on !resp.ok or fetch throw; prevents a transient
GitHub outage from poisoning the 5-min version cache
- Guard parseInt result with Number.isFinite() in compareVersions;
malformed -pre.abc tags no longer silently compare as equal via NaN
- Pre-compute stripped versions before sort in checkVersion(); avoids
mutating input array and redundant replace() calls in comparator
- Bump GitHub releases fetch from per_page=20 to per_page=100
- Store appVersion in authStore; populate from App.tsx getAppConfig call
and remove redundant getAppConfig fetch in Navbar useEffect
- Type GitHubPanel error/expanded state as string|null and Record<number,boolean>
- Add concurrency groups to both workflows to prevent parallel version-bump races
- Defer git tag push to merge job so orphan tags can't exist without a live image
- Pin build/merge jobs to the SHA captured in version-bump to prevent TOCTOU
- Guard auto-finalize in docker.yml against cross-major prereleases (requires bump=major + confirm_major=MAJOR)
- Add STABLE fallback to 0.0.0 for fresh repos with no stable tag
- Fix cleanup sort to extract numeric N via awk instead of fragile sort -t. -k4 -n
- Add 5-minute in-memory cache to checkVersion to avoid GitHub API rate limits
- Type GitHubPanel releases state; remove any cast on filter
- Quote all $VERSION/$MAJOR_TAG vars in imagetools create calls
- Add docker-dev.yml: prerelease CI for dev branch with minor/major bump
inputs; auto-continues in-flight major line via existing pre tags;
publishes floating major-pre Docker tag (e.g. 2-pre)
- Rewrite docker.yml version-bump: tag-based versioning, manual bump
inputs (auto/patch/minor/major), major guarded by confirm_major=MAJOR,
auto-finalizes in-flight prereleases; publishes floating major tag (e.g. 2)
- Inject APP_VERSION build-arg through Dockerfile so the running container
knows its real version instead of reading package.json
- Server reads APP_VERSION env in authService/adminService; exposes
is_prerelease in app config and update-check response; prerelease builds
compare against GitHub prerelease releases rather than latest stable
- Client stores isPrerelease from config; navbar shows amber version badge
on prerelease builds (left of dark-mode toggle); GitHubPanel filters out
prerelease releases unless the running build is itself a prerelease
EXIF was only available for Immich photos and inconsistent for local
uploads. Removed entirely for now — cleaner lightbox with just photo,
nav, counter, and caption. Nav buttons now show on hover (desktop)
and always on mobile.
- New setting in Vacay Settings to choose Mon or Sun as week start
- DB migration adds week_start column to vacay_plans (default: Monday)
- Calendar grid and weekday headers adapt to the selected start day
- Weekend column highlighting works correctly for both modes
- Translations added for all 14 languages
Use position:fixed with calculated coordinates instead of
position:absolute so the dropdown escapes the overflow:hidden
container. Also adds a backdrop to close on outside click.
When a place has a google_place_id, the Maps link now uses the place
name + query_place_id for an exact match. Falls back to lat,lng
coordinates when no google_place_id is available.
- Mobile hero now shows spotlight trip (next upcoming / ongoing) instead of only ongoing
- Reuse SpotlightCard component for mobile hero (same as desktop)
- Smaller status badges on non-hero trip cards (9px text, compact padding)
- CircleCheck icon for completed trips instead of Clock
- Add new fields to AppConfig type and buildAppConfig factory
- Update FE-PAGE-ADMIN-018: heading changed to "Authentication Methods"
- Update FE-PAGE-ADMIN-053: oidc_only toggle removed from OIDC panel
- Update FE-PAGE-LOGIN-007/017: mocks now include password_login/oidc_login
- Update ADMIN-SVC-049: updateOidcSettings no longer writes oidc_only
Replaces the coarse oidc_only + allow_registration settings with four
independent toggles: password_login, password_registration, oidc_login,
oidc_registration. Each can be enabled/disabled individually in
Admin > Settings without affecting the others.
- Add resolveAuthToggles() in authService.ts as the central resolver;
falls back to legacy oidc_only/allow_registration keys when new keys
are absent (backward compat)
- OIDC_ONLY env var still works and overrides DB toggles for password_*,
with a visual lock in the admin UI when active
- Server enforces lockout prevention: cannot disable all login methods
- oidc_login gate added to OIDC /login and /callback routes
- Remove oidc_only toggle from OIDC settings panel; replaced by the
granular toggles in the Settings tab
- Add 6 new resolveAuthToggles() unit tests; fix AUTH-DB-033 error
message assertion
- Update OIDC_ONLY descriptions in README, docker-compose, Helm values,
Unraid template, and .env.example to clarify override semantics
Closes#492
- Fix#521: `isVisitedFeature()` now scopes name-based region matching to
the feature's parent country (via `iso_a2`), preventing same-name regions
in different countries (e.g. Luxembourg BE vs LU) from falsely lighting up
- Fix#489: Add ~50 missing countries to COUNTRY_BOXES, NAME_TO_CODE, and
CONTINENT_MAP so the bounding-box fallback correctly identifies Georgia
instead of falling through to Russia/Azerbaijan's overlapping boxes
- DASH-016/017: Spotlight trip not in list view — test non-spotlight trip instead
- DASH-021: New trip appears in both mobile + desktop — use getAllByText
- Add title attributes to action buttons in SpotlightCard, MobileTripCard, TripCard
so tests can find them by accessible name (edit, delete, archive, copy)
- Remove FE-PAGE-PLANNER-018 test — MemoriesPanel moved to Journey addon
- 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)
12 of 14 language files showed 'Immich-Einstellungen gespeichert' (or
equivalent) instead of the actual provider name when saving settings.
The frontend already passes provider_name to the translation function;
only the translation strings were wrong.
- Fix endpoint path: users now provide full base URL (e.g. https://nas:5001/photo)
- Add OTP/2FA field for Synology login
- Add skip SSL verification option (DB column + checkbox UI)
- Add device ID (synology_did) column for session tracking
- Trigger in-app notification when Synology session is cleared
- Show disconnection banner in MemoriesPanel
- Add URL hint in provider settings
- Map Synology API error codes to human-readable messages
- Update i18n for all locales
Adds new and expanded test suites across client and server to cover the
OAuth 2.1 scope system, MCP session manager, collab service, unified
memories helpers, OIDC service, budget slice, and OAuth authorize page.
Also extends SonarQube coverage exclusions to include bootstrapping files
(migrations, scheduler, main.tsx, types.ts) that are not meaningfully
testable.
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).
- Split `media:read` into `geo:read` and `weather:read` scopes
- Add dedicated `atlas:read/write` scopes (previously under `places`)
- Add dedicated `todos:read/write` scopes (previously under `collab`)
- Rate limiting now keyed by userId+clientId instead of userId alone
- Bind MCP sessions to the OAuth client that created them
- Log MCP tool calls to audit log with clientId
- Invalidate all MCP sessions on addon state change
- Reduce session sweep interval from 10min to 1min
- Update all translations with new scope labels
Show first 6 scope badges per session with a clickable "+N more" pill
that expands to all scopes; a "show less" pill collapses them again.
Also fix column alignment to items-start so Owner/Created stay at the
top of tall rows.
Show active OAuth sessions (first) and static API tokens (second) in
the admin MCP Access tab. Admins can revoke any OAuth session, which
immediately terminates the live MCP transport for that client.
- Add admin-level listOAuthSessions / revokeOAuthSession in adminService
- Add GET /admin/oauth-sessions and DELETE /admin/oauth-sessions/:id routes
- Restructure AdminMcpTokensPanel into two sections; rename tab to MCP Access
- Fix stale writeAudit call in rotate-jwt-secret route (user_id → userId)
- Add admin.oauthSessions.* i18n keys across all 14 locale files
OAuthRegisterPage and its server routes (GET /api/oauth/register/validate,
POST /api/oauth/register) are superseded by the RFC 7591 machine-to-machine
DCR endpoint (POST /oauth/register). Claude.ai and compliant MCP clients
register via RFC 7591, then go through the standard /oauth/authorize consent
screen for scope selection.
Settings-created clients have fixed scopes chosen at creation time and
should show a read-only scope list on the consent screen. Only DCR-registered
clients expose the interactive checkbox UI for user-controlled scope selection.
When an MCP client registers via DCR and redirects the user to authorize,
the consent screen now shows checkboxes instead of a read-only scope list.
The user can grant any subset of the scopes the client requested — the same
level of control as when creating a client manually from user settings.
- selectedScopes state initialized from validation.scopes (all pre-checked)
- Group-level indeterminate checkbox to select/deselect an entire category
- Approve button reflects selection count and is disabled when nothing selected
- Auto-approve path (consent already on record) bypasses selection and passes
the existing granted scopes directly
Adds an OAuth 2.1 public client registration flow so MCP clients can
self-register via a user-facing consent page instead of requiring manual
setup in Settings.
Server:
- DB migration adds `is_public` and `created_via` columns to oauth_clients
- New GET /api/oauth/register/validate — validates DCR params, returns
requested scopes; unauthenticated callers get loginRequired flag
- New POST /api/oauth/register — creates a public client, saves consent,
and redirects with client_id (cookie auth required)
- `authenticateClient` / `refreshTokens` skip secret check for public
clients (PKCE provides the security guarantee)
- `createOAuthClient` accepts options for isPublic/createdVia; public
clients store an opaque secret hash instead of a usable secret
- `rotateOAuthClientSecret` blocked on public clients
- `isValidRedirectUri` extracted as a shared helper
- Discovery metadata now advertises registration_endpoint and auth method
`none`; token/revoke endpoints no longer require client_secret for
public clients
Client:
- New OAuthRegisterPage (/oauth/register) — loading → optional
login-required gate → scope selection → done states
- New ScopeGroupPicker component — collapsible groups, indeterminate
checkboxes, select-all per group or globally
- oauthApi.register.{validate,submit} added to api/client.ts
- apiClient exported so it can be reused outside api/client.ts
- IntegrationsTab tests fixed for new collapsible section structure
- collab_notes fallback changed from undefined to [] in MCP trip tools
Navigation tools:
- list_trips and get_trip_summary are now always registered for any
OAuth session regardless of granted scopes — they are required for
trip ID discovery before any scoped tool can be used
- get_trip_summary filters optional sections (budget, packing, collab,
reservations) by the client's OAuth scopes when called without trips:read
Deprecation notice:
- Inject static token deprecation warning into the first tool result
(list_trips or get_trip_summary) via a per-session closure so Claude
is forced to surface it — the instructions field alone is only
background context and is not proactively shown to the user
UI:
- OAuth client creation modal: add hint explaining the always-available
tools, remove the "must select at least one scope" submit guard
- OAuth consent screen: add "Always included" section showing list_trips
and get_trip_summary; handles zero-scope clients gracefully (empty
permissions section is hidden)
The polyfill was injected as an inline script at build time, causing a hard
CSP block under script-src 'self'. All browsers that support ES modules also
support modulepreload natively, so the polyfill is unnecessary.
Introduce trips:share as a dedicated OAuth scope for managing public
share links, decoupled from trips:read and trips:write. Share link
tools (get/create/delete_share_link) now gate on canShareTrips()
instead of the generic read/write scopes. Scope added to both client
and server definitions with full test coverage.
Redesign the consent screen from a narrow single-column card
(max-w-sm) to a two-panel layout (max-w-2xl): app identity and
action buttons on the left, scrollable scope list on the right.
Responsive — stacks vertically on mobile.
field, with Google Places Autocomplete API and Nominatim fallback.
- Add POST /api/maps/autocomplete route and autocompletePlaces service
- Add mapsApi.autocomplete client method
- Add debounced autocomplete dropdown to PlaceFormModal with keyboard
navigation (arrow keys, enter, escape) and mouse selection
- Use place details API to populate form fields on suggestion selection
- Derive location bias from existing trip places for better results
- Extract Google Maps URL regex to shared constant
vitest@4 requires vite@^6, causing two conflicting esbuild versions in
the lockfile and EBADPLATFORM errors during Docker npm ci. Pin to vitest
3.x which supports vite@5 and resolves a single esbuild@0.21.5.
OAuth 2.1 authentication for MCP:
- Add OAuth 2.1 authorization server with PKCE support (routes/oauth.ts)
- Add OAuth service for client CRUD, auth-code flow, and token management (services/oauthService.ts)
- Add typed scope definitions and enforcement helpers (mcp/scopes.ts)
- Add OAuth consent UI page (OAuthAuthorizePage.tsx)
- Add client-side scope labels and descriptions (api/oauthScopes.ts)
- Integrate OAuth token auth into MCP handler alongside existing static tokens
- All OAuth endpoints gated on `mcp` addon
Addon gating across MCP tools, resources, and prompts:
- Add typed ADDON_IDS constant (server/src/addons.ts) replacing all string literals
- Gate budget tools and resources (trip-budget, per-person, settlement) on `budget` addon
- Gate packing tools and resources (trip-packing, trip-packing-bags, trip-todos) on `packing` addon
- Gate todos tools on `packing` addon (mirrors web UI Lists tab behavior)
- Expand atlas gate to cover full tool body (bucket-list + country tools no longer leak)
- Expand collab gate to cover full tool body (collab notes no longer leak)
- Gate packing-list and budget-overview MCP prompts on their respective addons
- Gate get_trip_summary sections per addon; blank packing/budget/collab_notes/todos when disabled
- Remove trip-files resource and files field from get_trip_summary
- Replace all isAddonEnabled('literal') calls with ADDON_IDS constants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add reordering support for budget categories and line items within
categories. Changes persist via new DB table (budget_category_order)
and existing sort_order column. Live sync via WebSocket budget:reordered
event. Use Map instead of plain objects for category grouping to
preserve insertion order with numeric category names.
The "Unplanned" filter button in PlacesSidebar only filtered the place
list but not the map. Propagate the filter state to TripPlannerPage so
mapPlaces excludes planned places when the filter is active.