mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(roadtrip): a day after a booked night starts at the stay, a day before one ends there
A trip can now switch on roadtrip_hotel_bookends, a shared driving preference that is off until somebody sets it, so every trip drives exactly as before. Switched on, the planner in the browser and calculate_roadtrip on the server seat the hotel slept in at the start of the next day and tonight's hotel at the end of the day, both through seatNightBookends over the trip's stays in id order, with the earliest booking linked to each. A linked booking changes the hotel row and asks the router nothing. The switch sits in the desktop driving settings under the route line, and the phone's driving figures carry the same switch as the one control they offer; both write through the planner's own preference save, so a flip offline queues like any other. get_roadtrip_context reports the stays, calculate_roadtrip previews the switch through its settings, and the settings, corridor and day boundary tools say what it does and that missing means off.
This commit is contained in:
@@ -356,8 +356,10 @@ export class AccommodationsService {
|
||||
*
|
||||
* Only the check-in day gets a stop, even for a fortnight's stay: that is the
|
||||
* day you drive there, and it is exactly what the road-trip side writes for a
|
||||
* night it books itself. The later nights ride on the stay row, which is where
|
||||
* the rail reads check-out from.
|
||||
* night it books itself. The later nights ride on the stay row: a trip that
|
||||
* starts and ends its days at the stay seats the hotel at the edges of the days
|
||||
* around them from it (`seatNightBookends`), and reads the check-out there as a
|
||||
* label only.
|
||||
*
|
||||
* Runs inside the caller's transaction.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ export class DayBoundariesMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'set_day_boundary',
|
||||
description: 'Override a road trip day ending at a visit or a fraction along the driving leg between consecutive visits. Daily travel times must be enabled in the planner. Fixed visit times stay protected and conflicts are shown. Pass null to restore the automatic day ending.',
|
||||
description: 'Override a road trip day ending at a visit or a fraction along the driving leg between consecutive visits. Daily travel times must be enabled in the planner. Fixed visit times stay protected and conflicts are shown. With roadtrip_hotel_bookends on, a boundary between two stops a booked night separates is ignored: the night ends the day. Pass null to restore the automatic day ending.',
|
||||
inputSchema: { tripId: z.number().int().positive(), dayNumber: z.number().int().min(1).max(MAX_TRIP_DAYS), boundary: roadtripDayBoundarySchema.nullable() },
|
||||
annotations: TOOL_ANNOTATIONS_WRITE, access: { group: 'trips', mode: 'write' }, when,
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
carrierLegsFor,
|
||||
carrierSeam,
|
||||
foldRouteRun,
|
||||
hotelBookendsOn,
|
||||
isStationaryJoin,
|
||||
splitIntoRuns,
|
||||
spillChains,
|
||||
@@ -20,8 +21,10 @@ import {
|
||||
formatDistance,
|
||||
formatDurationShort,
|
||||
seatCarrierStops,
|
||||
seatNightBookends,
|
||||
undatedRides,
|
||||
viasOnLeg,
|
||||
type BookendStay,
|
||||
type CarrierBooking,
|
||||
type CarrierSeam,
|
||||
type DistanceUnit,
|
||||
@@ -79,6 +82,13 @@ interface VisitRow {
|
||||
stop_type: string | null;
|
||||
fill_percent: number | null;
|
||||
}
|
||||
/**
|
||||
* A booked night as the road trip reads it: the stay, the days it spans, where it is and
|
||||
* the earliest booking linked to it. The rule reads these for the hotel at the edges of
|
||||
* the days around the night (`seatNightBookends`); get_roadtrip_context reports them as
|
||||
* they are, with the check-in beside the check-out.
|
||||
*/
|
||||
type StayRow = BookendStay & { check_in: string | null };
|
||||
const stopKey = (s: RoadtripStop) =>
|
||||
`${s.lat.toFixed(5)},${s.lng.toFixed(5)},${s.legMode ?? ''},${s.incomingLegMode ?? ''}`;
|
||||
|
||||
@@ -110,9 +120,21 @@ export class RoadtripPlanService {
|
||||
WHERE d.trip_id = ? ORDER BY d.day_number, a.order_index, a.created_at`,
|
||||
tripId,
|
||||
);
|
||||
// Every stay of the trip in id order, the order the rule reads them in, so a browser
|
||||
// holding them in another order still picks the same hotel where two overlap. The
|
||||
// booking is the earliest linked one, the one the planner opens from the hotel's row.
|
||||
const stays = this.db.all<StayRow>(
|
||||
`SELECT a.id, a.place_id, a.start_day_id, a.end_day_id, a.check_in, a.check_out,
|
||||
p.name AS place_name, p.lat AS place_lat, p.lng AS place_lng,
|
||||
(SELECT MIN(r.id) FROM reservations r WHERE r.accommodation_id = a.id) AS reservation_id
|
||||
FROM day_accommodations a LEFT JOIN places p ON p.id = a.place_id
|
||||
WHERE a.trip_id = ? ORDER BY a.id`,
|
||||
tripId,
|
||||
);
|
||||
return {
|
||||
days,
|
||||
visits,
|
||||
stays,
|
||||
carriers: this.carriers(tripId),
|
||||
settings: this.preferences.read(tripId),
|
||||
profiles: this.router.profiles(),
|
||||
@@ -200,7 +222,7 @@ export class RoadtripPlanService {
|
||||
throw new HttpException({ error: 'Day end must be later than day start.' }, 400);
|
||||
const seams = context.carriers.map((booking) => carrierSeam(booking)).filter((seam): seam is CarrierSeam => seam !== null);
|
||||
const dayNumberOf = (dayId: number): number => context.days.find((d) => d.id === dayId)?.day_number ?? 0;
|
||||
const plan: PlanDay[] = context.days.map((day) => {
|
||||
const stored: PlanDay[] = context.days.map((day) => {
|
||||
const visits = context.visits.filter((v) => v.day_id === day.id && v.lat !== null && v.lng !== null);
|
||||
const stops = visits.map(
|
||||
(v, index): RoadtripStop => ({
|
||||
@@ -239,6 +261,10 @@ export class RoadtripPlanService {
|
||||
),
|
||||
};
|
||||
});
|
||||
// A day after a booked night starts at the stay and a day before one ends there, by
|
||||
// the rule the planner runs in the browser, and only while the trip (or the preview's
|
||||
// own settings) has it switched on. Everything below reads the seated days.
|
||||
const plan = hotelBookendsOn(preferences) ? seatNightBookends(stored, context.days, context.stays) : stored;
|
||||
const allLegs: Record<string, RoutedLeg> = {};
|
||||
const snapByDay: Record<number, Record<string, SnappedWaypoint>> = {};
|
||||
const missedByDay: Record<number, RouteAvoidClass[]> = {};
|
||||
|
||||
@@ -36,7 +36,7 @@ export class RoadtripPlanningMcp {
|
||||
@Tool({
|
||||
name: 'get_roadtrip_context',
|
||||
description:
|
||||
'Read the saved roadtrip days, visits, pinned times, end times (when the drive leaves a visit), stays, stop types, fill levels, travel modes, via points, followed tracks, manual day endings and the carrier bookings (flight, train, ferry, cruise, bus) that seam the drive at their terminals, plus the hire cars whose pick-up and return desks stand on it. Includes the shared driving preferences for this trip. No routing request and no browser needed. Coordinates missing from a visit prevent it from being routed. Use calculate_roadtrip to get the derived day layout. Edit visits with the existing place and assignment tools; change their saved order with reorder_day_assignments. Settings, visits and manual boundaries all belong to the shared trip.',
|
||||
'Read the saved roadtrip days, visits, pinned times, end times (when the drive leaves a visit), stays, stop types, fill levels, travel modes, via points, followed tracks, manual day endings and the carrier bookings (flight, train, ferry, cruise, bus) that seam the drive at their terminals, plus the hire cars whose pick-up and return desks stand on it. stays lists every booked stay with its check-in and check-out day, its place and the earliest linked reservation (reservation_id). Includes the shared driving preferences for this trip. No routing request and no browser needed. Coordinates missing from a visit prevent it from being routed. Use calculate_roadtrip to get the derived day layout. Edit visits with the existing place and assignment tools; change their saved order with reorder_day_assignments. Settings, visits and manual boundaries all belong to the shared trip.',
|
||||
inputSchema: { tripId: z.number().int().positive() },
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
access: { group: 'trips', mode: 'read' },
|
||||
@@ -49,7 +49,7 @@ export class RoadtripPlanningMcp {
|
||||
@Tool({
|
||||
name: 'calculate_roadtrip',
|
||||
description:
|
||||
'Calculate a saved trip without an open browser, using the same daily scheduling and vehicle-range rules as the planner. Returns derived days, arrivals, departures, driving limits, fuel warnings, automatic pauses and routing failures. A flight, train, ferry, cruise or bus booking with located terminals is a seam: its terminals appear as stops carrying `carrier` (role departure or arrival), the road ends at the departure terminal and resumes at the arrival one, and the leg between them is the ride, in the mode of the booking type, with its timetable minutes and no distance. A hire car booking with a located pick-up desk puts that desk on the road as a stop carrying `carrier` with role pickup, and its return desk with role return when the booking names a return day and place; the road runs through both. A flight, train, ferry, cruise or bus booking that is on none of the days of the trip (saved without a date, or on a day the trip no longer has) is not on the drive at all, although its terminals are located: undatedRides lists each one (id, type, title) so it can be given a date within the trip with the reservation tools, after which the road ends at its terminal. Fixed visit times retain priority. The end time of a visit is when the drive leaves it, in place of its stay; reaching the visit after it is reported as a missedLeave warning. No accommodation or place is created. Optional settings are a read-only preview, not saved. Distances are metres, durations seconds, dwell times minutes. A failed leg or conflicting window means the output is incomplete: never present its totals as a complete route. Up to 150 legs; requests are paced for public routing servers and can take time. Existing place/assignment tools edit stops, stays and ordering, update_roadtrip_settings saves preferences, set_assignment_end_day and set_day_boundary set exceptions. Recalculate after changes. Plugin profiles follow installed route-provider permissions.',
|
||||
'Calculate a saved trip without an open browser, using the same daily scheduling and vehicle-range rules as the planner. Returns derived days, arrivals, departures, driving limits, fuel warnings, automatic pauses and routing failures. A flight, train, ferry, cruise or bus booking with located terminals is a seam: its terminals appear as stops carrying `carrier` (role departure or arrival), the road ends at the departure terminal and resumes at the arrival one, and the leg between them is the ride, in the mode of the booking type, with its timetable minutes and no distance. A hire car booking with a located pick-up desk puts that desk on the road as a stop carrying `carrier` with role pickup, and its return desk with role return when the booking names a return day and place; the road runs through both. A flight, train, ferry, cruise or bus booking that is on none of the days of the trip (saved without a date, or on a day the trip no longer has) is not on the drive at all, although its terminals are located: undatedRides lists each one (id, type, title) so it can be given a date within the trip with the reservation tools, after which the road ends at its terminal. With roadtrip_hotel_bookends switched on (missing means off), a day after a booked night starts at that stay and a day before one ends there. Such stops carry `bookend` {phase, accommodationId, reservationId, checkingOut, checkingIn, checkOut}, stopType hotel, the real placeId of the stay and a synthetic negative assignmentId that belongs to no assignment: they cannot be reordered, edited or given via points, and `failures` may name them. The check-out is a label, never a departure time. A day between two nights in the same stay with no visit has no drive. Pass settings {roadtrip_hotel_bookends:true} to preview the drive from and to the stays without saving it. Fixed visit times retain priority. The end time of a visit is when the drive leaves it, in place of its stay; reaching the visit after it is reported as a missedLeave warning. No accommodation or place is created. Optional settings are a read-only preview, not saved. Distances are metres, durations seconds, dwell times minutes. A failed leg or conflicting window means the output is incomplete: never present its totals as a complete route. Up to 150 legs; requests are paced for public routing servers and can take time. Existing place/assignment tools edit stops, stays and ordering, update_roadtrip_settings saves preferences, set_assignment_end_day and set_day_boundary set exceptions. Recalculate after changes. Plugin profiles follow installed route-provider permissions.',
|
||||
inputSchema: roadtripPlanRequestSchema.shape,
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
access: { group: 'trips', mode: 'read' },
|
||||
@@ -93,7 +93,7 @@ export class RoadtripPlanningMcp {
|
||||
@Tool({
|
||||
name: 'search_roadtrip_corridor',
|
||||
description:
|
||||
'Search for fuel, charging, rest areas, campsites, food, sights or accommodation along a calculated roadtrip day, without a browser. Uses the routed road, not a straight line, and leaves out the ride between the two terminals of a flight, train, ferry, cruise or bus booking: no rectangle is searched under it and nothing found there is a hit. Returns distance along the route and distance from it. Search rectangles are paged in batches of six; continue with nextOffset until null for full coverage. Optional name, socket family and minimum power filter the matches; missing charging details mean unknown. Does not add stops. Add a chosen place and assign it to the appropriate stored day, preserving any via anchors. Routing failures refuse the corridor search rather than searching an invented line.',
|
||||
'Search for fuel, charging, rest areas, campsites, food, sights or accommodation along a calculated roadtrip day, without a browser. Uses the routed road, not a straight line, and leaves out the ride between the two terminals of a flight, train, ferry, cruise or bus booking: no rectangle is searched under it and nothing found there is a hit. Returns distance along the route and distance from it. Search rectangles are paged in batches of six; continue with nextOffset until null for full coverage. Optional name, socket family and minimum power filter the matches; missing charging details mean unknown. Does not add stops. Add a chosen place and assign it to the appropriate stored day, preserving any via anchors. With roadtrip_hotel_bookends on, the road of the day includes the drive from the stay slept in and to the stay of that night; a place found there belongs at the start or the end of the stored day. Routing failures refuse the corridor search rather than searching an invented line.',
|
||||
inputSchema: roadtripCorridorRequestSchema.shape,
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
access: { group: 'trips', mode: 'read' },
|
||||
|
||||
@@ -35,7 +35,7 @@ export class RoadtripPreferencesMcp {
|
||||
@Tool({
|
||||
name: 'get_roadtrip_settings',
|
||||
description:
|
||||
'Read the shared driving preferences for one trip, including daily start and end times, day-ending behavior, vehicle specifications, range, fill percentage, driving limits, avoided road classes and route display options. Every member of this trip sees the same preferences. Missing numbers mean no limit, missing daily times disable automatic scheduling, missing vehicle means unspecified, and missing end mode means route. Range and consumption always use kilometres, litres and kWh. Instance URLs and credentials are not exposed.',
|
||||
'Read the shared driving preferences for one trip, including daily start and end times, day-ending behavior, vehicle specifications, range, fill percentage, driving limits, avoided road classes and route display options. Every member of this trip sees the same preferences. Missing numbers mean no limit, missing daily times disable automatic scheduling, missing vehicle means unspecified, and missing end mode means route. Missing roadtrip_hotel_bookends means off. Range and consumption always use kilometres, litres and kWh. Instance URLs and credentials are not exposed.',
|
||||
inputSchema: { tripId: z.number().int().positive() },
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
access: { group: 'trips', mode: 'read' },
|
||||
@@ -49,7 +49,7 @@ export class RoadtripPreferencesMcp {
|
||||
@Tool({
|
||||
name: 'update_roadtrip_settings',
|
||||
description:
|
||||
'Change the shared driving preferences for this trip. Changes apply to all its travellers and require permission to edit days. Read get_roadtrip_settings first for relative changes. Daily times use HH:mm; clear either with an empty string to disable automatic daily scheduling. Fixed visit times retain priority. Numeric zero clears a limit. Vehicle specifications take precedence over manual range when complete; clear the relevant specifications to use a manual range. Avoid classes are a comma-separated selection of toll,motorway,ferry. Unspecified fields remain unchanged. End mode route pauses at the cutoff; stop ends at the last reachable visit. Per-visit end_day and dragged boundaries are edited with set_assignment_end_day and set_day_boundary. This never changes instance routing URLs or credentials.',
|
||||
'Change the shared driving preferences for this trip. Changes apply to all its travellers and require permission to edit days. Read get_roadtrip_settings first for relative changes. Daily times use HH:mm; clear either with an empty string to disable automatic daily scheduling. Fixed visit times retain priority. Numeric zero clears a limit. Vehicle specifications take precedence over manual range when complete; clear the relevant specifications to use a manual range. Avoid classes are a comma-separated selection of toll,motorway,ferry. Unspecified fields remain unchanged. End mode route pauses at the cutoff; stop ends at the last reachable visit. Set roadtrip_hotel_bookends true to start and end each day at the stay: a day after a booked night starts where it was slept, a day before one ends there. False, like missing, drives each day from its first stored stop, as before. Per-visit end_day and dragged boundaries are edited with set_assignment_end_day and set_day_boundary. This never changes instance routing URLs or credentials.',
|
||||
inputSchema: { tripId: z.number().int().positive(), settings: roadtripPreferencesUpdateSchema },
|
||||
annotations: TOOL_ANNOTATIONS_WRITE,
|
||||
access: { group: 'trips', mode: 'write' },
|
||||
|
||||
Reference in New Issue
Block a user