Fix waypoint loading in Edit in Planner flow

Three issues fixed:

1. GPX parser used browser DOMParser which doesn't exist in Node/Vite SSR.
   Added async parseGpxAsync() using linkedom for server-side parsing.

2. Server-side session initialization stored waypoints in a Yjs doc
   instance separate from the Vite WebSocket plugin's doc store.
   Moved waypoint initialization to client-side: API returns parsed
   waypoints, client adds them to Yjs after sync.

3. GPX was encoded in URL params causing HTTP 431. Now the Journal
   creates a Planner session via API (POST body), and only passes
   compact waypoint coordinates in URL params.

Verified: Journal route → Edit in Planner → 4 waypoints loaded,
route computed (79.3km), elevation profile, Save to Journal ready.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-24 21:12:57 +01:00 committed by GitHub
parent 2e2dbe05c1
commit 14b179bb0c
9 changed files with 201 additions and 13 deletions

View file

@ -21,10 +21,11 @@ interface SessionViewProps {
callbackUrl?: string;
callbackToken?: string;
returnUrl?: string;
initialWaypoints?: Array<{ lat: number; lon: number; name?: string }>;
}
export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl }: SessionViewProps) {
const yjs = useYjs(sessionId);
export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, initialWaypoints }: SessionViewProps) {
const yjs = useYjs(sessionId, initialWaypoints);
const { isHost, computing, routeStats, requestRoute } = useRouting(yjs);
const [highlightPosition, setHighlightPosition] = useState<[number, number] | null>(null);

View file

@ -36,10 +36,14 @@ export interface YjsState {
connected: boolean;
}
export function useYjs(sessionId: string): YjsState | null {
export function useYjs(
sessionId: string,
initialWaypoints?: Array<{ lat: number; lon: number; name?: string }>,
): YjsState | null {
const [state, setState] = useState<YjsState | null>(null);
const providerRef = useRef<WebsocketProvider | null>(null);
const docRef = useRef<Y.Doc | null>(null);
const initializedWaypoints = useRef(false);
useEffect(() => {
const doc = new Y.Doc();
@ -52,10 +56,28 @@ export function useYjs(sessionId: string): YjsState | null {
const waypoints = doc.getArray<Y.Map<unknown>>("waypoints");
const routeData = doc.getMap("routeData");
// Use persistent identity
const { color, name } = getOrCreateUserIdentity();
provider.awareness.setLocalStateField("user", { color, name });
// Initialize waypoints once after first sync
if (initialWaypoints?.length && !initializedWaypoints.current) {
(provider as unknown as { on(event: string, cb: () => void): void }).on("synced", () => {
// Only add if the doc is empty (avoid duplicating on reconnect)
if (waypoints.length === 0 && !initializedWaypoints.current) {
initializedWaypoints.current = true;
doc.transact(() => {
for (const wp of initialWaypoints) {
const yMap = new Y.Map();
yMap.set("lat", wp.lat);
yMap.set("lon", wp.lon);
if (wp.name) yMap.set("name", wp.name);
waypoints.push([yMap]);
}
});
}
});
}
const updateState = (connected: boolean) => {
setState({
doc,

View file

@ -1,7 +1,7 @@
import { data } from "react-router";
import type { Route } from "./+types/api.sessions";
import { createSession, initializeSessionWithWaypoints, listSessions } from "~/lib/sessions";
import { parseGpx } from "@trails-cool/gpx";
import { createSession, listSessions } from "~/lib/sessions";
import { parseGpxAsync } from "@trails-cool/gpx";
export async function action({ request }: Route.ActionArgs) {
if (request.method !== "POST") {
@ -17,10 +17,11 @@ export async function action({ request }: Route.ActionArgs) {
const session = await createSession({ callbackUrl, callbackToken });
let initialWaypoints: Array<{ lat: number; lon: number; name?: string }> | undefined;
if (gpx) {
try {
const gpxData = parseGpx(gpx);
initializeSessionWithWaypoints(session.id, gpxData.waypoints);
const gpxData = await parseGpxAsync(gpx);
initialWaypoints = gpxData.waypoints;
} catch (_e) {
// Continue with empty session if GPX is invalid
}
@ -30,6 +31,7 @@ export async function action({ request }: Route.ActionArgs) {
{
sessionId: session.id,
url: `/session/${session.id}`,
initialWaypoints,
},
{ status: 201 },
);

View file

@ -29,6 +29,11 @@ export default function SessionPage({ loaderData }: Route.ComponentProps) {
const { id } = useParams();
const [searchParams] = useSearchParams();
const returnUrl = searchParams.get("returnUrl") ?? undefined;
const waypointsParam = searchParams.get("waypoints");
let initialWaypoints: Array<{ lat: number; lon: number; name?: string }> | undefined;
if (waypointsParam) {
try { initialWaypoints = JSON.parse(waypointsParam); } catch { /* ignore */ }
}
return (
<div className="flex h-full flex-col">
@ -52,6 +57,7 @@ export default function SessionPage({ loaderData }: Route.ComponentProps) {
callbackUrl={loaderData.callbackUrl ?? undefined}
callbackToken={loaderData.callbackToken ?? undefined}
returnUrl={returnUrl}
initialWaypoints={initialWaypoints}
/>
</Suspense>
)}