From da3cba2de37b410fa17501ae2fc86d5981d35152 Mon Sep 17 00:00:00 2001 From: "Julien G." <66769052+jubnl@users.noreply.github.com> Date: Wed, 13 May 2026 10:13:17 +0200 Subject: [PATCH 01/15] v3.0.19 Bug Fixes (#992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mcp): replace relative oauth constent redirect by absolute redirect derived from APP_URL (#987) * feat(journey): convert HEIC/HEIF uploads to JPEG for cross-platform compatibility HEIC is an Apple-only format not recognised as an image by many browsers and platforms. heic-to (lazy-loaded) now converts HEIC/HEIF files to JPEG before upload in both the gallery and entry editor photo pickers. Embedded metadata (EXIF, GPS) may be lost during conversion — documented in the Journey Journal wiki page. * fix(journey): skip heic-to import for non-HEIC files to avoid test env failures * fix(notifications): prevent double-escaping HTML in password reset emails buildPasswordResetHtml passed a pre-built HTML block to buildEmailHtml, which then escaped it again — rendering raw tags as plain text in the email. --- client/package-lock.json | 7 +++++++ client/package.json | 1 + client/src/pages/JourneyDetailPage.tsx | 7 +++++-- client/src/utils/convertHeic.ts | 17 +++++++++++++++++ server/src/mcp/oauthProvider.ts | 3 ++- server/src/services/notifications.ts | 6 +++--- wiki/Journey-Journal.md | 1 + 7 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 client/src/utils/convertHeic.ts diff --git a/client/package-lock.json b/client/package-lock.json index f829027e..7ddbaaa2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", "dexie": "^4.4.2", + "heic-to": "^1.4.2", "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", @@ -5827,6 +5828,12 @@ "dev": true, "license": "MIT" }, + "node_modules/heic-to": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz", + "integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==", + "license": "LGPL-3.0" + }, "node_modules/hsl-to-hex": { "version": "1.0.0", "license": "MIT", diff --git a/client/package.json b/client/package.json index ab8585b7..80def49d 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", "dexie": "^4.4.2", + "heic-to": "^1.4.2", "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index e7aa51f9..360247e7 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react' import { formatLocationName } from '../utils/formatters' +import { normalizeImageFiles } from '../utils/convertHeic' import { createPortal } from 'react-dom' import { useParams, useNavigate } from 'react-router-dom' import { useJourneyStore } from '../store/journeyStore' @@ -1027,8 +1028,9 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, if (!files?.length) return setGalleryUploading(true) try { + const normalized = await normalizeImageFiles(files) const formData = new FormData() - for (const f of files) formData.append('photos', f) + for (const f of normalized) formData.append('photos', f) await journeyApi.uploadGalleryPhotos(journeyId, formData) toast.success(t('journey.photosUploaded', { count: files.length })) onRefresh() @@ -2265,7 +2267,8 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa if (!files?.length) return // Queue files locally until Save so cancel/close actually discards. This // keeps photo behavior consistent with text fields — no silent persistence. - setPendingFiles(prev => [...prev, ...Array.from(files)]) + const normalized = await normalizeImageFiles(files) + setPendingFiles(prev => [...prev, ...normalized]) } return ( diff --git a/client/src/utils/convertHeic.ts b/client/src/utils/convertHeic.ts new file mode 100644 index 00000000..6f757792 --- /dev/null +++ b/client/src/utils/convertHeic.ts @@ -0,0 +1,17 @@ +function looksLikeHeic(file: File): boolean { + const ext = file.name.split('.').pop()?.toLowerCase() ?? '' + return ext === 'heic' || ext === 'heif' || file.type === 'image/heic' || file.type === 'image/heif' +} + +export async function normalizeImageFile(file: File): Promise { + if (!looksLikeHeic(file)) return file + const { isHeic, heicTo } = await import('heic-to') + if (!(await isHeic(file))) return file + const blob = await heicTo({ blob: file, type: 'image/jpeg', quality: 0.92 }) + const jpegName = file.name.replace(/\.(heic|heif)$/i, '.jpg') + return new File([blob], jpegName, { type: 'image/jpeg' }) +} + +export async function normalizeImageFiles(files: FileList | File[]): Promise { + return Promise.all(Array.from(files).map(normalizeImageFile)) +} diff --git a/server/src/mcp/oauthProvider.ts b/server/src/mcp/oauthProvider.ts index 943a515f..592fd282 100644 --- a/server/src/mcp/oauthProvider.ts +++ b/server/src/mcp/oauthProvider.ts @@ -147,7 +147,8 @@ export const trekOAuthProvider: OAuthServerProvider = { if (params.state) qs.set('state', params.state); if (params.resource) qs.set('resource', params.resource.href); - res.redirect(302, `/oauth/consent?${qs.toString()}`); + const base = getMcpSafeUrl().replace(/\/+$/, ''); + res.redirect(302, `${base}/oauth/consent?${qs.toString()}`); }, // Not called because skipLocalPkceValidation = true. diff --git a/server/src/services/notifications.ts b/server/src/services/notifications.ts index ecc9a3bc..94b6b34c 100644 --- a/server/src/services/notifications.ts +++ b/server/src/services/notifications.ts @@ -316,12 +316,12 @@ export function getEventText(lang: string, event: NotifEventType, params: Record // ── Email HTML builder ───────────────────────────────────────────────────── -export function buildEmailHtml(subject: string, body: string, lang: string, navigateTarget?: string): string { +export function buildEmailHtml(subject: string, body: string, lang: string, navigateTarget?: string, rawBody = false): string { const s = I18N[lang] || I18N.en; const appUrl = getAppUrl(); const ctaHref = escapeHtml(navigateTarget ? `${appUrl}${navigateTarget}` : (appUrl || '')); const safeSubject = escapeHtml(subject); - const safeBody = escapeHtml(body); + const safeBody = rawBody ? body : escapeHtml(body); return ` @@ -396,7 +396,7 @@ function buildPasswordResetHtml(subject: string, strings: PasswordResetStrings,

${safeExpiry}

${safeIgnore}

`; - return buildEmailHtml(subject, block, lang); + return buildEmailHtml(subject, block, lang, undefined, true); } /** diff --git a/wiki/Journey-Journal.md b/wiki/Journey-Journal.md index 5dd39145..ceea0ca7 100644 --- a/wiki/Journey-Journal.md +++ b/wiki/Journey-Journal.md @@ -37,6 +37,7 @@ Each entry corresponds to a day in your journey. The entry editor provides: - **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Cold. - **Photos** — attach photos to the entry. The first photo becomes the card thumbnail in list views. + > **Note on HEIC files:** HEIC is an Apple-only format that many browsers and platforms do not recognise as an image. To ensure broad compatibility, HEIC/HEIF files are automatically converted to JPEG before upload. This conversion may result in the loss of embedded metadata (EXIF data such as GPS coordinates, camera information, etc.). - **Pros / Cons** — optional verdict cards. Add items to a **Pros** list (thumbs-up) or a **Cons** list (thumbs-down) to summarise what you loved or what could have been better. These are stored in the `pros_cons.pros` and `pros_cons.cons` arrays on the entry. - **Tags** — free-form labels (e.g. "hidden gem", "best meal"). - **Location** — pin the entry to a map location. From 5b710a429a05b6fd255418b01823cca77f66b095 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 08:13:30 +0000 Subject: [PATCH 02/15] chore: bump version to 3.0.19 [skip ci] --- charts/trek/Chart.yaml | 4 ++-- client/package-lock.json | 4 ++-- client/package.json | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/trek/Chart.yaml b/charts/trek/Chart.yaml index ddad80e7..4a337ec9 100644 --- a/charts/trek/Chart.yaml +++ b/charts/trek/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 name: trek -version: 3.0.18 +version: 3.0.19 description: Minimal Helm chart for TREK app -appVersion: "3.0.18" +appVersion: "3.0.19" diff --git a/client/package-lock.json b/client/package-lock.json index 7ddbaaa2..351c182f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-client", - "version": "3.0.18", + "version": "3.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-client", - "version": "3.0.18", + "version": "3.0.19", "dependencies": { "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", diff --git a/client/package.json b/client/package.json index 80def49d..2ee67324 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "trek-client", - "version": "3.0.18", + "version": "3.0.19", "private": true, "type": "module", "scripts": { diff --git a/server/package-lock.json b/server/package-lock.json index 24482213..fef80e9a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-server", - "version": "3.0.18", + "version": "3.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-server", - "version": "3.0.18", + "version": "3.0.19", "dependencies": { "@modelcontextprotocol/sdk": "^1.28.0", "archiver": "^6.0.1", diff --git a/server/package.json b/server/package.json index 1d61368c..b751dcab 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "trek-server", - "version": "3.0.18", + "version": "3.0.19", "main": "src/index.ts", "scripts": { "start": "node --import tsx src/index.ts", From 93b51a0bf50b717d4694de7c51a9e2a4f02292ac Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 13 May 2026 10:34:57 +0200 Subject: [PATCH 03/15] fix(csp): allow unsafe-eval for HEIC image conversion --- server/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/app.ts b/server/src/app.ts index c03c7583..0be79570 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -122,7 +122,7 @@ export function createApp(): express.Application { contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], - scriptSrc: ["'self'", "'wasm-unsafe-eval'"], + scriptSrc: ["'self'", "'wasm-unsafe-eval'", "'unsafe-eval'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://unpkg.com"], imgSrc: ["'self'", "data:", "blob:", "https:"], connectSrc: [ From 7e49f3467cd36ae2f7657a1c68a845ab9b779192 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 08:35:23 +0000 Subject: [PATCH 04/15] chore: bump version to 3.0.20 [skip ci] --- charts/trek/Chart.yaml | 4 ++-- client/package-lock.json | 4 ++-- client/package.json | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/trek/Chart.yaml b/charts/trek/Chart.yaml index 4a337ec9..88488ea7 100644 --- a/charts/trek/Chart.yaml +++ b/charts/trek/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 name: trek -version: 3.0.19 +version: 3.0.20 description: Minimal Helm chart for TREK app -appVersion: "3.0.19" +appVersion: "3.0.20" diff --git a/client/package-lock.json b/client/package-lock.json index 351c182f..fcab0a2c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-client", - "version": "3.0.19", + "version": "3.0.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-client", - "version": "3.0.19", + "version": "3.0.20", "dependencies": { "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", diff --git a/client/package.json b/client/package.json index 2ee67324..d4976742 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "trek-client", - "version": "3.0.19", + "version": "3.0.20", "private": true, "type": "module", "scripts": { diff --git a/server/package-lock.json b/server/package-lock.json index fef80e9a..8fda935a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-server", - "version": "3.0.19", + "version": "3.0.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-server", - "version": "3.0.19", + "version": "3.0.20", "dependencies": { "@modelcontextprotocol/sdk": "^1.28.0", "archiver": "^6.0.1", diff --git a/server/package.json b/server/package.json index b751dcab..91bb626d 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "trek-server", - "version": "3.0.19", + "version": "3.0.20", "main": "src/index.ts", "scripts": { "start": "node --import tsx src/index.ts", From e7211325dfbb0754b95a5873aa9a2ac676758d77 Mon Sep 17 00:00:00 2001 From: "Julien G." <66769052+jubnl@users.noreply.github.com> Date: Fri, 15 May 2026 23:16:34 +0200 Subject: [PATCH 05/15] Add asset.download permission to Photo Providers --- wiki/Photo-Providers.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wiki/Photo-Providers.md b/wiki/Photo-Providers.md index d6754e00..5be8d8b5 100644 --- a/wiki/Photo-Providers.md +++ b/wiki/Photo-Providers.md @@ -44,6 +44,7 @@ When generating the API key in Immich (**Account Settings → API Keys**), grant | `asset.read` | Read photo metadata and search results | | `asset.view` | Load thumbnails and preview images | | `album.read` | List owned + shared albums and their contents | +| `asset.download` | Download the assets | | `asset.upload` | *Only if you enable "Mirror journey photos to Immich on upload"* — push TREK uploads back to your library | TREK never modifies or deletes anything in Immich, so no `update`, `delete`, or admin scopes are needed. @@ -94,4 +95,4 @@ Once a provider is connected, you can browse and attach photos to your trips. Se ## See also - [Admin-Addons](Admin-Addons) -- [Internal-Network-Access](Internal-Network-Access) \ No newline at end of file +- [Internal-Network-Access](Internal-Network-Access) From 117942f45e15c98dc0fc6473d97626a10aefd4ba Mon Sep 17 00:00:00 2001 From: "Julien G." <66769052+jubnl@users.noreply.github.com> Date: Sat, 16 May 2026 00:53:02 +0200 Subject: [PATCH 06/15] v3.0.21 Bug Fixes (#998) * fix(journey): remove photo upload count limit and surface upload errors (#997) Removes the arbitrary 10-file cap on journey entry photo uploads and 20-file cap on gallery uploads. MulterErrors now return proper 4xx responses instead of 500, and the client surfaces the server error message via toast rather than silently trapping the user in the post editor overlay. * fix(planner): remove correct assignment when place assigned to same day multiple times When a place was assigned to the same day more than once, the "Remove from day" button in PlaceInspector always deleted the first assignment (Array.find on place.id) instead of the currently selected one. Now prefers selectedAssignmentId when available. Fixes #1005 * fix(map): enable 3D terrain for Mapbox outdoors style in trip planner wantsTerrain() only matched satellite styles, so the outdoors-v12 style was flat in the planner despite showing correct 3D terrain in the settings preview. Added outdoors-v12 to the allowlist; marker drift is already handled by syncMarkerAltitudes(). Fixes #1002 * fix(maps): send Referer header on Google API calls when APP_URL is set Supports HTTP referrer restrictions on GCP API keys. Documents the restriction types and photo troubleshooting steps in the wiki. --- client/src/components/Map/mapboxSetup.ts | 10 +++-- .../src/components/Planner/PlaceInspector.tsx | 5 ++- client/src/i18n/translations/ar.ts | 2 + client/src/i18n/translations/br.ts | 2 + client/src/i18n/translations/cs.ts | 2 + client/src/i18n/translations/de.ts | 2 + client/src/i18n/translations/en.ts | 2 + client/src/i18n/translations/es.ts | 2 + client/src/i18n/translations/fr.ts | 2 + client/src/i18n/translations/hu.ts | 2 + client/src/i18n/translations/id.ts | 2 + client/src/i18n/translations/it.ts | 2 + client/src/i18n/translations/nl.ts | 2 + client/src/i18n/translations/pl.ts | 2 + client/src/i18n/translations/ru.ts | 2 + client/src/i18n/translations/zh.ts | 2 + client/src/i18n/translations/zhTw.ts | 2 + client/src/pages/JourneyDetailPage.tsx | 12 ++++-- server/src/app.ts | 5 +++ server/src/routes/journey.ts | 4 +- server/src/services/mapsService.ts | 7 +++- wiki/Places-and-Search.md | 2 + wiki/Troubleshooting.md | 39 +++++++++++++++++++ 23 files changed, 103 insertions(+), 11 deletions(-) diff --git a/client/src/components/Map/mapboxSetup.ts b/client/src/components/Map/mapboxSetup.ts index b3fc9071..a77fa2d0 100644 --- a/client/src/components/Map/mapboxSetup.ts +++ b/client/src/components/Map/mapboxSetup.ts @@ -8,13 +8,15 @@ export function isStandardFamily(style: string): boolean { return style === 'mapbox://styles/mapbox/standard' || style === 'mapbox://styles/mapbox/standard-satellite' } -// Terrain is only genuinely useful for the satellite imagery styles — on -// clean flat styles like streets/light/dark it nudges route lines onto -// the DEM while our HTML markers stay at Z=0, which causes the visible -// offset when the map is pitched. Restrict terrain to satellite. +// Terrain is only genuinely useful for styles that benefit from elevation +// data. On flat vector styles (streets/light/dark) it nudges route lines +// onto the DEM while HTML markers stay at Z=0, causing a visible drift +// when the map is pitched. Satellite and Outdoors are the intended styles +// for terrain; markers are re-pinned by syncMarkerAltitudes(). export function wantsTerrain(style: string): boolean { return style === 'mapbox://styles/mapbox/satellite-v9' || style === 'mapbox://styles/mapbox/satellite-streets-v12' + || style === 'mapbox://styles/mapbox/outdoors-v12' } // 3D can be added to every style now — the standard family has it built-in diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index 9e09f1ff..881ff253 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -169,7 +169,10 @@ export default function PlaceInspector({ const category = categories?.find(c => c.id === place.category_id) const dayAssignments = selectedDayId ? (assignments[String(selectedDayId)] || []) : [] - const assignmentInDay = selectedDayId ? dayAssignments.find(a => a.place?.id === place.id) : null + const assignmentInDay = selectedDayId + ? ((selectedAssignmentId ? dayAssignments.find(a => a.id === selectedAssignmentId) : null) + ?? dayAssignments.find(a => a.place?.id === place.id)) + : null const openingHours = googleDetails?.opening_hours || null const openNow = googleDetails?.open_now ?? null diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 390b05c0..8a3313f0 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -1674,6 +1674,7 @@ const ar: Record = { 'journey.settings.failedToDelete': 'فشل في الحذف', 'journey.entries.deleteTitle': 'حذف الإدخال', 'journey.photosUploaded': 'تم رفع {count} صورة', + 'journey.photosUploadFailed': 'فشل رفع بعض الصور', 'journey.photosAdded': 'تمت إضافة {count} صورة', 'journey.picker.tripPeriod': 'فترة الرحلة', 'journey.picker.dateRange': 'نطاق التاريخ', @@ -1705,6 +1706,7 @@ const ar: Record = { // Journey Entry Editor 'journey.editor.discardChangesConfirm': 'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟', + 'journey.editor.uploadFailed': 'فشل رفع الصور', 'journey.editor.uploadPhotos': 'رفع صور', 'journey.editor.uploading': '...جارٍ الرفع', 'journey.editor.fromGallery': 'من المعرض', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 0757c3d2..cf73f2fa 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -2077,6 +2077,7 @@ const br: Record = { 'journey.synced.places': 'lugares', 'journey.synced.synced': 'sincronizado', 'journey.editor.discardChangesConfirm': 'Você tem alterações não salvas. Descartá-las?', + 'journey.editor.uploadFailed': 'Falha ao enviar fotos', 'journey.editor.uploadPhotos': 'Enviar fotos', 'journey.editor.uploading': 'Enviando...', 'journey.editor.fromGallery': 'Da galeria', @@ -2169,6 +2170,7 @@ const br: Record = { 'journey.settings.failedToDelete': 'Falha ao excluir', 'journey.entries.deleteTitle': 'Excluir entrada', 'journey.photosUploaded': '{count} fotos enviadas', + 'journey.photosUploadFailed': 'Algumas fotos não foram enviadas', 'journey.photosAdded': '{count} fotos adicionadas', 'journey.public.notFound': 'Não encontrado', 'journey.public.notFoundMessage': 'Esta jornada não existe ou o link expirou.', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index a14b633d..cefbf21b 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -2082,6 +2082,7 @@ const cs: Record = { 'journey.synced.places': 'místa', 'journey.synced.synced': 'synchronizováno', 'journey.editor.discardChangesConfirm': 'Máte neuložené změny. Zahodit?', + 'journey.editor.uploadFailed': 'Nahrávání fotek selhalo', 'journey.editor.uploadPhotos': 'Nahrát fotky', 'journey.editor.uploading': 'Nahrávání...', 'journey.editor.fromGallery': 'Z galerie', @@ -2174,6 +2175,7 @@ const cs: Record = { 'journey.settings.failedToDelete': 'Smazání se nezdařilo', 'journey.entries.deleteTitle': 'Smazat záznam', 'journey.photosUploaded': '{count} fotografií nahráno', + 'journey.photosUploadFailed': 'Některé fotky se nepodařilo nahrát', 'journey.photosAdded': '{count} fotografií přidáno', 'journey.public.notFound': 'Nenalezeno', 'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index cbb6d153..d287a4aa 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -2085,6 +2085,7 @@ const de: Record = { 'journey.synced.places': 'Orte', 'journey.synced.synced': 'synchronisiert', 'journey.editor.discardChangesConfirm': 'Du hast ungespeicherte Änderungen. Verwerfen?', + 'journey.editor.uploadFailed': 'Foto-Upload fehlgeschlagen', 'journey.editor.uploadPhotos': 'Fotos hochladen', 'journey.editor.uploading': 'Hochladen...', 'journey.editor.fromGallery': 'Aus Galerie', @@ -2181,6 +2182,7 @@ const de: Record = { 'journey.settings.failedToDelete': 'Löschen fehlgeschlagen', 'journey.entries.deleteTitle': 'Eintrag löschen', 'journey.photosUploaded': '{count} Fotos hochgeladen', + 'journey.photosUploadFailed': 'Einige Fotos konnten nicht hochgeladen werden', 'journey.photosAdded': '{count} Fotos hinzugefügt', 'journey.public.notFound': 'Nicht gefunden', 'journey.public.notFoundMessage': 'Diese Journey existiert nicht oder der Link ist abgelaufen.', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index ce8321a6..2f0bfc71 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -2111,6 +2111,7 @@ const en: Record = { // Journey Entry Editor 'journey.editor.discardChangesConfirm': 'You have unsaved changes. Discard them?', + 'journey.editor.uploadFailed': 'Photo upload failed', 'journey.editor.uploadPhotos': 'Upload photos', 'journey.editor.uploading': 'Uploading...', 'journey.editor.fromGallery': 'From Gallery', @@ -2219,6 +2220,7 @@ const en: Record = { 'journey.settings.failedToDelete': 'Failed to delete', 'journey.entries.deleteTitle': 'Delete Entry', 'journey.photosUploaded': '{count} photos uploaded', + 'journey.photosUploadFailed': 'Some photos failed to upload', 'journey.photosAdded': '{count} photos added', // Journey — Public Page diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index a66bdfb6..b9b93dc7 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -2084,6 +2084,7 @@ const es: Record = { 'journey.synced.places': 'lugares', 'journey.synced.synced': 'sincronizado', 'journey.editor.discardChangesConfirm': 'Tienes cambios sin guardar. ¿Descartarlos?', + 'journey.editor.uploadFailed': 'Error al subir fotos', 'journey.editor.uploadPhotos': 'Subir fotos', 'journey.editor.uploading': 'Subiendo...', 'journey.editor.fromGallery': 'Desde galería', @@ -2176,6 +2177,7 @@ const es: Record = { 'journey.settings.failedToDelete': 'Error al eliminar', 'journey.entries.deleteTitle': 'Eliminar entrada', 'journey.photosUploaded': '{count} fotos subidas', + 'journey.photosUploadFailed': 'Algunas fotos no se pudieron subir', 'journey.photosAdded': '{count} fotos añadidas', 'journey.public.notFound': 'No encontrado', 'journey.public.notFoundMessage': 'Esta travesía no existe o el enlace ha expirado.', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index c7cd1605..bb84a30b 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -2078,6 +2078,7 @@ const fr: Record = { 'journey.synced.places': 'lieux', 'journey.synced.synced': 'synchronisé', 'journey.editor.discardChangesConfirm': 'Vous avez des modifications non enregistrées. Les ignorer ?', + 'journey.editor.uploadFailed': 'Échec du téléversement des photos', 'journey.editor.uploadPhotos': 'Téléverser des photos', 'journey.editor.uploading': 'Envoi...', 'journey.editor.fromGallery': 'Depuis la galerie', @@ -2170,6 +2171,7 @@ const fr: Record = { 'journey.settings.failedToDelete': 'Échec de la suppression', 'journey.entries.deleteTitle': "Supprimer l'entrée", 'journey.photosUploaded': '{count} photos téléversées', + 'journey.photosUploadFailed': "Certaines photos n'ont pas pu être téléversées", 'journey.photosAdded': '{count} photos ajoutées', 'journey.public.notFound': 'Introuvable', 'journey.public.notFoundMessage': 'Ce journal n\'existe pas ou le lien a expiré.', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index f8046fab..bfae8e1a 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -2079,6 +2079,7 @@ const hu: Record = { 'journey.synced.places': 'helyszín', 'journey.synced.synced': 'szinkronizálva', 'journey.editor.discardChangesConfirm': 'Mentetlen módosításaid vannak. Elveted?', + 'journey.editor.uploadFailed': 'A fotók feltöltése sikertelen', 'journey.editor.uploadPhotos': 'Fotók feltöltése', 'journey.editor.uploading': 'Feltöltés...', 'journey.editor.fromGallery': 'Galériából', @@ -2171,6 +2172,7 @@ const hu: Record = { 'journey.settings.failedToDelete': 'Törlés sikertelen', 'journey.entries.deleteTitle': 'Bejegyzés törlése', 'journey.photosUploaded': '{count} fotó feltöltve', + 'journey.photosUploadFailed': 'Néhány fotót nem sikerült feltölteni', 'journey.photosAdded': '{count} fotó hozzáadva', 'journey.public.notFound': 'Nem található', 'journey.public.notFoundMessage': 'Ez az útinapló nem létezik vagy a link lejárt.', diff --git a/client/src/i18n/translations/id.ts b/client/src/i18n/translations/id.ts index 112d17fc..a8b80d04 100644 --- a/client/src/i18n/translations/id.ts +++ b/client/src/i18n/translations/id.ts @@ -2094,6 +2094,7 @@ const id: Record = { // Journey Entry Editor 'journey.editor.discardChangesConfirm': 'Anda memiliki perubahan yang belum disimpan. Buang?', + 'journey.editor.uploadFailed': 'Gagal mengunggah foto', 'journey.editor.uploadPhotos': 'Unggah foto', 'journey.editor.uploading': 'Mengunggah...', 'journey.editor.fromGallery': 'Dari Galeri', @@ -2198,6 +2199,7 @@ const id: Record = { 'journey.settings.failedToDelete': 'Gagal menghapus', 'journey.entries.deleteTitle': 'Hapus Entri', 'journey.photosUploaded': '{count} foto diunggah', + 'journey.photosUploadFailed': 'Beberapa foto gagal diunggah', 'journey.photosAdded': '{count} foto ditambahkan', // Journey — Public Page diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 2ac5424f..21569ed9 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -2079,6 +2079,7 @@ const it: Record = { 'journey.synced.places': 'luoghi', 'journey.synced.synced': 'sincronizzato', 'journey.editor.discardChangesConfirm': 'Hai modifiche non salvate. Vuoi scartarle?', + 'journey.editor.uploadFailed': 'Caricamento foto non riuscito', 'journey.editor.uploadPhotos': 'Carica foto', 'journey.editor.uploading': 'Caricamento...', 'journey.editor.fromGallery': 'Dalla galleria', @@ -2171,6 +2172,7 @@ const it: Record = { 'journey.settings.failedToDelete': 'Eliminazione non riuscita', 'journey.entries.deleteTitle': 'Elimina voce', 'journey.photosUploaded': '{count} foto caricate', + 'journey.photosUploadFailed': 'Alcune foto non sono state caricate', 'journey.photosAdded': '{count} foto aggiunte', 'journey.public.notFound': 'Non trovato', 'journey.public.notFoundMessage': 'Questo diario non esiste o il link è scaduto.', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 0cb55bc1..289f2ae9 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -2078,6 +2078,7 @@ const nl: Record = { 'journey.synced.places': 'plaatsen', 'journey.synced.synced': 'gesynchroniseerd', 'journey.editor.discardChangesConfirm': 'Je hebt niet-opgeslagen wijzigingen. Verwerpen?', + 'journey.editor.uploadFailed': 'Foto uploaden mislukt', 'journey.editor.uploadPhotos': 'Foto\'s uploaden', 'journey.editor.uploading': 'Uploaden...', 'journey.editor.fromGallery': 'Uit galerij', @@ -2170,6 +2171,7 @@ const nl: Record = { 'journey.settings.failedToDelete': 'Verwijderen mislukt', 'journey.entries.deleteTitle': 'Vermelding verwijderen', 'journey.photosUploaded': "{count} foto's geüpload", + 'journey.photosUploadFailed': "Sommige foto's konden niet worden geüpload", 'journey.photosAdded': "{count} foto's toegevoegd", 'journey.public.notFound': 'Niet gevonden', 'journey.public.notFoundMessage': 'Dit reisverslag bestaat niet of de link is verlopen.', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 87f768a9..b0c53c00 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -2071,6 +2071,7 @@ const pl: Record = { 'journey.synced.places': 'miejsca', 'journey.synced.synced': 'zsynchronizowane', 'journey.editor.discardChangesConfirm': 'Masz niezapisane zmiany. Odrzucić?', + 'journey.editor.uploadFailed': 'Przesyłanie zdjęć nie powiodło się', 'journey.editor.uploadPhotos': 'Prześlij zdjęcia', 'journey.editor.uploading': 'Przesyłanie...', 'journey.editor.fromGallery': 'Z galerii', @@ -2163,6 +2164,7 @@ const pl: Record = { 'journey.settings.failedToDelete': 'Nie udało się usunąć', 'journey.entries.deleteTitle': 'Usuń wpis', 'journey.photosUploaded': '{count} zdjęć przesłanych', + 'journey.photosUploadFailed': 'Nie udało się przesłać niektórych zdjęć', 'journey.photosAdded': '{count} zdjęć dodanych', 'journey.public.notFound': 'Nie znaleziono', 'journey.public.notFoundMessage': 'Ten dziennik podróży nie istnieje lub link wygasł.', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index f4f23fb8..3384f353 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -2078,6 +2078,7 @@ const ru: Record = { 'journey.synced.places': 'мест', 'journey.synced.synced': 'синхронизировано', 'journey.editor.discardChangesConfirm': 'У вас есть несохранённые изменения. Отменить?', + 'journey.editor.uploadFailed': 'Не удалось загрузить фото', 'journey.editor.uploadPhotos': 'Загрузить фото', 'journey.editor.uploading': 'Загрузка...', 'journey.editor.fromGallery': 'Из галереи', @@ -2170,6 +2171,7 @@ const ru: Record = { 'journey.settings.failedToDelete': 'Не удалось удалить', 'journey.entries.deleteTitle': 'Удалить запись', 'journey.photosUploaded': '{count} фото загружено', + 'journey.photosUploadFailed': 'Некоторые фото не удалось загрузить', 'journey.photosAdded': '{count} фото добавлено', 'journey.public.notFound': 'Не найдено', 'journey.public.notFoundMessage': 'Это путешествие не существует или ссылка устарела.', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index ffa564b6..6549509a 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -2078,6 +2078,7 @@ const zh: Record = { 'journey.synced.places': '个地点', 'journey.synced.synced': '已同步', 'journey.editor.discardChangesConfirm': '您有未保存的更改。要放弃吗?', + 'journey.editor.uploadFailed': '照片上传失败', 'journey.editor.uploadPhotos': '上传照片', 'journey.editor.uploading': '上传中...', 'journey.editor.fromGallery': '从相册', @@ -2170,6 +2171,7 @@ const zh: Record = { 'journey.settings.failedToDelete': '删除失败', 'journey.entries.deleteTitle': '删除条目', 'journey.photosUploaded': '{count} 张照片已上传', + 'journey.photosUploadFailed': '部分照片上传失败', 'journey.photosAdded': '{count} 张照片已添加', 'journey.public.notFound': '未找到', 'journey.public.notFoundMessage': '此旅程不存在或链接已过期。', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 331596c5..00385ad2 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -2036,6 +2036,7 @@ const zhTw: Record = { 'journey.synced.places': '個地點', 'journey.synced.synced': '已同步', 'journey.editor.discardChangesConfirm': '您有未儲存的變更。要放棄嗎?', + 'journey.editor.uploadFailed': '照片上傳失敗', 'journey.editor.uploadPhotos': '上傳照片', 'journey.editor.uploading': '上傳中...', 'journey.editor.fromGallery': '從相簿', @@ -2128,6 +2129,7 @@ const zhTw: Record = { 'journey.settings.failedToDelete': '刪除失敗', 'journey.entries.deleteTitle': '刪除條目', 'journey.photosUploaded': '{count} 張照片已上傳', + 'journey.photosUploadFailed': '部分照片上傳失敗', 'journey.photosAdded': '{count} 張照片已新增', 'journey.public.notFound': '未找到', 'journey.public.notFoundMessage': '此旅程不存在或連結已過期。', diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index 360247e7..fcd54545 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -30,6 +30,7 @@ import MobileEntryView from '../components/Journey/MobileEntryView' import { useIsMobile } from '../hooks/useIsMobile' import type { JourneyEntry, JourneyPhoto, GalleryPhoto, JourneyTrip, JourneyDetail } from '../store/journeyStore' import { computeJourneyLifecycle } from '../utils/journeyLifecycle' +import { getApiErrorMessage } from '../types' const GRADIENTS = [ 'linear-gradient(135deg, #0F172A 0%, #6366F1 45%, #EC4899 100%)', @@ -1034,8 +1035,8 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, await journeyApi.uploadGalleryPhotos(journeyId, formData) toast.success(t('journey.photosUploaded', { count: files.length })) onRefresh() - } catch { - toast.error(t('journey.settings.coverFailed')) + } catch (err) { + toast.error(getApiErrorMessage(err, t('journey.photosUploadFailed'))) } finally { setGalleryUploading(false) } @@ -2175,6 +2176,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa onDone: () => void }) { const { t } = useTranslation() + const toast = useToast() const isMobile = useIsMobile() const [title, setTitle] = useState(entry.title || '') const [story, setStory] = useState(entry.story || '') @@ -2248,7 +2250,11 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa if (pendingFiles.length > 0 && entryId) { const formData = new FormData() for (const f of pendingFiles) formData.append('photos', f) - await onUploadPhotos(entryId, formData) + try { + await onUploadPhotos(entryId, formData) + } catch (err) { + toast.error(getApiErrorMessage(err, t('journey.editor.uploadFailed'))) + } } // link gallery photos that were picked before save if (pendingLinkIds.length > 0 && entryId) { diff --git a/server/src/app.ts b/server/src/app.ts index 0be79570..45d17b7d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,6 +5,7 @@ import cookieParser from 'cookie-parser'; import path from 'node:path'; import fs from 'node:fs'; +import multer from 'multer'; import { logDebug, logWarn, logError } from './services/auditLog'; import { enforceGlobalMfaPolicy } from './middleware/mfaPolicy'; import { authenticate, verifyJwtAndLoadUser } from './middleware/auth'; @@ -507,6 +508,10 @@ export function createApp(): express.Application { } else { console.error('Unhandled error:', err); } + if (err instanceof multer.MulterError) { + const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400; + return res.status(status).json({ error: err.message }); + } const status = err.statusCode || err.status || 500; // Expose the message for client errors (4xx); keep 'Internal server error' for 5xx. const message = status < 500 ? err.message : 'Internal server error'; diff --git a/server/src/routes/journey.ts b/server/src/routes/journey.ts index 1336bd50..b655d7ce 100644 --- a/server/src/routes/journey.ts +++ b/server/src/routes/journey.ts @@ -98,7 +98,7 @@ router.delete('/entries/:entryId', authenticate, (req: Request, res: Response) = // ── Photos (prefix /photos and /entries — before /:id) ─────────────────── -router.post('/entries/:entryId/photos', authenticate, upload.array('photos', 10), async (req: Request, res: Response) => { +router.post('/entries/:entryId/photos', authenticate, upload.array('photos'), async (req: Request, res: Response) => { const authReq = req as AuthRequest; const files = req.files as Express.Multer.File[]; if (!files?.length) return res.status(400).json({ error: 'No files uploaded' }); @@ -201,7 +201,7 @@ router.delete('/photos/:photoId', authenticate, async (req: Request, res: Respon // ── Gallery (prefix /:id/gallery — before /:id) ────────────────────────── // Upload photos directly to the journey gallery (no entry association) -router.post('/:id/gallery/photos', authenticate, upload.array('photos', 20), async (req: Request, res: Response) => { +router.post('/:id/gallery/photos', authenticate, upload.array('photos'), async (req: Request, res: Response) => { const authReq = req as AuthRequest; const files = req.files as Express.Multer.File[]; if (!files?.length) return res.status(400).json({ error: 'No files uploaded' }); diff --git a/server/src/services/mapsService.ts b/server/src/services/mapsService.ts index 00151fe2..c336135c 100644 --- a/server/src/services/mapsService.ts +++ b/server/src/services/mapsService.ts @@ -1,6 +1,7 @@ import { db } from '../db/database'; import { decrypt_api_key } from './apiKeyCrypto'; import { checkSsrf } from '../utils/ssrfGuard'; +import { getAppUrl } from './notifications'; // ── Google API call counter ─────────────────────────────────────────────────── @@ -12,7 +13,11 @@ export function resetGoogleApiCallCount(): void { googleApiCallCount = 0; } function googleFetch(endpoint: string, label: string, init?: RequestInit): Promise { googleApiCallCount++; console.debug(`[Google API] #${googleApiCallCount} ${label} → ${endpoint}`); - return fetch(endpoint, init); + const referer = process.env.APP_URL ? getAppUrl() : undefined; + return fetch(endpoint, { + ...init, + headers: { ...(referer ? { Referer: referer } : {}), ...(init?.headers as Record ?? {}) }, + }); } // ── Interfaces ─────────────────────────────────────────────────────────────── diff --git a/wiki/Places-and-Search.md b/wiki/Places-and-Search.md index efc49453..8a336294 100644 --- a/wiki/Places-and-Search.md +++ b/wiki/Places-and-Search.md @@ -21,6 +21,8 @@ Type in the search box at the top of the form. After 2 or more characters, with When a key is present, the autocomplete uses the Google Places API, which can return ratings, opening hours, photos, and phone numbers from Google's database. +> **API key restrictions:** TREK calls the Google Places API from the server, not the browser. If you apply **HTTP referrers** restrictions to your key in Google Cloud Console, you must also set `APP_URL` in your environment — TREK sends it as the `Referer` header on every outbound Google API request. Without it, Google will reject all server-side calls with `REQUEST_DENIED`. For server-side deployments, **IP address** restrictions are simpler and require no extra configuration. See [Troubleshooting](Troubleshooting) if photos are missing after adding a key. + ### Without a Google Maps API key TREK falls back to OpenStreetMap (Nominatim) automatically — no API key needed. A notice appears above the search box explaining that OpenStreetMap is in use and that photos, ratings, and opening hours are unavailable. Results include name, address, and coordinates. diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md index 7b7a6d03..dae924c8 100644 --- a/wiki/Troubleshooting.md +++ b/wiki/Troubleshooting.md @@ -223,6 +223,45 @@ If `ALLOWED_ORIGINS` is not set, TREK allows all origins (development default). --- +## Place photos not loading / place thumbnail shows default map pin (Google Maps API key configured) + +**Cause:** When a Google Maps API key is set, TREK fetches photo references and image bytes from the Google Places API on the server side. If the server-side call is rejected or returns no photos, the `/place-photo/:id` endpoint returns 404 and the place falls back to the default map-pin thumbnail. The most common causes are: + +1. **HTTP referrer restriction on the API key.** Google Cloud Console lets you restrict a key to specific HTTP referrers. Because TREK calls Google from the server (not the browser), it sends a `Referer` header derived from `APP_URL`. If `APP_URL` is not set, the fallback is `http://localhost:`, which will not match any domain whitelist in GCP. + +2. **Wrong key restriction type.** API keys restricted by **HTTP referrers** are designed for browser-side JavaScript. For a self-hosted server application, use **IP address** restrictions instead — add the public IP of your TREK server and no `APP_URL` configuration is needed. + +3. **Places API (New) not enabled.** The key must have **Places API (New)** enabled in Google Cloud Console under APIs & Services → Enabled APIs. Enabling only the legacy Places API is not sufficient. + +4. **Billing not set up.** Google requires a billing account to be linked to the project even within the free tier. Without it, photo and details requests return `REQUEST_DENIED`. + +**Fix for HTTP referrer restriction:** + +Set `APP_URL` to the public URL of your instance and add that URL (or its domain with a wildcard, e.g. `https://trek.example.com/*`) to the allowed referrers in GCP: + +```yaml +environment: + - APP_URL=https://trek.example.com +``` + +**Fix for wrong restriction type:** + +Switch the key's "Application restrictions" from **HTTP referrers** to **IP addresses** in Google Cloud Console, and add your server's public IP. No `APP_URL` change needed. + +**Verifying the issue:** + +Run the following curl command using your key to check whether Google returns photo references: + +```bash +curl "https://places.googleapis.com/v1/places/" \ + -H "X-Goog-Api-Key: YOUR_API_KEY" \ + -H "X-Goog-FieldMask: photos" +``` + +If the response is `{}` or `{"error": {...}}`, the key or its restrictions are blocking the request. If it returns a `photos` array, the key is valid and the issue is elsewhere. + +--- + ## MCP OAuth flow does not initiate / "Connect" redirects but authentication never starts **Cause:** TREK builds the OAuth 2.1 redirect URI from `APP_URL`. If `APP_URL` is not set, the authorization URL is constructed from a localhost fallback that external clients (Claude.ai, Claude Desktop) cannot reach, so the OAuth handshake never completes. From bfe6664ac4fd2452426c920b87059486c6823ab0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 May 2026 22:53:13 +0000 Subject: [PATCH 07/15] chore: bump version to 3.0.21 [skip ci] --- charts/trek/Chart.yaml | 4 ++-- client/package-lock.json | 4 ++-- client/package.json | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/trek/Chart.yaml b/charts/trek/Chart.yaml index 88488ea7..7d533803 100644 --- a/charts/trek/Chart.yaml +++ b/charts/trek/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 name: trek -version: 3.0.20 +version: 3.0.21 description: Minimal Helm chart for TREK app -appVersion: "3.0.20" +appVersion: "3.0.21" diff --git a/client/package-lock.json b/client/package-lock.json index fcab0a2c..44ecbb48 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-client", - "version": "3.0.20", + "version": "3.0.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-client", - "version": "3.0.20", + "version": "3.0.21", "dependencies": { "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", diff --git a/client/package.json b/client/package.json index d4976742..9c62e751 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "trek-client", - "version": "3.0.20", + "version": "3.0.21", "private": true, "type": "module", "scripts": { diff --git a/server/package-lock.json b/server/package-lock.json index 8fda935a..22da1273 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "trek-server", - "version": "3.0.20", + "version": "3.0.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trek-server", - "version": "3.0.20", + "version": "3.0.21", "dependencies": { "@modelcontextprotocol/sdk": "^1.28.0", "archiver": "^6.0.1", diff --git a/server/package.json b/server/package.json index 91bb626d..a29ce811 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "trek-server", - "version": "3.0.20", + "version": "3.0.21", "main": "src/index.ts", "scripts": { "start": "node --import tsx src/index.ts", From 75772445a7a396f4b534f027df5ce8997e67cd76 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Sun, 24 May 2026 19:39:53 +0200 Subject: [PATCH 08/15] Update security contact email in SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 218618a8..e2ce0414 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,7 +14,7 @@ Only the latest version receives security updates. Please update to the latest r If you discover a security vulnerability, please report it responsibly: 1. **Do not** open a public issue -2. Emails: **mauriceboe@icloud.com**, **trek-security@jubnl.ch** +2. Email: **report@liketrek.com** 3. Include a description of the vulnerability and steps to reproduce You will receive a response within 48 hours. Once confirmed, a fix will be released as soon as possible. From 86ee8044da01efb5c3bcf751f15c9a4bbe8bec41 Mon Sep 17 00:00:00 2001 From: "Julien G." <66769052+jubnl@users.noreply.github.com> Date: Mon, 25 May 2026 01:13:20 +0200 Subject: [PATCH 09/15] v3.0.22 Bug Fixes & Improvements (#1041) Bundles the v3.0.22 bug fixes and improvements. See the release notes for the full list. --- README.md | 2 +- client/src/api/client.ts | 18 ++- .../components/Journey/MobileEntryView.tsx | 2 +- client/src/components/Map/MapViewGL.tsx | 31 ++++ client/src/components/PDF/TripPDF.tsx | 3 +- .../src/components/Planner/DayDetailPanel.tsx | 29 ++-- .../src/components/Planner/DayPlanSidebar.tsx | 79 ++++++---- .../src/components/Planner/PlaceInspector.tsx | 41 ++++-- .../Planner/ReservationsPanel.test.tsx | 47 ++++++ .../components/Planner/ReservationsPanel.tsx | 55 +++---- .../src/components/Planner/TransportModal.tsx | 8 +- .../components/Settings/IntegrationsTab.tsx | 64 ++++++-- client/src/components/shared/PlaceAvatar.tsx | 14 +- client/src/i18n/translations/ar.ts | 6 + client/src/i18n/translations/br.ts | 6 + client/src/i18n/translations/cs.ts | 6 + client/src/i18n/translations/de.ts | 6 + client/src/i18n/translations/en.ts | 6 + client/src/i18n/translations/es.ts | 6 + client/src/i18n/translations/fr.ts | 6 + client/src/i18n/translations/hu.ts | 6 + client/src/i18n/translations/id.ts | 6 + client/src/i18n/translations/it.ts | 6 + client/src/i18n/translations/nl.ts | 6 + client/src/i18n/translations/pl.ts | 6 + client/src/i18n/translations/ru.ts | 6 + client/src/i18n/translations/zh.ts | 6 + client/src/i18n/translations/zhTw.ts | 6 + client/src/pages/JourneyDetailPage.tsx | 52 ++++--- client/src/pages/SharedTripPage.tsx | 8 +- client/src/pages/TripPlannerPage.tsx | 3 +- client/src/store/journeyStore.test.ts | 65 +++++++- client/src/store/journeyStore.ts | 70 +++++---- client/src/utils/dayMerge.test.ts | 18 ++- client/src/utils/dayMerge.ts | 2 +- client/src/utils/formatters.test.ts | 50 +++++++ client/src/utils/formatters.ts | 12 ++ client/src/utils/uploadQueue.ts | 106 +++++++++++++ client/vite.config.js | 2 +- server/src/app.ts | 2 +- server/src/db/migrations.ts | 36 +++++ server/src/mcp/tools/days.ts | 8 +- server/src/mcp/tools/places.ts | 16 +- server/src/mcp/tools/reservations.ts | 20 ++- server/src/mcp/tools/transports.ts | 22 ++- server/src/routes/oauth.ts | 51 ++++++- server/src/routes/reservations.ts | 6 +- server/src/services/atlasService.ts | 24 ++- server/src/services/budgetService.ts | 11 ++ server/src/services/oauthService.ts | 58 +++++++- server/src/services/tripService.ts | 5 + server/tests/integration/oauth.test.ts | 139 +++++++++++++++++- wiki/MCP-Overview.md | 10 ++ wiki/MCP-Setup.md | 65 ++++---- 54 files changed, 1110 insertions(+), 234 deletions(-) create mode 100644 client/src/utils/formatters.test.ts create mode 100644 client/src/utils/uploadQueue.ts diff --git a/README.md b/README.md index f3dfae0f..b1b68317 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
-Demo +Demo   Docker   diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 837ed16b..57c90fbb 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -209,7 +209,7 @@ export const oauthApi = { clients: { list: () => apiClient.get('/oauth/clients').then(r => r.data), - create: (data: { name: string; redirect_uris: string[]; allowed_scopes: string[] }) => + create: (data: { name: string; redirect_uris?: string[]; allowed_scopes: string[]; allows_client_credentials?: boolean }) => apiClient.post('/oauth/clients', data).then(r => r.data), rotate: (id: string) => apiClient.post(`/oauth/clients/${id}/rotate`).then(r => r.data), delete: (id: string) => apiClient.delete(`/oauth/clients/${id}`).then(r => r.data), @@ -407,8 +407,20 @@ export const journeyApi = { reorderEntries: (journeyId: number, orderedIds: number[]) => apiClient.put(`/journeys/${journeyId}/entries/reorder`, { orderedIds }).then(r => r.data), // Photos - uploadPhotos: (entryId: number, formData: FormData) => apiClient.post(`/journeys/entries/${entryId}/photos`, formData, { headers: { 'Content-Type': undefined as any } }).then(r => r.data), - uploadGalleryPhotos: (journeyId: number, formData: FormData) => apiClient.post(`/journeys/${journeyId}/gallery/photos`, formData, { headers: { 'Content-Type': undefined as any } }).then(r => r.data), + uploadPhotos: (entryId: number, formData: FormData, opts?: { onUploadProgress?: (e: import('axios').AxiosProgressEvent) => void; idempotencyKey?: string; signal?: AbortSignal }) => + apiClient.post(`/journeys/entries/${entryId}/photos`, formData, { + headers: { 'Content-Type': undefined as any, ...(opts?.idempotencyKey ? { 'X-Idempotency-Key': opts.idempotencyKey } : {}) }, + timeout: 0, + onUploadProgress: opts?.onUploadProgress, + signal: opts?.signal, + }).then(r => r.data), + uploadGalleryPhotos: (journeyId: number, formData: FormData, opts?: { onUploadProgress?: (e: import('axios').AxiosProgressEvent) => void; idempotencyKey?: string; signal?: AbortSignal }) => + apiClient.post(`/journeys/${journeyId}/gallery/photos`, formData, { + headers: { 'Content-Type': undefined as any, ...(opts?.idempotencyKey ? { 'X-Idempotency-Key': opts.idempotencyKey } : {}) }, + timeout: 0, + onUploadProgress: opts?.onUploadProgress, + signal: opts?.signal, + }).then(r => r.data), addProviderPhotosToGallery: (journeyId: number, provider: string, assetIds: string[], passphrase?: string) => apiClient.post(`/journeys/${journeyId}/gallery/provider-photos`, { provider, asset_ids: assetIds, ...(passphrase ? { passphrase } : {}) }).then(r => r.data), addProviderPhoto: (entryId: number, provider: string, assetId: string, caption?: string, passphrase?: string) => apiClient.post(`/journeys/entries/${entryId}/provider-photos`, { provider, asset_id: assetId, caption, ...(passphrase ? { passphrase } : {}) }).then(r => r.data), addProviderPhotos: (entryId: number, provider: string, assetIds: string[], caption?: string, passphrase?: string) => apiClient.post(`/journeys/entries/${entryId}/provider-photos`, { provider, asset_ids: assetIds, caption, ...(passphrase ? { passphrase } : {}) }).then(r => r.data), diff --git a/client/src/components/Journey/MobileEntryView.tsx b/client/src/components/Journey/MobileEntryView.tsx index 766f8b7f..fc340f4a 100644 --- a/client/src/components/Journey/MobileEntryView.tsx +++ b/client/src/components/Journey/MobileEntryView.tsx @@ -52,7 +52,7 @@ export default function MobileEntryView({ entry, readOnly, publicPhotoUrl, onClo const dateStr = date.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long' }) return ( -
+
{/* Top bar */}
@@ -360,7 +369,15 @@ export default function IntegrationsTab(): React.ReactElement {
-

{client.name}

+
+

{client.name}

+ {client.allows_client_credentials && ( + + {t('settings.oauth.badge.machine')} + + )} +

{t('settings.oauth.clientId')}: {client.client_id} {t('settings.mcp.tokenCreatedAt')} {new Date(client.created_at).toLocaleDateString(locale)} @@ -616,15 +633,26 @@ export default function IntegrationsTab(): React.ReactElement { autoFocus />

-
- -