diff --git a/client/src/components/Journey/JourneyMapGL.tsx b/client/src/components/Journey/JourneyMapGL.tsx index bd0996381..f42d83066 100644 --- a/client/src/components/Journey/JourneyMapGL.tsx +++ b/client/src/components/Journey/JourneyMapGL.tsx @@ -501,9 +501,10 @@ function JourneyMapGL( // fit bounds to all points const known = marksRef.current ?? [] marksFramedRef.current = known.length > 0 + known.forEach(mark => bounds.extend([mark.lng, mark.lat])) if (hasPoints || known.length > 0) { try { - map.fitBounds(markBounds(gl, known, entryPointsRef.current), fitOptions(paddingBottom, enableMapbox3d && fullScreen)) + map.fitBounds(bounds, fitOptions(paddingBottom, enableMapbox3d && fullScreen)) } catch { /* empty bounds */ } } }) diff --git a/client/src/components/Journey/useJourneyDayImport.test.ts b/client/src/components/Journey/useJourneyDayImport.test.ts index c9d259ff4..6cc5e2e96 100644 --- a/client/src/components/Journey/useJourneyDayImport.test.ts +++ b/client/src/components/Journey/useJourneyDayImport.test.ts @@ -152,7 +152,7 @@ describe('useJourneyDayImport', () => { await act(async () => result.current.submit()); expect(toast.success).not.toHaveBeenCalled(); - expect(toast.info).toHaveBeenCalledWith('2 stays were in the journal already. Nothing was written twice.'); + expect(toast.info).toHaveBeenCalledWith('2 stays were taken over already. Nothing was written twice.'); expect(repo.save).not.toHaveBeenCalled(); }); diff --git a/client/src/components/Journey/useJourneyRecordingPrep.test.ts b/client/src/components/Journey/useJourneyRecordingPrep.test.ts index 927930130..ef0fe1824 100644 --- a/client/src/components/Journey/useJourneyRecordingPrep.test.ts +++ b/client/src/components/Journey/useJourneyRecordingPrep.test.ts @@ -103,7 +103,7 @@ describe('useJourneyRecordingPrep', () => { await act(async () => { await result.current.setEnabled(true); }); - expect(toast.error).toHaveBeenCalledWith('Failed to save'); + expect(toast.error).toHaveBeenCalledWith('journey.settings.saveFailed'); expect(opts.onSwitched).not.toHaveBeenCalled(); expect(dawarich.syncNow).not.toHaveBeenCalled(); }); @@ -124,7 +124,7 @@ describe('useJourneyRecordingPrep', () => { await act(async () => { await result.current.fetchNow(); }); - expect(toast.error).toHaveBeenCalledWith('Dawarich could not be asked right now.'); + expect(toast.error).toHaveBeenCalledWith('journey.prep.fetchFailed'); await waitFor(() => expect(repo.prep).toHaveBeenCalledTimes(3)); }); @@ -136,9 +136,7 @@ describe('useJourneyRecordingPrep', () => { await act(async () => { await result.current.fetchNow(); }); - expect(toast.info).toHaveBeenCalledWith( - 'A fetch is already running in the background. Its stays show up once it is through.' - ); + expect(toast.info).toHaveBeenCalledWith('journey.prep.alreadyRunning'); }); it('FE-JRN-PREP-HOOK-007: a journey whose trips all ended out of reach marks no day and offers no fetch', async () => { @@ -149,7 +147,7 @@ describe('useJourneyRecordingPrep', () => { expect(result.current.dayPrep('2026-05-14')).toBeNull(); expect(result.current.canFetch).toBe(false); expect(result.current.statusLine).toEqual({ - text: 'Trips that ended more than 400 days ago are not prepared in the background. Open one of their days to fetch it by hand.', + text: 'journey.prep.outOfReach', tone: 'warning', }); }); diff --git a/client/src/components/Tours/poi/tourPoiSearch.test.tsx b/client/src/components/Tours/poi/tourPoiSearch.test.tsx index 5037ba03d..27fc03afc 100644 --- a/client/src/components/Tours/poi/tourPoiSearch.test.tsx +++ b/client/src/components/Tours/poi/tourPoiSearch.test.tsx @@ -63,6 +63,7 @@ const searchPlanner = (over: Partial = {}) => tourType: 'hike', route: ROUTE, mode: { type: 'edit-saved', placeId: 9 }, + waypoints: [], ...over, }) as TourPlannerController; @@ -720,7 +721,9 @@ describe('e-bike charging along a bike tour (BIK-07)', () => { points: TourPlannerWaypoint[] ) => TourPlannerWaypoint[]; const added = normalizePlannerWaypoints(updater(POINTS)).find((point) => point.lat === CHARGER.lat); - expect(added).toMatchObject({ lng: CHARGER.lng, name: 'E-bike charging' }); + // Unnamed, it keeps no name of its own, as every unnamed place does; its row shows the kind. + expect(added).toMatchObject({ lng: CHARGER.lng, poiCategory: 'ebike_charging' }); + expect(added).not.toHaveProperty('name'); }); it('FE-TOUR-POI-029: a point that only lets bikes charge says so, and an unstated plug is dimmed, never named', async () => { diff --git a/server/src/nest/journey-recordings/journey-day-import.mcp.ts b/server/src/nest/journey-recordings/journey-day-import.mcp.ts index 7381be9f1..36f39365b 100644 --- a/server/src/nest/journey-recordings/journey-day-import.mcp.ts +++ b/server/src/nest/journey-recordings/journey-day-import.mcp.ts @@ -1,5 +1,12 @@ import { ADDON_IDS } from '../../addons'; -import { McpController, TOOL_ANNOTATIONS_WRITE, Tool, ok, type McpContext } from '../../nest-mcp'; +import { + McpController, + TOOL_ANNOTATIONS_READONLY, + TOOL_ANNOTATIONS_WRITE, + Tool, + ok, + type McpContext, +} from '../../nest-mcp'; import { addonGate, type HasAddons } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; import { JourneyDayImportService } from './journey-day-import.service'; @@ -33,9 +40,9 @@ export class JourneyDayImportMcp { description: "The stays Dawarich found on one local day, from the caller's own connection, as a journey would take them over: each with its suggestionId, name, place, arrival and departure, whether Dawarich only guessed it, and its status for this journey (open, skipped before, here with the entry holding it, or taken over elsewhere), plus the entries the journey has on the day. When Dawarich does not answer, the stays of the last sync are shown with fresh: false and the reason. Reading the day again refreshes the caller's own stored stays of it, nothing in the journey. Needs edit access to the journey.", inputSchema: { journeyId: idSchema, date: journeyRecordingDateSchema }, - // Reaches the caller's own Dawarich instance and refreshes their stored stays of the day; the journey - // scope's read access covers that, since only the caller's own stay list changes. - annotations: { ...TOOL_ANNOTATIONS_WRITE, openWorldHint: true }, + // Reaches the caller's own Dawarich instance and refreshes their stored stays of the day, like the + // recording preview; nothing of the journey changes, so it reads as a read. + annotations: { ...TOOL_ANNOTATIONS_READONLY, openWorldHint: true }, when: bothAddonsOn, access: { group: 'journey', mode: 'read' }, }) diff --git a/server/tests/e2e/journey-day-import.e2e.test.ts b/server/tests/e2e/journey-day-import.e2e.test.ts index 6fa264302..68d7d7eb6 100644 --- a/server/tests/e2e/journey-day-import.e2e.test.ts +++ b/server/tests/e2e/journey-day-import.e2e.test.ts @@ -20,6 +20,7 @@ import { JourneyEntryStays } from '../../src/db/entities/JourneyEntryStays.entit import { encrypt_api_key } from '../../src/nest/common/crypto/apiKeyCrypto'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; +import { withRequestContext } from '../../src/nest/database/request-context'; import { DawarichClient } from '../../src/nest/integrations/dawarich.client'; import { JourneyRecordingsModule } from '../../src/nest/journey-recordings/journey-recordings.module'; import { JourneyShareService } from '../../src/nest/journey/journey-share.service'; @@ -34,6 +35,7 @@ import { FakeRealtimeService } from '../helpers/fake-realtime'; import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; import { sessionCookie } from './harness'; +import { MikroORM } from '@mikro-orm/core'; import { Test } from '@nestjs/testing'; import cookieParser from 'cookie-parser'; @@ -263,13 +265,15 @@ describe('Journey day take-over e2e (real guards + temp SQLite)', () => { await makeJourneyEntry(orm, journeyId, OWNER, { title: 'Arrival', entry_date: DAY }); expect((await takeOver({ groups: [{ kind: 'single', stays: [{ suggestionId: first }] }] })).status).toBe(200); const share = app.get(JourneyShareService, { strict: false }); - const link = await share.createOrUpdateJourneyShareLink(journeyId, OWNER, { - share_timeline: true, - share_map: true, + // Called beside a request, so the services get the request context a route would give them. + const published = await withRequestContext(app.get(MikroORM), async () => { + const link = await share.createOrUpdateJourneyShareLink(journeyId, OWNER, { + share_timeline: true, + share_map: true, + }); + if (!link) throw new Error('no share link'); + return share.getPublicJourney(link.token); }); - if (!link) throw new Error('no share link'); - - const published = await share.getPublicJourney(link.token); expect(published?.entries.map((entry) => entry.title)).toEqual(['Arrival']); expect(published?.stats.entries).toBe(1); diff --git a/server/tests/e2e/journey-recordings.e2e.test.ts b/server/tests/e2e/journey-recordings.e2e.test.ts index e1e4367b4..c581c2741 100644 --- a/server/tests/e2e/journey-recordings.e2e.test.ts +++ b/server/tests/e2e/journey-recordings.e2e.test.ts @@ -19,6 +19,7 @@ import { Journeys } from '../../src/db/entities/Journeys.entity'; import { encrypt_api_key } from '../../src/nest/common/crypto/apiKeyCrypto'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; +import { withRequestContext } from '../../src/nest/database/request-context'; import { DawarichSyncService } from '../../src/nest/integrations/dawarich-sync.service'; import { DawarichClient, DawarichError } from '../../src/nest/integrations/dawarich.client'; import { JourneyRecordingsModule } from '../../src/nest/journey-recordings/journey-recordings.module'; @@ -34,6 +35,7 @@ import { FakeRealtimeService } from '../helpers/fake-realtime'; import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; import { sessionCookie } from './harness'; +import { MikroORM } from '@mikro-orm/core'; import { Test } from '@nestjs/testing'; import cookieParser from 'cookie-parser'; @@ -313,7 +315,8 @@ describe('Journey recordings e2e (real guards + temp SQLite)', () => { await linkTripToJourney(orm, journeyId, trip.id); await updateRows(orm, Journeys, { id: journeyId }, { auto_prepare_recordings: 1 }); client.listVisits.mockResolvedValue({ visits: [], truncated: false, version: null }); - await app.get(DawarichSyncService).syncUser(OWNER); + // The cron runs the sync in a request context of its own; called here, the test gives it one. + await withRequestContext(app.get(MikroORM), () => app.get(DawarichSyncService).syncUser(OWNER)); const on = await request(server).get(`${base()}/prep`).set(as(OWNER)); expect(on.body).toMatchObject({ enabled: true, connection: 'ready', lastError: null }); diff --git a/server/tests/unit/nest/city-walk.mcp.test.ts b/server/tests/unit/nest/city-walk.mcp.test.ts index 1dca1b426..c99ec9604 100644 --- a/server/tests/unit/nest/city-walk.mcp.test.ts +++ b/server/tests/unit/nest/city-walk.mcp.test.ts @@ -440,7 +440,7 @@ describe('CityWalkMcp (CW-11)', () => { { lat: 48.009, lng: 11 }, { lat: 48.018, lng: 11 }, ]; - expect(route).toHaveBeenCalledWith(f.user.id, f.trip.id, 0, points, 'walking', []); + expect(route).toHaveBeenCalledWith(f.user.id, f.trip.id, 0, points, 'walking', [], { footOnly: true }); expect(proposal.estimate).toEqual({ kind: 'routed', profile: 'walking' }); expect( proposal.stations.map((s: { name: string; arrival: string; walk_from_previous: unknown }) => [ @@ -461,7 +461,7 @@ describe('CityWalkMcp (CW-11)', () => { ctx(f.user.id), ), ); - expect(route).toHaveBeenLastCalledWith(f.user.id, f.trip.id, 0, points, 'walking', []); + expect(route).toHaveBeenLastCalledWith(f.user.id, f.trip.id, 0, points, 'walking', [], { footOnly: true }); expect(created.foot_way).toEqual({ planned: true }); expect(created.tour).toMatchObject({ route_pending: false, duration: 27, elevation_gain: null, station_count: 3 }); expect(created.tour.distance).toBeCloseTo(2.0, 1); diff --git a/server/tests/unit/nest/dawarich-tracks.service.test.ts b/server/tests/unit/nest/dawarich-tracks.service.test.ts index afc69530d..8a11f76d6 100644 --- a/server/tests/unit/nest/dawarich-tracks.service.test.ts +++ b/server/tests/unit/nest/dawarich-tracks.service.test.ts @@ -884,12 +884,12 @@ describe('DawarichTracksService.fetchDay', () => { expect(day.segments).toHaveLength(1); }); - it('DAWARICH-TRACKS-063: the source zone is the one of the first track on the date, not of the evening before in another zone', async () => { + it('DAWARICH-TRACKS-063: the source zone is the one of the first track on the date, not of the afternoon before in another zone', async () => { const h = harness({ capabilities: CAPABILITIES }); h.listTracks.mockResolvedValue({ features: [ - // The evening before in Berlin, then the morning after landing in Tokyo. - feature({ start_at: '2026-04-30T20:00:00+02:00', end_at: '2026-04-30T21:00:00+02:00' }), + // The afternoon before in Berlin, then the morning after landing in Tokyo. + feature({ start_at: '2026-04-30T15:00:00+02:00', end_at: '2026-04-30T16:00:00+02:00' }), feature({ start_at: '2026-05-01T09:00:00+09:00', end_at: '2026-05-01T10:00:00+09:00' }), ], truncated: false, diff --git a/server/tests/unit/nest/tour-change-save.mcp.test.ts b/server/tests/unit/nest/tour-change-save.mcp.test.ts index b692d6b3c..385e54fdc 100644 --- a/server/tests/unit/nest/tour-change-save.mcp.test.ts +++ b/server/tests/unit/nest/tour-change-save.mcp.test.ts @@ -204,6 +204,7 @@ describe('TourChangeMcp.saveTourChange (MCP-T03)', () => { after.map(([lat, lng]) => ({ lat, lng })), 'walking', [], + { footOnly: true }, ); // Saved: the line, the points with the kept keys and the link, the measures, the visits as they were. diff --git a/server/tests/unit/nest/tour-change.mcp.test.ts b/server/tests/unit/nest/tour-change.mcp.test.ts index 35c73a0ed..c45b0629a 100644 --- a/server/tests/unit/nest/tour-change.mcp.test.ts +++ b/server/tests/unit/nest/tour-change.mcp.test.ts @@ -163,7 +163,7 @@ describe('TourChangeMcp (MCP-T02)', () => { [48.108, 11.5], [48.112, 11.5], ]; - expect(route).toHaveBeenCalledWith(f.user.id, f.trip.id, 0, asked(saved), 'walking', []); + expect(route).toHaveBeenCalledWith(f.user.id, f.trip.id, 0, asked(saved), 'walking', [], { footOnly: true }); expect(route).toHaveBeenCalledWith( f.user.id, f.trip.id, @@ -171,6 +171,7 @@ describe('TourChangeMcp (MCP-T02)', () => { asked([...saved.slice(0, 3), [48.109, 11.501], [48.113, 11.502]]), 'walking', [], + { footOnly: true }, ); expect(proposal.effects.walking).toEqual({ planned_by: 'router_on_foot',