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] 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
-
+
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 */}