fix(planner): nearby-POI lookup sends the real session (was 401)

fetchNearbyPois defaulted sessionId to the placeholder "nearby", which
/api/pois's requireSession rejects (401 Unauthorized) — so the nearby-POI
lookup for a selected waypoint always failed on real deployments. Thread
the live planner sessionId through useNearbyPois → fetchNearbyPois →
queryPois (same session the main POI markers already use), and make
sessionId a required arg so the placeholder can't silently return.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-07-16 23:37:31 +02:00
parent 0f371d868f
commit 267c2fb8b7
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
6 changed files with 13 additions and 11 deletions

View file

@ -185,7 +185,7 @@ export function PlannerMap({ yjs, sessionId, onRouteRequest, highlightPosition,
const { draggingOver, handleDragEnter, handleDragLeave, handleDragOver, handleDrop } = useGpxDrop(yjs, onImportError); const { draggingOver, handleDragEnter, handleDragLeave, handleDragOver, handleDrop } = useGpxDrop(yjs, onImportError);
const selectedWp = selectedWaypointIndex != null ? waypoints[selectedWaypointIndex] : undefined; const selectedWp = selectedWaypointIndex != null ? waypoints[selectedWaypointIndex] : undefined;
const nearbyPoisState = useNearbyPois(selectedWp?.lat, selectedWp?.lon); const nearbyPoisState = useNearbyPois(selectedWp?.lat, selectedWp?.lon, sessionId);
const handleSnapToPoi = useCallback((poi: Poi) => { const handleSnapToPoi = useCallback((poi: Poi) => {
if (selectedWaypointIndex == null) return; if (selectedWaypointIndex == null) return;

View file

@ -114,7 +114,7 @@ function useAwarenessToasts(yjs: YjsState | null, t: TFunction, addToast: (messa
} }
function SidebarTabs({ yjs, routeStats, days, onWaypointHover, onWaypointSelect }: { yjs: YjsState; routeStats: ReturnType<typeof useRouting>["routeStats"]; days: ReturnType<typeof useDays>; onWaypointHover: (index: number | null) => void; onWaypointSelect: (index: number | null) => void }) { function SidebarTabs({ yjs, sessionId, routeStats, days, onWaypointHover, onWaypointSelect }: { yjs: YjsState; sessionId: string; routeStats: ReturnType<typeof useRouting>["routeStats"]; days: ReturnType<typeof useDays>; onWaypointHover: (index: number | null) => void; onWaypointSelect: (index: number | null) => void }) {
const { t } = useTranslation("planner"); const { t } = useTranslation("planner");
const [tab, setTab] = useState<"waypoints" | "notes">("waypoints"); const [tab, setTab] = useState<"waypoints" | "notes">("waypoints");
@ -137,7 +137,7 @@ function SidebarTabs({ yjs, routeStats, days, onWaypointHover, onWaypointSelect
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{tab === "waypoints" ? ( {tab === "waypoints" ? (
<Suspense fallback={null}> <Suspense fallback={null}>
<WaypointSidebar yjs={yjs} routeStats={routeStats} days={days} onWaypointHover={onWaypointHover} onWaypointSelect={onWaypointSelect} /> <WaypointSidebar yjs={yjs} sessionId={sessionId} routeStats={routeStats} days={days} onWaypointHover={onWaypointHover} onWaypointSelect={onWaypointSelect} />
</Suspense> </Suspense>
) : ( ) : (
<NotesPanel yjs={yjs} /> <NotesPanel yjs={yjs} />
@ -292,7 +292,7 @@ export function SessionView({ sessionId, hasJournalCallback, returnUrl, initialW
</div> </div>
</Suspense> </Suspense>
</main> </main>
<SidebarTabs yjs={yjs} routeStats={routeStats} days={days} onWaypointHover={setHighlightedWaypoint} onWaypointSelect={setSelectedWaypointIndex} /> <SidebarTabs yjs={yjs} sessionId={sessionId} routeStats={routeStats} days={days} onWaypointHover={setHighlightedWaypoint} onWaypointSelect={setSelectedWaypointIndex} />
</div> </div>
<YjsDebugPanel yjs={yjs} sessionId={sessionId} /> <YjsDebugPanel yjs={yjs} sessionId={sessionId} />
{toasts.length > 0 && ( {toasts.length > 0 && (

View file

@ -19,6 +19,7 @@ const NOTE_MAX = 500;
interface WaypointSidebarProps { interface WaypointSidebarProps {
yjs: YjsState; yjs: YjsState;
sessionId: string;
routeStats?: { routeStats?: {
distance?: number; distance?: number;
elevationGain?: number; elevationGain?: number;
@ -29,7 +30,7 @@ interface WaypointSidebarProps {
onWaypointSelect?: (index: number | null) => void; onWaypointSelect?: (index: number | null) => void;
} }
export function WaypointSidebar({ yjs, routeStats, days, onWaypointHover, onWaypointSelect }: WaypointSidebarProps) { export function WaypointSidebar({ yjs, sessionId, routeStats, days, onWaypointHover, onWaypointSelect }: WaypointSidebarProps) {
const { t } = useTranslation("planner"); const { t } = useTranslation("planner");
const [waypoints, setWaypoints] = useState<WaypointData[]>([]); const [waypoints, setWaypoints] = useState<WaypointData[]>([]);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null); const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
@ -133,7 +134,7 @@ export function WaypointSidebar({ yjs, routeStats, days, onWaypointHover, onWayp
); );
const selectedWp = selectedIndex !== null ? waypoints[selectedIndex] : undefined; const selectedWp = selectedIndex !== null ? waypoints[selectedIndex] : undefined;
const nearbyPoisState = useNearbyPois(selectedWp?.lat, selectedWp?.lon); const nearbyPoisState = useNearbyPois(selectedWp?.lat, selectedWp?.lon, sessionId);
const [showAllNearby, setShowAllNearby] = useState(false); const [showAllNearby, setShowAllNearby] = useState(false);
const hasMultipleDays = days.length > 1; const hasMultipleDays = days.length > 1;

View file

@ -144,7 +144,7 @@ describe("fetchNearbyPois", () => {
afterEach(() => vi.unstubAllGlobals()); afterEach(() => vi.unstubAllGlobals());
it("builds a bbox around the point and calls /api/pois", async () => { it("builds a bbox around the point and calls /api/pois", async () => {
await fetchNearbyPois(50.0, 10.0, 500, drinkingWater); await fetchNearbyPois(50.0, 10.0, 500, drinkingWater, "sess");
expect(fetch).toHaveBeenCalledOnce(); expect(fetch).toHaveBeenCalledOnce();
const [url] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string]; const [url] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string];
expect(url).toMatch(/\/api\/pois\?bbox=\d+\.\d+,\d+\.\d+,\d+\.\d+,\d+\.\d+/); expect(url).toMatch(/\/api\/pois\?bbox=\d+\.\d+,\d+\.\d+,\d+\.\d+,\d+\.\d+/);
@ -153,7 +153,7 @@ describe("fetchNearbyPois", () => {
it("forwards the AbortSignal to fetch", async () => { it("forwards the AbortSignal to fetch", async () => {
const controller = new AbortController(); const controller = new AbortController();
await fetchNearbyPois(50.0, 10.0, 500, drinkingWater, controller.signal); await fetchNearbyPois(50.0, 10.0, 500, drinkingWater, "sess", controller.signal);
const [, opts] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit]; const [, opts] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
expect(opts.signal).toBe(controller.signal); expect(opts.signal).toBe(controller.signal);
}); });

View file

@ -133,8 +133,8 @@ export async function fetchNearbyPois(
lon: number, lon: number,
radiusMeters: number, radiusMeters: number,
categories: PoiCategory[], categories: PoiCategory[],
sessionId: string,
signal?: AbortSignal, signal?: AbortSignal,
sessionId = "nearby",
): Promise<Poi[]> { ): Promise<Poi[]> {
const dLat = radiusMeters * DEG_PER_METER_LAT; const dLat = radiusMeters * DEG_PER_METER_LAT;
const dLon = dLat / Math.cos((lat * Math.PI) / 180); const dLon = dLat / Math.cos((lat * Math.PI) / 180);

View file

@ -19,6 +19,7 @@ const rateLimitedUntilRef = { current: 0 };
export function useNearbyPois( export function useNearbyPois(
lat: number | undefined, lat: number | undefined,
lon: number | undefined, lon: number | undefined,
sessionId: string,
): NearbyPoisState { ): NearbyPoisState {
const [state, setState] = useState<NearbyPoisState>({ pois: [], status: "idle" }); const [state, setState] = useState<NearbyPoisState>({ pois: [], status: "idle" });
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
@ -47,7 +48,7 @@ export function useNearbyPois(
setState((prev) => ({ ...prev, status: "loading" })); setState((prev) => ({ ...prev, status: "loading" }));
try { try {
const pois = await fetchNearbyPois(lat, lon, NEARBY_RADIUS_METERS, poiCategories, controller.signal); const pois = await fetchNearbyPois(lat, lon, NEARBY_RADIUS_METERS, poiCategories, sessionId, controller.signal);
if (!controller.signal.aborted) { if (!controller.signal.aborted) {
setState({ pois, status: "done" }); setState({ pois, status: "done" });
} }
@ -68,7 +69,7 @@ export function useNearbyPois(
if (timerRef.current) clearTimeout(timerRef.current); if (timerRef.current) clearTimeout(timerRef.current);
abortRef.current?.abort(); abortRef.current?.abort();
}; };
}, [lat, lon]); }, [lat, lon, sessionId]);
return state; return state;
} }