From 775b35d5afea9903a34073777d1b65017b1900e4 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 14 Oct 2026 10:45:24 +0200 Subject: [PATCH] fix(dashboard): leave planned public transit out of upcoming reservations Journeys from the transit search are routing, not bookings, and a day of them pushed the real bookings out of the six slots. Booked trains and buses keep their own types and stay. The MCP tool reads the same feed. --- .../src/db/repositories/Reservations.repository.ts | 2 +- server/src/nest/reservations/reservations.mcp.ts | 2 +- .../src/nest/reservations/reservations.service.ts | 2 +- server/tests/e2e/reservations.e2e.test.ts | 11 +++++++++++ .../db/repositories/Reservations.repository.test.ts | 8 ++++++++ server/tests/unit/mcp/tools-reservations.test.ts | 13 +++++++++++++ wiki/Dashboard-Widgets.md | 2 +- wiki/MCP-Tools-and-Resources.md | 2 +- 8 files changed, 37 insertions(+), 5 deletions(-) diff --git a/server/src/db/repositories/Reservations.repository.ts b/server/src/db/repositories/Reservations.repository.ts index 2d42a0b3b..dd7c086d1 100644 --- a/server/src/db/repositories/Reservations.repository.ts +++ b/server/src/db/repositories/Reservations.repository.ts @@ -1084,7 +1084,7 @@ export class ReservationsRepository extends TrekRepository { .as('at_time'), ]) .where('r.status', '!=', 'cancelled') - .where((eb) => eb(eb.fn.coalesce(eb.ref('r.type'), eb.val('')), '!=', 'hotel')) + .where((eb) => eb(eb.fn.coalesce(eb.ref('r.type'), eb.val('')), 'not in', ['hotel', 'transit'])) .$castTo(); const checkins = qb diff --git a/server/src/nest/reservations/reservations.mcp.ts b/server/src/nest/reservations/reservations.mcp.ts index 3a3f60861..577186ee6 100644 --- a/server/src/nest/reservations/reservations.mcp.ts +++ b/server/src/nest/reservations/reservations.mcp.ts @@ -607,7 +607,7 @@ export class ReservationsMcp { @Tool({ name: 'list_upcoming_reservations', - description: 'The next bookings across ALL of the user\'s trips, soonest first: "what is coming up?", "when is my next flight?". Prefer this over get_trip_summary when no particular trip is in question, or when the trip is unknown. Hotel stays appear as their check-in and check-out moments (the stay itself covers a range and is not a point in time), which no per-trip reservation list reports. Cancelled bookings and archived trips are left out.', + description: 'The next bookings across ALL of the user\'s trips, soonest first: "what is coming up?", "when is my next flight?". Prefer this over get_trip_summary when no particular trip is in question, or when the trip is unknown. Hotel stays appear as their check-in and check-out moments (the stay itself covers a range and is not a point in time), which no per-trip reservation list reports. Cancelled bookings, planned public-transit journeys and archived trips are left out.', inputSchema: { limit: z.number().int().min(1).max(50).optional().describe('How many entries to return, soonest first (default 6, the dashboard widget\'s size)'), }, diff --git a/server/src/nest/reservations/reservations.service.ts b/server/src/nest/reservations/reservations.service.ts index e2ab8cabe..45aad9994 100644 --- a/server/src/nest/reservations/reservations.service.ts +++ b/server/src/nest/reservations/reservations.service.ts @@ -423,7 +423,7 @@ export class ReservationsService { * panel's auto-created booking, stamps the start day into reservation_time * once and never restamps it, so moving the stay left the widget showing the * old date. Both halves of the report come from reading a hotel's date off - * fields hotels do not use. + * fields hotels do not use. Planned transit journeys (type 'transit') are routing, not bookings. * * RS20 — Task 4 (calendar + listUpcoming + the visibility predicate * consumers). Ruling (ii): ONE fully typed Kysely statement, in diff --git a/server/tests/e2e/reservations.e2e.test.ts b/server/tests/e2e/reservations.e2e.test.ts index b030e5484..63ac3e665 100644 --- a/server/tests/e2e/reservations.e2e.test.ts +++ b/server/tests/e2e/reservations.e2e.test.ts @@ -173,6 +173,17 @@ describe('Reservations + accommodations e2e (real auth guard + temp SQLite, real expect(titles).not.toContain('Stay'); }); + it('200 upcoming feed leaves planned public transit out and keeps booked trains and buses', async () => { + await seedReservation(tripId, 'Metro leg', 'transit', { reservation_time: '2998-12-30T08:00:00' }); + await seedReservation(tripId, 'Booked train', 'train', { reservation_time: '2998-12-30T09:00:00' }); + await seedReservation(tripId, 'Booked bus', 'bus', { reservation_time: '2998-12-30T10:00:00' }); + const res = await request(server).get('/api/reservations/upcoming').set('Cookie', sessionCookie(1)); + expect(res.status).toBe(200); + const titles = res.body.reservations.map((r: { title: string }) => r.title); + expect(titles).toEqual(expect.arrayContaining(['Booked train', 'Booked bus'])); + expect(titles).not.toContain('Metro leg'); + }); + it('404 when trip not accessible (reservations)', async () => { // Plan 3c Task 0b: TripAccessGuard reads TripsRepository.findAccessible // directly now, a real query — `canAccessTrip.mockReturnValue(...)` no diff --git a/server/tests/unit/db/repositories/Reservations.repository.test.ts b/server/tests/unit/db/repositories/Reservations.repository.test.ts index caac642ad..e205a052b 100644 --- a/server/tests/unit/db/repositories/Reservations.repository.test.ts +++ b/server/tests/unit/db/repositories/Reservations.repository.test.ts @@ -312,6 +312,7 @@ const LEGACY_LIST_UPCOMING = ` LEFT JOIN places p ON r.place_id = p.id WHERE r.status != 'cancelled' AND COALESCE(r.type, '') != 'hotel' + AND COALESCE(r.type, '') != 'transit' UNION ALL @@ -397,6 +398,11 @@ describe('ReservationsRepository — RS20 (listUpcomingForUser)', () => { await setReservation(cancelled.id, { reservation_time: '2999-01-03T10:00:00', status: 'cancelled' }); const hotelDated = createReservation(testDb, tripOwned.id, { title: 'Hotel dated', type: 'hotel' }); await setReservation(hotelDated.id, { reservation_time: '2999-01-03T10:00:00' }); + // A planned public-transit journey is routing, not a booking; a booked train is one. + const transit = createReservation(testDb, tripOwned.id, { title: 'Metro A to B', type: 'transit' }); + await setReservation(transit.id, { reservation_time: '2999-01-03T11:00:00', status: 'confirmed' }); + const train = createReservation(testDb, tripOwned.id, { title: 'Booked train', type: 'train' }); + await setReservation(train.id, { reservation_time: '2999-01-03T12:00:00', status: 'confirmed' }); // A NULL-typed booking still shows. const untyped = createReservation(testDb, tripOwned.id, { title: 'Untyped' }); @@ -456,6 +462,8 @@ describe('ReservationsRepository — RS20 (listUpcomingForUser)', () => { // linked ".0" booking. const baseline = await reservationsRepo.listUpcomingForUser(owner.id, '2000-01-01', '2000-01-01T00:00', 20); expect(baseline.map((r) => r.title)).not.toContain('Hidden'); + expect(baseline.map((r) => r.title)).not.toContain('Metro A to B'); + expect(baseline.map((r) => r.title)).toContain('Booked train'); const tieIndex = baseline.findIndex((r) => r.title === 'Tie 1'); expect(baseline[tieIndex + 1]?.title).toBe('Tie 2'); expect(baseline.some((r) => r.title === 'Hotel Ibis')).toBe(true); diff --git a/server/tests/unit/mcp/tools-reservations.test.ts b/server/tests/unit/mcp/tools-reservations.test.ts index 316a3df0b..db09d3120 100644 --- a/server/tests/unit/mcp/tools-reservations.test.ts +++ b/server/tests/unit/mcp/tools-reservations.test.ts @@ -1067,6 +1067,19 @@ describe('Tool: list_upcoming_reservations', () => { expect(rows.map((r) => r.title)).toEqual(['Still on']); }); + it('leaves out planned public-transit journeys and keeps a booked train, as the dashboard does', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + await seedBooking(trip.id, 'Metro to the museum', `${dateInDays(1)}T09:00:00`, { + type: 'transit', + status: 'confirmed', + }); + await seedBooking(trip.id, 'Night train', `${dateInDays(2)}T21:00:00`, { type: 'train', status: 'confirmed' }); + + const rows = await upcoming(user.id); + expect(rows.map((r) => r.title)).toEqual(['Night train']); + }); + it('returns the dashboard six by default and honours a limit', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); diff --git a/wiki/Dashboard-Widgets.md b/wiki/Dashboard-Widgets.md index f8c9c772e..a46fb2d0a 100644 --- a/wiki/Dashboard-Widgets.md +++ b/wiki/Dashboard-Widgets.md @@ -58,7 +58,7 @@ Clocks refresh every 30 seconds and are always shown in 24-hour time — the 12- ## Upcoming reservations -**Upcoming reservations** lists the next six bookings across all your trips that are not archived, soonest first. A booking counts when its time is still ahead, or, without a time, when its day is today or later; cancelled bookings are left out. A hotel stay is not listed as a whole: its **Check-in** and **Check-out** appear as two entries of their own. Each row shows the date, the time in your 12 or 24 hour format, the title with a **Pending** tag while the booking is not confirmed, and the booking's type as an icon. Click a row to open its trip. With nothing ahead the widget reads *Nothing booked yet.* +**Upcoming reservations** lists the next six bookings across all your trips that are not archived, soonest first. A booking counts when its time is still ahead, or, without a time, when its day is today or later; cancelled bookings are left out. A hotel stay is not listed as a whole: its **Check-in** and **Check-out** appear as two entries of their own. Public-transit connections planned with the transit search are not listed either; a train or bus you booked is. Each row shows the date, the time in your 12 or 24 hour format, the title with a **Pending** tag while the booking is not confirmed, and the booking's type as an icon. Click a row to open its trip. With nothing ahead the widget reads *Nothing booked yet.* --- diff --git a/wiki/MCP-Tools-and-Resources.md b/wiki/MCP-Tools-and-Resources.md index b0feb1dbd..e2046de56 100644 --- a/wiki/MCP-Tools-and-Resources.md +++ b/wiki/MCP-Tools-and-Resources.md @@ -147,7 +147,7 @@ Requires `reservations:read` or `reservations:write` scope. | `reorder_reservations` | Reorder reservations within a day. | | `set_reservation_travelers` | Set who is travelling on a booking, from the trip roster (members and guests). Replaces the list; an empty array clears it. Ids that are not on the trip come back under `ignored_user_ids` rather than being attached. | | `link_hotel_accommodation` | Set or update a hotel reservation's check-in/out day links and place. | -| `list_upcoming_reservations` | The next bookings across all of the user's trips, soonest first, for "what is coming up?" when no particular trip is in question. Hotel stays appear as their check-in and check-out moments. Cancelled bookings and archived trips are left out. `limit` defaults to 6, at most 50. Requires `reservations:read`. | +| `list_upcoming_reservations` | The next bookings across all of the user's trips, soonest first, for "what is coming up?" when no particular trip is in question. Hotel stays appear as their check-in and check-out moments. Cancelled bookings, planned public-transit journeys and archived trips are left out. `limit` defaults to 6, at most 50. Requires `reservations:read`. | ### Budget