From ed18c379036e5bcf6d04746382310709bacd0747 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 20 Sep 2026 13:54:31 +0200 Subject: [PATCH] fix: the findings of the final 4.3.0 audit Every file the release touches was read once more, and what survived an independent check against the code is fixed here, with a test for each. Data and credentials - A document sync connection edited to a new address no longer carries the stored token along: a blank form under another origin is refused, the same rule the probe route already had. Dawarich and AirTrail testConnection send the stored key only to the host it was stored for. - The key rotation script now covers document_connections.secrets and trip_document_links.webhook_secret, which it had skipped. - Turning a booked night into a pause in the road trip popup asks first and says that the booking and its expense go with it. Deleting a place says the same when a night is booked there, on the desktop and on the phone. - A moved version marker over unchanged bytes no longer replaces the TREK file and its attachments; a real revision carries the attachments over. Planner - A drop in the day plan names the row it lands ahead of in the full day, so a hidden hotel stop no longer shifts it one slot early; the store sorts before it splices. Via re-anchoring, the booked-night guard and the sidebar's day membership read the stored day, the index space the server uses. - A hotel stop placed mid-day re-pins the road trip vias of that day, and the booking session receives the new stop order too. The Public API and the iCal feed list a booked hotel once. A plugin can no longer write a stay against another trip's day. Road trip and map - The route overview paces its requests to the public routers and retries a rate limit, and it says which legs it could not route instead of quietly reporting a shorter trip. A click on a via handle no longer drops a second via; handles hide when zooming out. Read-only members cannot edit the driving limits. The phone shows a way back when no day is picked and prints the night drive departure in the chosen clock format. Integrations and search - Google transit keeps conventional trains when the train chip is on. TREK_PLACES_ENABLED=false stops the details and enrichment calls too, and accepts the whole boolean family. A caller-supplied description no longer lands in the instance-wide enrichment cache. Nominatim searches carry a timeout. An OSM place id is checked for shape before it reaches Overpass. - Dawarich puts a stay on the trip whose dates hold it, the Atlas hand-off knows what the Atlas already shows, and a host change with a new key clears the old instance's state. - Document sync: a webhook address pasted by hand works on the token alone, deleting a connection unregisters its subscriptions, Nextcloud under a sub-path connects, Synology answers a name collision before uploading and verifies a move into the bin, Paperless titles are capped at 128 characters, a DNS failure reads as unreachable rather than as a policy verdict, videos from the store are accepted like uploads, and remote_missing has a way out. Conflict buttons only for the owner, a refused choice is reported, a refused credential gets a Reconnect button, disconnecting asks first on the desktop as it does on the phone, and the folder picker no longer spins forever. Release - The server holds the release notice back from a bundle that cannot draw it, so the previous shell served from the service worker cache after an update no longer shows bare keys and no longer lets a click use the notice up. - Import into an existing list gets the same body limit as a new list. Chat images say when the limit is reached and the new controls are translated. The transit backend switch reports a failed save. The Helm chart carries the Amap credentials in its Secret; the shipped templates document the Places API and Amap variables and the Overpass default. The wiki follows. --- charts/trek/templates/configmap.yaml | 12 + charts/trek/templates/deployment.yaml | 12 + charts/trek/templates/secret.yaml | 12 + charts/trek/values.yaml | 29 +- client/src/api/client.surface.test.ts | 47 +++- client/src/api/client.ts | 42 ++- .../components/Admin/AddonManager.test.tsx | 18 +- client/src/components/Admin/AddonManager.tsx | 2 +- .../src/components/Collab/CollabChat.test.tsx | 27 ++ client/src/components/Collab/CollabChat.tsx | 6 +- .../Collab/CollabChatAttachment.tsx | 7 +- .../components/Collab/useChatImages.test.ts | 44 ++- client/src/components/Collab/useChatImages.ts | 25 +- client/src/components/Collab/useCollabChat.ts | 7 +- .../Dawarich/DawarichAcceptDialog.test.tsx | 59 +++- .../Dawarich/DawarichAcceptDialog.tsx | 33 ++- .../Files/docsync/DocSyncBinding.test.tsx | 79 +++++- .../Files/docsync/DocSyncBinding.tsx | 44 ++- .../Files/docsync/DocSyncPanel.test.tsx | 126 ++++++++- .../components/Files/docsync/DocSyncPanel.tsx | 47 +++- .../Files/docsync/DocSyncScopeModal.test.tsx | 18 +- .../Files/docsync/useDocSync.test.ts | 75 ++++- .../components/Files/docsync/useDocSync.ts | 52 +++- client/src/components/Map/MapViewGL.test.tsx | 112 ++++++++ client/src/components/Map/MapViewGL.tsx | 32 ++- .../components/Map/TripRouteOverview.test.tsx | 35 +++ .../src/components/Map/TripRouteOverview.tsx | 30 +- .../components/Map/tripRouteGeometry.test.ts | 110 +++++++- .../src/components/Map/tripRouteGeometry.ts | 95 +++++-- .../Map/useTripRouteOverview.test.ts | 78 +++++- .../components/Map/useTripRouteOverview.ts | 23 +- .../Planner/DayPlanSidebar.test.tsx | 98 +++++++ .../src/components/Planner/DayPlanSidebar.tsx | 44 ++- .../Planner/PlaceInspector.test.tsx | 46 ++++ .../src/components/Planner/PlaceInspector.tsx | 10 +- .../Planner/usePlacesSidebar.test.tsx | 17 +- .../components/Planner/usePlacesSidebar.ts | 18 +- .../Roadtrip/RoadtripLimitsCard.test.tsx | 49 +++- .../Roadtrip/RoadtripLimitsCard.tsx | 34 ++- .../SystemNotices/SystemNoticeHost.tsx | 4 + .../screens/admin/MAdminAddonManager.tsx | 2 +- .../screens/trip/roadtrip/MRoadtripRows.tsx | 2 +- .../screens/trip/roadtrip/MRoadtripTab.tsx | 31 ++- .../screens/trip/sheets/MTripSheets.tsx | 12 +- .../mobile/screens/trip/tabs/MCollabChat.tsx | 7 +- .../screens/trip/tabs/MDocSyncSheet.test.tsx | 136 ++++++++- .../screens/trip/tabs/MDocSyncSheet.tsx | 123 +++++++-- client/src/pages/TripPlannerPage.tsx | 19 +- .../src/pages/TripPlannerPage.wiring.test.tsx | 18 +- .../src/pages/admin/AdminSettingsTab.test.tsx | 24 +- client/src/pages/admin/AdminSettingsTab.tsx | 7 +- .../useTripPlanner.roadtrip.test.tsx | 258 +++++++++++++++++- .../pages/tripPlanner/useTripPlanner.test.tsx | 78 +++++- .../src/pages/tripPlanner/useTripPlanner.ts | 162 +++++++++-- client/src/repo/roadtripHazardsRepo.test.ts | 61 +++++ client/src/repo/roadtripHazardsRepo.ts | 15 +- client/src/store/slices/assignmentsSlice.ts | 7 +- client/src/store/stayStops.ts | 8 +- client/src/store/systemNoticeStore.ts | 7 +- .../mobile/admin/MAdminAddonManager.test.tsx | 19 +- .../unit/mobile/trip/MCollabChat.test.tsx | 35 ++- .../unit/mobile/trip/MRoadtripRows.test.tsx | 11 +- .../unit/mobile/trip/MRoadtripTab.test.tsx | 28 +- .../unit/mobile/trip/MTripSheets.test.tsx | 14 +- .../unit/slices/assignmentsSlice.test.ts | 29 ++ .../unit/stores/systemNoticeStore.test.ts | 71 +++++ docker-compose.yml | 5 + docs/system-notices.md | 10 +- server/.env.example | 9 +- server/scripts/migrate-encryption.ts | 38 +++ server/src/app-config/derive.ts | 8 +- server/src/bootstrap.ts | 12 +- .../accommodations.controller.ts | 36 ++- .../accommodations/accommodations.service.ts | 149 +++++++++- server/src/nest/calendar/calendar.service.ts | 6 + .../nest/doc-sync/doc-sync-config.service.ts | 58 +++- .../doc-sync/doc-sync-webhook.controller.ts | 14 +- .../src/nest/doc-sync/doc-sync.controller.ts | 41 +-- server/src/nest/doc-sync/doc-sync.helpers.ts | 84 +++++- server/src/nest/doc-sync/doc-sync.service.ts | 87 ++++-- .../doc-sync/providers/paperless.provider.ts | 22 +- .../nest/doc-sync/providers/provider-http.ts | 17 +- .../providers/synology-drive.provider.ts | 56 +++- .../nest/doc-sync/providers/webdav.client.ts | 97 +++++-- .../doc-sync/providers/webdav.provider.ts | 22 +- .../src/nest/integrations/airtrail.service.ts | 35 ++- .../dawarich-suggestions.service.ts | 16 +- .../integrations/dawarich-sync.service.ts | 60 +++- .../src/nest/integrations/dawarich.service.ts | 28 +- server/src/nest/maps/maps.helpers.ts | 5 +- server/src/nest/maps/maps.service.ts | 28 +- server/src/nest/maps/trek-places.client.ts | 8 + .../place-enrichment.service.ts | 60 ++-- server/src/nest/places/places.controller.ts | 14 +- server/src/nest/places/places.mcp.ts | 4 +- .../src/nest/public-api/public-api.service.ts | 7 + .../src/nest/reservations/reservations.rpc.ts | 27 +- server/src/nest/roadtrip/charging.mcp.ts | 3 +- .../src/nest/roadtrip/day-boundaries.mcp.ts | 14 +- server/src/nest/roadtrip/google-route.mcp.ts | 5 +- .../src/nest/roadtrip/roadtrip-mcp.helpers.ts | 37 +++ .../nest/roadtrip/roadtrip-planning.mcp.ts | 183 +++++++------ .../nest/roadtrip/roadtrip-preferences.mcp.ts | 4 +- .../system-notices.controller.ts | 17 +- .../system-notices/system-notices.service.ts | 15 +- .../nest/transit/google-transit.provider.ts | 16 +- server/tests/e2e/dawarich.e2e.test.ts | 26 ++ server/tests/e2e/doc-sync.e2e.test.ts | 37 +++ server/tests/e2e/maps.e2e.test.ts | 17 ++ server/tests/e2e/places.e2e.test.ts | 17 ++ server/tests/e2e/public-api.e2e.test.ts | 22 +- server/tests/e2e/system-notices.e2e.test.ts | 28 ++ server/tests/integration/misc.test.ts | 14 +- .../tests/integration/systemNotices.test.ts | 26 ++ .../unit/app-config/config-templates.test.ts | 83 ++++++ server/tests/unit/app-config/derive.test.ts | 25 +- .../unit/db/migrate-encryption-parity.test.ts | 68 +++++ .../nest/accommodations.controller.test.ts | 23 +- .../unit/nest/accommodations.service.test.ts | 216 +++++++++++++++ .../tests/unit/nest/calendar.service.test.ts | 23 ++ .../unit/nest/collections.service.test.ts | 3 +- .../nest/dawarich-suggestions.service.test.ts | 56 +++- .../unit/nest/dawarich-sync.service.test.ts | 95 +++++++ .../tests/unit/nest/dawarich.service.test.ts | 84 ++++++ .../doc-sync/doc-sync-config.service.test.ts | 96 +++++++ .../doc-sync-webhook.controller.test.ts | 46 +++- .../nest/doc-sync/doc-sync.controller.test.ts | 88 +++++- .../nest/doc-sync/doc-sync.helpers.test.ts | 118 ++++++++ .../nest/doc-sync/doc-sync.service.test.ts | 159 ++++++++++- .../nest/doc-sync/paperless.provider.test.ts | 24 ++ .../unit/nest/doc-sync/provider-http.test.ts | 13 + .../doc-sync/synology-drive.provider.test.ts | 98 ++++++- .../nest/doc-sync/webdav.provider.test.ts | 113 ++++++++ .../unit/nest/google-transit.provider.test.ts | 58 ++++ .../tests/unit/nest/maps.gers-details.test.ts | 14 + .../tests/unit/nest/maps.search-merge.test.ts | 17 ++ server/tests/unit/nest/maps.service.test.ts | 34 +++ .../nest/place-enrichment.description.test.ts | 169 ++++++++++-- .../nest/place-enrichment.service.test.ts | 1 + .../tests/unit/nest/places.controller.test.ts | 26 +- .../unit/nest/roadtrip-mcp.helpers.test.ts | 38 +++ .../tests/unit/nest/roadtrip-planning.test.ts | 31 +++ .../unit/nest/roadtrip-read-tools.test.ts | 26 ++ .../nest/system-notices.controller.test.ts | 28 +- .../unit/nest/system-notices.service.test.ts | 38 +++ .../unit/nest/trek-places.client.test.ts | 29 +- .../unit/reservations/bookings.rpc.test.ts | 34 ++- .../services/airtrailTestConnection.test.ts | 78 ++++++ shared/src/dawarich/dawarich.schema.ts | 6 +- shared/src/i18n/ar/admin.ts | 1 + shared/src/i18n/ar/collab.ts | 5 + shared/src/i18n/ar/docsync.ts | 1 + shared/src/i18n/ar/map.ts | 2 + shared/src/i18n/ar/mobileTrip.ts | 2 + shared/src/i18n/ar/roadtrip.ts | 4 + shared/src/i18n/ar/trip.ts | 2 + shared/src/i18n/br/admin.ts | 1 + shared/src/i18n/br/collab.ts | 5 + shared/src/i18n/br/docsync.ts | 1 + shared/src/i18n/br/map.ts | 2 + shared/src/i18n/br/mobileTrip.ts | 2 + shared/src/i18n/br/roadtrip.ts | 4 + shared/src/i18n/br/trip.ts | 2 + shared/src/i18n/ca/admin.ts | 1 + shared/src/i18n/ca/collab.ts | 5 + shared/src/i18n/ca/docsync.ts | 1 + shared/src/i18n/ca/map.ts | 2 + shared/src/i18n/ca/mobileTrip.ts | 2 + shared/src/i18n/ca/roadtrip.ts | 4 + shared/src/i18n/ca/trip.ts | 2 + shared/src/i18n/cs/admin.ts | 1 + shared/src/i18n/cs/collab.ts | 5 + shared/src/i18n/cs/docsync.ts | 1 + shared/src/i18n/cs/map.ts | 2 + shared/src/i18n/cs/mobileTrip.ts | 2 + shared/src/i18n/cs/roadtrip.ts | 4 + shared/src/i18n/cs/trip.ts | 2 + shared/src/i18n/de/admin.ts | 1 + shared/src/i18n/de/collab.ts | 5 + shared/src/i18n/de/docsync.ts | 1 + shared/src/i18n/de/map.ts | 2 + shared/src/i18n/de/mobileTrip.ts | 2 + shared/src/i18n/de/roadtrip.ts | 4 + shared/src/i18n/de/trip.ts | 2 + shared/src/i18n/en/admin.ts | 1 + shared/src/i18n/en/collab.ts | 5 + shared/src/i18n/en/docsync.ts | 1 + shared/src/i18n/en/map.ts | 2 + shared/src/i18n/en/mobileTrip.ts | 2 + shared/src/i18n/en/roadtrip.ts | 4 + shared/src/i18n/en/trip.ts | 2 + shared/src/i18n/es/admin.ts | 1 + shared/src/i18n/es/collab.ts | 5 + shared/src/i18n/es/docsync.ts | 1 + shared/src/i18n/es/map.ts | 2 + shared/src/i18n/es/mobileTrip.ts | 2 + shared/src/i18n/es/roadtrip.ts | 4 + shared/src/i18n/es/trip.ts | 2 + shared/src/i18n/fr/admin.ts | 1 + shared/src/i18n/fr/collab.ts | 5 + shared/src/i18n/fr/docsync.ts | 1 + shared/src/i18n/fr/map.ts | 2 + shared/src/i18n/fr/mobileTrip.ts | 2 + shared/src/i18n/fr/roadtrip.ts | 4 + shared/src/i18n/fr/trip.ts | 2 + shared/src/i18n/gr/admin.ts | 1 + shared/src/i18n/gr/collab.ts | 5 + shared/src/i18n/gr/docsync.ts | 1 + shared/src/i18n/gr/map.ts | 2 + shared/src/i18n/gr/mobileTrip.ts | 2 + shared/src/i18n/gr/roadtrip.ts | 4 + shared/src/i18n/gr/trip.ts | 2 + shared/src/i18n/hu/admin.ts | 1 + shared/src/i18n/hu/collab.ts | 5 + shared/src/i18n/hu/docsync.ts | 1 + shared/src/i18n/hu/map.ts | 2 + shared/src/i18n/hu/mobileTrip.ts | 2 + shared/src/i18n/hu/roadtrip.ts | 4 + shared/src/i18n/hu/trip.ts | 2 + shared/src/i18n/id/admin.ts | 1 + shared/src/i18n/id/collab.ts | 5 + shared/src/i18n/id/docsync.ts | 1 + shared/src/i18n/id/map.ts | 2 + shared/src/i18n/id/mobileTrip.ts | 2 + shared/src/i18n/id/roadtrip.ts | 4 + shared/src/i18n/id/trip.ts | 2 + shared/src/i18n/it/admin.ts | 1 + shared/src/i18n/it/collab.ts | 5 + shared/src/i18n/it/docsync.ts | 1 + shared/src/i18n/it/map.ts | 2 + shared/src/i18n/it/mobileTrip.ts | 2 + shared/src/i18n/it/roadtrip.ts | 4 + shared/src/i18n/it/trip.ts | 2 + shared/src/i18n/ja/admin.ts | 1 + shared/src/i18n/ja/collab.ts | 5 + shared/src/i18n/ja/docsync.ts | 1 + shared/src/i18n/ja/map.ts | 2 + shared/src/i18n/ja/mobileTrip.ts | 2 + shared/src/i18n/ja/roadtrip.ts | 4 + shared/src/i18n/ja/trip.ts | 2 + shared/src/i18n/ko/admin.ts | 1 + shared/src/i18n/ko/collab.ts | 5 + shared/src/i18n/ko/docsync.ts | 1 + shared/src/i18n/ko/map.ts | 2 + shared/src/i18n/ko/mobileTrip.ts | 2 + shared/src/i18n/ko/roadtrip.ts | 4 + shared/src/i18n/ko/trip.ts | 2 + shared/src/i18n/nl/admin.ts | 1 + shared/src/i18n/nl/collab.ts | 5 + shared/src/i18n/nl/docsync.ts | 1 + shared/src/i18n/nl/map.ts | 2 + shared/src/i18n/nl/mobileTrip.ts | 2 + shared/src/i18n/nl/roadtrip.ts | 4 + shared/src/i18n/nl/trip.ts | 2 + shared/src/i18n/pl/admin.ts | 1 + shared/src/i18n/pl/collab.ts | 5 + shared/src/i18n/pl/docsync.ts | 1 + shared/src/i18n/pl/map.ts | 2 + shared/src/i18n/pl/mobileTrip.ts | 2 + shared/src/i18n/pl/roadtrip.ts | 4 + shared/src/i18n/pl/trip.ts | 2 + shared/src/i18n/ru/admin.ts | 1 + shared/src/i18n/ru/collab.ts | 5 + shared/src/i18n/ru/docsync.ts | 1 + shared/src/i18n/ru/map.ts | 2 + shared/src/i18n/ru/mobileTrip.ts | 2 + shared/src/i18n/ru/roadtrip.ts | 4 + shared/src/i18n/ru/trip.ts | 2 + shared/src/i18n/sv/admin.ts | 1 + shared/src/i18n/sv/collab.ts | 5 + shared/src/i18n/sv/docsync.ts | 1 + shared/src/i18n/sv/map.ts | 2 + shared/src/i18n/sv/mobileTrip.ts | 2 + shared/src/i18n/sv/roadtrip.ts | 4 + shared/src/i18n/sv/trip.ts | 2 + shared/src/i18n/tr/admin.ts | 1 + shared/src/i18n/tr/collab.ts | 5 + shared/src/i18n/tr/docsync.ts | 1 + shared/src/i18n/tr/map.ts | 2 + shared/src/i18n/tr/mobileTrip.ts | 2 + shared/src/i18n/tr/roadtrip.ts | 4 + shared/src/i18n/tr/trip.ts | 2 + shared/src/i18n/uk/admin.ts | 1 + shared/src/i18n/uk/collab.ts | 5 + shared/src/i18n/uk/docsync.ts | 1 + shared/src/i18n/uk/map.ts | 2 + shared/src/i18n/uk/mobileTrip.ts | 2 + shared/src/i18n/uk/roadtrip.ts | 4 + shared/src/i18n/uk/trip.ts | 2 + shared/src/i18n/vi/admin.ts | 1 + shared/src/i18n/vi/collab.ts | 5 + shared/src/i18n/vi/docsync.ts | 1 + shared/src/i18n/vi/map.ts | 2 + shared/src/i18n/vi/mobileTrip.ts | 2 + shared/src/i18n/vi/roadtrip.ts | 4 + shared/src/i18n/vi/trip.ts | 2 + shared/src/i18n/zh-TW/admin.ts | 1 + shared/src/i18n/zh-TW/collab.ts | 5 + shared/src/i18n/zh-TW/docsync.ts | 1 + shared/src/i18n/zh-TW/map.ts | 2 + shared/src/i18n/zh-TW/mobileTrip.ts | 2 + shared/src/i18n/zh-TW/roadtrip.ts | 4 + shared/src/i18n/zh-TW/trip.ts | 2 + shared/src/i18n/zh/admin.ts | 1 + shared/src/i18n/zh/collab.ts | 5 + shared/src/i18n/zh/docsync.ts | 1 + shared/src/i18n/zh/map.ts | 2 + shared/src/i18n/zh/mobileTrip.ts | 2 + shared/src/i18n/zh/roadtrip.ts | 4 + shared/src/i18n/zh/trip.ts | 2 + shared/src/maps/maps.schema.ts | 7 +- wiki/Document-Sync.md | 21 +- wiki/Environment-Variables.md | 8 +- wiki/MCP-Tools-and-Resources.md | 4 +- wiki/TREK-Places-API.md | 4 +- 315 files changed, 6307 insertions(+), 572 deletions(-) create mode 100644 client/src/repo/roadtripHazardsRepo.test.ts create mode 100644 client/tests/unit/stores/systemNoticeStore.test.ts create mode 100644 server/src/nest/roadtrip/roadtrip-mcp.helpers.ts create mode 100644 server/tests/unit/app-config/config-templates.test.ts create mode 100644 server/tests/unit/nest/roadtrip-mcp.helpers.test.ts create mode 100644 server/tests/unit/services/airtrailTestConnection.test.ts diff --git a/charts/trek/templates/configmap.yaml b/charts/trek/templates/configmap.yaml index aeab34b07..a7397e76f 100644 --- a/charts/trek/templates/configmap.yaml +++ b/charts/trek/templates/configmap.yaml @@ -85,3 +85,15 @@ data: {{- if .Values.env.OVERPASS_TIMEOUT_MS }} OVERPASS_TIMEOUT_MS: {{ .Values.env.OVERPASS_TIMEOUT_MS | quote }} {{- end }} + {{- /* Tested for presence rather than truth: the one value this switch gets set to is + false, and an unquoted false (or --set env.TREK_PLACES_ENABLED=false) would + otherwise drop the key and leave the index on. */}} + {{- if not (kindIs "invalid" .Values.env.TREK_PLACES_ENABLED) }} + TREK_PLACES_ENABLED: {{ .Values.env.TREK_PLACES_ENABLED | quote }} + {{- end }} + {{- if .Values.env.TREK_PLACES_URL }} + TREK_PLACES_URL: {{ .Values.env.TREK_PLACES_URL | quote }} + {{- end }} + {{- if .Values.env.AMAP_API_BASE }} + AMAP_API_BASE: {{ .Values.env.AMAP_API_BASE | quote }} + {{- end }} diff --git a/charts/trek/templates/deployment.yaml b/charts/trek/templates/deployment.yaml index 7b24b98dd..24e67325c 100644 --- a/charts/trek/templates/deployment.yaml +++ b/charts/trek/templates/deployment.yaml @@ -75,6 +75,18 @@ spec: name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }} key: UNSPLASH_ACCESS_KEY optional: true + - name: AMAP_API_KEY + valueFrom: + secretKeyRef: + name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }} + key: AMAP_API_KEY + optional: true + - name: AMAP_API_SECRET + valueFrom: + secretKeyRef: + name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }} + key: AMAP_API_SECRET + optional: true volumeMounts: - name: data mountPath: /app/data diff --git a/charts/trek/templates/secret.yaml b/charts/trek/templates/secret.yaml index 08d81cabd..c465e8e34 100644 --- a/charts/trek/templates/secret.yaml +++ b/charts/trek/templates/secret.yaml @@ -20,6 +20,12 @@ data: {{- if .Values.secretEnv.UNSPLASH_ACCESS_KEY }} UNSPLASH_ACCESS_KEY: {{ .Values.secretEnv.UNSPLASH_ACCESS_KEY | b64enc | quote }} {{- end }} + {{- if .Values.secretEnv.AMAP_API_KEY }} + AMAP_API_KEY: {{ .Values.secretEnv.AMAP_API_KEY | b64enc | quote }} + {{- end }} + {{- if .Values.secretEnv.AMAP_API_SECRET }} + AMAP_API_SECRET: {{ .Values.secretEnv.AMAP_API_SECRET | b64enc | quote }} + {{- end }} {{- end }} {{- if and (not .Values.existingSecret) (.Values.generateEncryptionKey) }} @@ -50,4 +56,10 @@ stringData: {{- if .Values.secretEnv.UNSPLASH_ACCESS_KEY }} UNSPLASH_ACCESS_KEY: {{ .Values.secretEnv.UNSPLASH_ACCESS_KEY }} {{- end }} + {{- if .Values.secretEnv.AMAP_API_KEY }} + AMAP_API_KEY: {{ .Values.secretEnv.AMAP_API_KEY }} + {{- end }} + {{- if .Values.secretEnv.AMAP_API_SECRET }} + AMAP_API_SECRET: {{ .Values.secretEnv.AMAP_API_SECRET }} + {{- end }} {{- end }} diff --git a/charts/trek/values.yaml b/charts/trek/values.yaml index 69c136e04..0d4bb1aeb 100644 --- a/charts/trek/values.yaml +++ b/charts/trek/values.yaml @@ -90,8 +90,24 @@ env: # Custom Overpass endpoint(s) for the map POI "explore" search, comma-separated. When set, REPLACES the bundled # public mirrors — point it at an internal/self-hosted Overpass instance when the public mirrors are unreachable # from the cluster (e.g. locked-down egress). Non-http(s) entries are ignored. - # OVERPASS_TIMEOUT_MS: "12000" - # Per-endpoint timeout (ms) for Overpass POI requests. Raise it for a slow self-hosted Overpass instance. Defaults to 12000. + # OVERPASS_TIMEOUT_MS: "25000" + # Per-endpoint timeout (ms) for Overpass POI requests. The query itself gives Overpass 20 seconds of work, so a + # lower value hangs up on answers that were still coming. Raise it for a slow self-hosted Overpass instance. + # Defaults to 25000. + # TREK_PLACES_ENABLED: "false" + # Place search (suggestions, full search, category buttons, Road trip search, import geocoding, offline downloads) + # asks the TREK Places API at https://places.liketrek.com by default, so the pod makes outbound requests to that + # host. Set "false" to search the way it did before 4.3.0 (OpenStreetMap, or your Google/Amap key) and send + # nothing there. There is no switch in the admin panel on purpose: whether searches leave the instance is decided + # here. Defaults to on. + # TREK_PLACES_URL: "" + # Base URL of a copy of the Places API you run yourself; it has to answer the same /v1 API. Read from + # configuration rather than typed by a user, so an in-cluster address works without ALLOW_INTERNAL_NETWORK. + # Defaults to https://places.liketrek.com. + # AMAP_API_BASE: "" + # Send the Amap (高德地图) calls through an egress proxy, a cache or a gateway that speaks the same API instead of + # restapi.amap.com directly. The key itself is a credential and goes under secretEnv.AMAP_API_KEY, or into + # Admin > Settings > API Keys. Defaults to https://restapi.amap.com. # Secret environment variables stored in a Kubernetes Secret. @@ -117,6 +133,15 @@ secretEnv: # Get a free key at https://unsplash.com/developers. Can also be set per-admin # in Admin > Settings; this value overrides that. Leave empty to disable. UNSPLASH_ACCESS_KEY: "" + # Optional Amap (高德地图) Web 服务 key for place search and geocoding inside mainland + # China. Overrides the key set in Admin > Settings > API Keys. A Web 端 (JS API) key is + # a different credential and is rejected with INVALID_USER_KEY. Leave empty to use the + # key from the admin panel, or none. + AMAP_API_KEY: "" + # The private secret (数字签名) of that key. Required exactly when the key was created + # with signing enabled, because such a key rejects every unsigned request. Leave empty + # otherwise. The endpoint itself is not a secret and is set under env.AMAP_API_BASE. + AMAP_API_SECRET: "" # If true, a random ENCRYPTION_KEY is generated at install and preserved across upgrades generateEncryptionKey: false diff --git a/client/src/api/client.surface.test.ts b/client/src/api/client.surface.test.ts index eebc8f933..be5aee90b 100644 --- a/client/src/api/client.surface.test.ts +++ b/client/src/api/client.surface.test.ts @@ -1,4 +1,4 @@ -// FE-APISURF-001 to FE-APISURF-056 +// FE-APISURF-001 to FE-APISURF-057 import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import type { AxiosResponse } from 'axios' import { http, HttpResponse } from 'msw' @@ -10,7 +10,7 @@ import { mapsApi, airportsApi, budgetApi, filesApi, reservationsApi, healthApi, weatherApi, configApi, helpApi, settingsApi, accommodationsApi, dayNotesApi, collabApi, backupApi, shareApi, transitApi, tripInviteApi, notificationsApi, inAppNotificationsApi, memoriesApi, - docsyncApi, + docsyncApi, DOCSYNC_RUN_TIMEOUT_MS, DOCSYNC_UPSTREAM_TIMEOUT_MS, } from './client' interface Recorded { method: string; url: string; body: unknown } @@ -902,4 +902,47 @@ describe('client > multipart uploads', () => { expect(call[2]).toMatchObject({ timeout: 40000 }) } }) + + it('FE-APISURF-057: every document-sync call that reaches the store outlives the 8s global timeout', async () => { + const post = spyPost() + const get = vi.spyOn(apiClient, 'get').mockResolvedValue({ data: { ok: true } } as unknown as AxiosResponse) + const del = vi.spyOn(apiClient, 'delete').mockResolvedValue({ data: { ok: true } } as unknown as AxiosResponse) + + // The server allows each request to a store 15 s, and a run waits for the + // whole listing and every transfer; cut off at 8 s the browser reported a + // failure while the server carried on and finished. + await docsyncApi.testConnection(1, { providerId: 'paperless' }) + await docsyncApi.createScope(1, 5, 'Norway') + await docsyncApi.createLink(1, { connectionId: 5 }) + await docsyncApi.listScopes(1, 5) + await docsyncApi.deleteLink(1, 9) + + expect(post.mock.calls.map(c => c[0])).toEqual([ + '/trips/1/docsync/connections/test', + '/trips/1/docsync/connections/5/scopes', + '/trips/1/docsync/links', + ]) + for (const call of post.mock.calls) { + expect(call[2]).toMatchObject({ timeout: DOCSYNC_UPSTREAM_TIMEOUT_MS }) + } + expect(get.mock.calls[0][0]).toBe('/trips/1/docsync/connections/5/scopes') + expect(get.mock.calls[0][1]).toMatchObject({ timeout: DOCSYNC_UPSTREAM_TIMEOUT_MS }) + expect(del.mock.calls[0][0]).toBe('/trips/1/docsync/links/9') + expect(del.mock.calls[0][1]).toMatchObject({ timeout: DOCSYNC_UPSTREAM_TIMEOUT_MS }) + expect(DOCSYNC_UPSTREAM_TIMEOUT_MS).toBeGreaterThan(8000) + + // A run, and a conflict choice that runs the binding afterwards. + post.mockClear() + await docsyncApi.syncNow(1, 9) + await docsyncApi.resolve(1, 4, 'trek') + + expect(post.mock.calls.map(c => c[0])).toEqual([ + '/trips/1/docsync/links/9/sync', + '/trips/1/docsync/items/4/resolve', + ]) + for (const call of post.mock.calls) { + expect(call[2]).toMatchObject({ timeout: DOCSYNC_RUN_TIMEOUT_MS }) + } + expect(DOCSYNC_RUN_TIMEOUT_MS).toBeGreaterThan(DOCSYNC_UPSTREAM_TIMEOUT_MS) + }) }) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index c033fc057..4d5809557 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -1361,6 +1361,34 @@ export const filesApi = { getLinks: (tripId: number | string, fileId: number) => apiClient.get(`/trips/${tripId}/files/${fileId}/links`).then(r => r.data), } +/** + * How long the browser waits for a document-sync call that makes the server go + * and ask the store. + * + * The shared 8 s on `apiClient` is right for TREK's own routes and wrong for + * these, for the reason `dawarich.ts` gives: the server allows each request to + * a store 15 s by itself (20 s for Papra), a connection test may probe more + * than once, and binding or unbinding first registers or removes a webhook at + * the store. Cut off at 8 s, the browser reported a failure while the server + * carried on and finished, so a listing that took twelve seconds read as a + * broken store. + */ +export const DOCSYNC_UPSTREAM_TIMEOUT_MS = 60_000 + +/** + * A run is longer again. The server answers only once the whole of it is + * through: a listing of up to 40 pages, then up to 25 transfers at up to 120 s + * each, and Paperless-ngx holds every push until its consume task has run. + * Resolving a conflict runs the binding afterwards, so it waits just as long. + * Ten minutes covers a full transfer budget of ordinary documents; a run that + * outlasts it still finishes on the server, and the `docsync:changed` ping + * refreshes the dialog when it does. + */ +export const DOCSYNC_RUN_TIMEOUT_MS = 600_000 + +const docsyncUpstream = { timeout: DOCSYNC_UPSTREAM_TIMEOUT_MS } +const docsyncRun = { timeout: DOCSYNC_RUN_TIMEOUT_MS } + /** * Document sync: one provider connection per trip, shared by every member. * @@ -1373,22 +1401,22 @@ export const docsyncApi = { status: (tripId: number | string) => apiClient.get(`/trips/${tripId}/docsync/status`).then(r => r.data), listConnections: (tripId: number | string) => apiClient.get(`/trips/${tripId}/docsync/connections`).then(r => r.data), saveConnection: (tripId: number | string, data: unknown) => apiClient.put(`/trips/${tripId}/docsync/connections`, data).then(r => r.data), - testConnection: (tripId: number | string, data: unknown) => apiClient.post(`/trips/${tripId}/docsync/connections/test`, data).then(r => r.data), + testConnection: (tripId: number | string, data: unknown) => apiClient.post(`/trips/${tripId}/docsync/connections/test`, data, docsyncUpstream).then(r => r.data), deleteConnection: (tripId: number | string, connectionId: number) => apiClient.delete(`/trips/${tripId}/docsync/connections/${connectionId}`).then(r => r.data), listScopes: (tripId: number | string, connectionId: number, q?: string) => - apiClient.get(`/trips/${tripId}/docsync/connections/${connectionId}/scopes`, { params: q ? { q } : {} }).then(r => r.data), + apiClient.get(`/trips/${tripId}/docsync/connections/${connectionId}/scopes`, { params: q ? { q } : {}, ...docsyncUpstream }).then(r => r.data), createScope: (tripId: number | string, connectionId: number, name: string) => - apiClient.post(`/trips/${tripId}/docsync/connections/${connectionId}/scopes`, { name }).then(r => r.data), + apiClient.post(`/trips/${tripId}/docsync/connections/${connectionId}/scopes`, { name }, docsyncUpstream).then(r => r.data), listLinks: (tripId: number | string) => apiClient.get(`/trips/${tripId}/docsync/links`).then(r => r.data), - createLink: (tripId: number | string, data: unknown) => apiClient.post(`/trips/${tripId}/docsync/links`, data).then(r => r.data), + createLink: (tripId: number | string, data: unknown) => apiClient.post(`/trips/${tripId}/docsync/links`, data, docsyncUpstream).then(r => r.data), updateLink: (tripId: number | string, linkId: number, data: unknown) => apiClient.patch(`/trips/${tripId}/docsync/links/${linkId}`, data).then(r => r.data), - deleteLink: (tripId: number | string, linkId: number) => apiClient.delete(`/trips/${tripId}/docsync/links/${linkId}`).then(r => r.data), + deleteLink: (tripId: number | string, linkId: number) => apiClient.delete(`/trips/${tripId}/docsync/links/${linkId}`, docsyncUpstream).then(r => r.data), syncNow: (tripId: number | string, linkId: number, full = false) => - apiClient.post(`/trips/${tripId}/docsync/links/${linkId}/sync`, { full }).then(r => r.data), + apiClient.post(`/trips/${tripId}/docsync/links/${linkId}/sync`, { full }, docsyncRun).then(r => r.data), items: (tripId: number | string, state?: string) => apiClient.get(`/trips/${tripId}/docsync/items`, { params: state ? { state } : {} }).then(r => r.data), resolve: (tripId: number | string, itemId: number, keep: 'trek' | 'provider' | 'both') => - apiClient.post(`/trips/${tripId}/docsync/items/${itemId}/resolve`, { keep }).then(r => r.data), + apiClient.post(`/trips/${tripId}/docsync/items/${itemId}/resolve`, { keep }, docsyncRun).then(r => r.data), } export const reservationsApi = { diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index 0964cedc0..1b86b373c 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -1,4 +1,4 @@ -// FE-ADMIN-ADDON-001 to FE-ADMIN-ADDON-036 +// FE-ADMIN-ADDON-001 to FE-ADMIN-ADDON-037 import { render, screen, waitFor } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { delay, http, HttpResponse } from 'msw'; @@ -543,6 +543,22 @@ describe('AddonManager', () => { expect(screen.queryByText('Polls')).not.toBeInTheDocument(); }); + it('FE-ADMIN-ADDON-037: the links row describes the feature, not an empty list', async () => { + // The row's tooltip is its description. The links tab's empty state read as a + // status here, and a status that never changes reads as a broken feature. + server.use(addonsRoute([buildAddon({ id: 'collab', name: 'Collab', enabled: true })])); + render( + , + ); + + const links = await screen.findByText('Links'); + expect(links).toHaveAttribute('title'); + expect(links.getAttribute('title')).not.toBe('No shared links yet'); + }); + it('FE-ADMIN-ADDON-020: a disabled AI-parsing addon renders the row without its config block', async () => { server.use(addonsRoute([{ ...llmAddon({ provider: 'local' }), enabled: false }])); render(); diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index 6002d42c7..94755412a 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -99,7 +99,7 @@ interface CollabFeatures { chat: boolean; notes: boolean; links?: boolean; polls const COLLAB_SUB_FEATURES = [ { key: 'chat', icon: MessageCircle, titleKey: 'admin.collab.chat.title', subtitleKey: 'admin.collab.chat.subtitle' }, { key: 'notes', icon: StickyNote, titleKey: 'admin.collab.notes.title', subtitleKey: 'admin.collab.notes.subtitle' }, - { key: 'links', icon: Link2, titleKey: 'collab.tabs.links', subtitleKey: 'collab.links.empty' }, + { key: 'links', icon: Link2, titleKey: 'collab.tabs.links', subtitleKey: 'admin.collab.links.subtitle' }, { key: 'polls', icon: BarChart3, titleKey: 'admin.collab.polls.title', subtitleKey: 'admin.collab.polls.subtitle' }, { key: 'whatsnext', icon: Sparkles, titleKey: 'admin.collab.whatsnext.title', subtitleKey: 'admin.collab.whatsnext.subtitle' }, ] as const diff --git a/client/src/components/Collab/CollabChat.test.tsx b/client/src/components/Collab/CollabChat.test.tsx index 2f818b142..2ed6657d6 100644 --- a/client/src/components/Collab/CollabChat.test.tsx +++ b/client/src/components/Collab/CollabChat.test.tsx @@ -708,4 +708,31 @@ describe('CollabChat', () => { { timeout: 3000 } ); }); + + // The strip shows the four pictures that will go out; the two the cap turned away + // vanish without a word otherwise, and the sender believes all six were sent. + it('FE-COMP-CHAT-037: picking more images than fit on one message says so, and says it once', async () => { + const addToast = vi.fn(); + window.__addToast = addToast as unknown as typeof window.__addToast; + let minted = 0; + const createObjURL = vi.spyOn(URL, 'createObjectURL').mockImplementation(() => `blob:mock/${++minted}`); + const revokeObjURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}); + try { + render(); + await screen.findByText('Start the conversation'); + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + const files = Array.from({ length: 6 }, (_, i) => new File([new Uint8Array(8)], `${i}.png`, { type: 'image/png' })); + + fireEvent.change(input, { target: { files } }); + + await waitFor(() => expect(document.querySelectorAll('img[src^="blob:mock/"]')).toHaveLength(4)); + expect(addToast).toHaveBeenCalledTimes(1); + const [message, type] = addToast.mock.calls[0]; + expect(type).toBe('error'); + expect(message).not.toBe('Only JPEG, PNG, GIF and WebP images up to 10 MB are allowed'); + } finally { + delete window.__addToast; + createObjURL.mockRestore(); revokeObjURL.mockRestore(); + } + }); }); diff --git a/client/src/components/Collab/CollabChat.tsx b/client/src/components/Collab/CollabChat.tsx index f3cc80526..a1badfd38 100644 --- a/client/src/components/Collab/CollabChat.tsx +++ b/client/src/components/Collab/CollabChat.tsx @@ -48,8 +48,8 @@ export default function CollabChat({ tripId, currentUser }: CollabChatProps) { )} - {imagePreviews.length > 0 &&
{imagePreviews.map((url, i) =>
)}
} - {uploadProgress > 0 && uploadProgress < 100 &&
Uploading {uploadProgress}%
} + {imagePreviews.length > 0 &&
{imagePreviews.map((url, i) =>
)}
} + {uploadProgress > 0 && uploadProgress < 100 &&
{t('collab.chat.uploading', { percent: uploadProgress })}
}
{/* Emoji button */} {canEdit && ( @@ -63,7 +63,7 @@ export default function CollabChat({ tripId, currentUser }: CollabChatProps) { )} - {canEdit && <> { if (e.target.files) addImageFiles(e.target.files); e.currentTarget.value = '' }} />} + {canEdit && <> { if (e.target.files) addImageFiles(e.target.files); e.currentTarget.value = '' }} />}