diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 806ee6047..743bd0263 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -61,6 +61,7 @@ import { type PluginActionDescriptor, type PluginActionResult, type PluginInstallRequest, + RoadtripDayTrack, RoadtripVia, RoadtripViaBatchRequest, RoadtripViaCreateRequest, @@ -1225,7 +1226,7 @@ export const mapsApi = { export const roadtripApi = { /** Every via of the trip, so all days can be routed without a request per day. */ listVias: (tripId: number | string) => - apiClient.get(`/trips/${tripId}/roadtrip/vias`).then(r => r.data as { vias: RoadtripVia[] }), + apiClient.get(`/trips/${tripId}/roadtrip/vias`).then(r => r.data as { vias: RoadtripVia[]; tracks: RoadtripDayTrack[] }), addVia: (tripId: number | string, dayId: number | string, body: RoadtripViaCreateRequest) => apiClient.post(`/trips/${tripId}/roadtrip/days/${dayId}/vias`, body).then(r => r.data as { via: RoadtripVia }), /** diff --git a/client/src/components/Map/RouteCalculator.ts b/client/src/components/Map/RouteCalculator.ts index 85cbfb540..788b20e5a 100644 --- a/client/src/components/Map/RouteCalculator.ts +++ b/client/src/components/Map/RouteCalculator.ts @@ -31,10 +31,23 @@ const OSRM_PROFILE_PATH: Record<'driving' | 'walking' | 'cycling', string> = { * A configured base is expected to serve the standard OSRM layout, * `/route/v1//…`, which is what `osrm-routed` does out of the box. */ +/** + * A configured base URL without its trailing slashes. + * + * Walked rather than matched with `/\/+$/`: that pattern backtracks over a run of + * slashes, and the value comes from an instance setting somebody types. Linear either + * way in practice, but the regex is the shape a scanner is right to flag. + */ +function withoutTrailingSlashes(value: string): string { + let end = value.length + while (end > 0 && value[end - 1] === '/') end -= 1 + return value.slice(0, end) +} + function routeBaseFor(profile: 'driving' | 'walking' | 'cycling'): string { const configured = useSettingsStore.getState().settings.routing_base_url?.trim() if (!configured) return OSRM_PROFILE_BASE[profile] - return `${configured.replace(/\/+$/, '')}/route/v1/${OSRM_PROFILE_PATH[profile]}` + return `${withoutTrailingSlashes(configured)}/route/v1/${OSRM_PROFILE_PATH[profile]}` } /** diff --git a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx index f950d4955..222e05033 100644 --- a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx +++ b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx @@ -236,7 +236,9 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro const socketsFound = useMemo(() => { const seen = new Set() for (const p of search.results) for (const s of p.charging?.sockets ?? []) seen.add(s.type) - return [...seen].sort() + // Compared explicitly: the default sort is by UTF-16 code unit, which is not the + // order anybody reads a list of plug names in. + return [...seen].sort((a, b) => a.localeCompare(b)) }, [search.results]) const hasCharging = socketsFound.length > 0 diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx index ed2932bc5..2803e31e8 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx @@ -60,6 +60,8 @@ interface RoadtripSidebarProps { * what somebody reading the day needs to know. */ viaCounts?: Record + /** The name of the track each day follows, so the badge can say which road it is. */ + trackNames?: Record } const MODE_ICON: Record = { @@ -759,7 +761,7 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s * (`dayOrderMap` numbers the selected day's assignments from 1). A rail counting across * the trip would put "17" beside a pin the map calls "3". */ -function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, drag, onAskAlternatives, openAlternatives, onEditStay, onSetStopKind, onFollowTrack, viaCount }: { +function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, drag, onAskAlternatives, openAlternatives, onEditStay, onSetStopKind, onFollowTrack, viaCount, trackName }: { day: RoadtripDay selectedAssignmentId?: number | null onSelectStop?: (placeId: number, assignmentId: number) => void @@ -773,6 +775,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on onSetStopKind?: RoadtripSidebarProps['onSetStopKind'] onFollowTrack?: RoadtripSidebarProps['onFollowTrack'] viaCount?: number + trackName?: string }): React.ReactElement { const { from, setFrom, dropAt, setDropAt } = drag const dragging = from?.dayId === day.dayId ? from.index : null @@ -837,7 +840,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on none of them, so this badge is the only place a drive shaped by hand differs from one the router picked on its own. */} {onFollowTrack ? ( - +