Implement Planner-Journal handoff (Group 9)

JWT-based handoff between Journal and Planner:

Journal side:
- JWT token generation scoped to route_id with 7-day expiry (jose)
- "Edit in Planner" button on route detail page — generates JWT,
  redirects to Planner /new with callback URL, token, and GPX
- Callback endpoint (POST /api/routes/:id/callback) validates JWT
  and creates new route version from received GPX

Planner side:
- /new route accepts callback, token, returnUrl, gpx params
- Creates session with callback metadata, initializes with GPX
- "Save to Journal" button POSTs GPX with Bearer token to callback
- "Return to Journal" link shown after successful save

All 6 Group 9 tasks complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-23 22:33:37 +01:00 committed by GitHub
parent f47eca565c
commit 40e541fcce
12 changed files with 287 additions and 11 deletions

View file

@ -3,6 +3,7 @@ import { useYjs } from "~/lib/use-yjs";
import { useRouting } from "~/lib/use-routing";
import { ProfileSelector } from "~/components/ProfileSelector";
import { ExportButton } from "~/components/ExportButton";
import { SaveToJournalButton } from "~/components/SaveToJournalButton";
import { YjsDebugPanel } from "~/components/YjsDebugPanel";
const PlannerMap = lazy(() =>
@ -15,7 +16,14 @@ const ElevationChart = lazy(() =>
import("~/components/ElevationChart").then((m) => ({ default: m.ElevationChart })),
);
export function SessionView({ sessionId }: { sessionId: string }) {
interface SessionViewProps {
sessionId: string;
callbackUrl?: string;
callbackToken?: string;
returnUrl?: string;
}
export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl }: SessionViewProps) {
const yjs = useYjs(sessionId);
const { isHost, computing, routeStats, requestRoute } = useRouting(yjs);
const [highlightPosition, setHighlightPosition] = useState<[number, number] | null>(null);
@ -40,6 +48,14 @@ export function SessionView({ sessionId }: { sessionId: string }) {
<ProfileSelector yjs={yjs} />
</div>
<div className="flex items-center gap-3">
{callbackUrl && callbackToken && (
<SaveToJournalButton
yjs={yjs}
callbackUrl={callbackUrl}
callbackToken={callbackToken}
returnUrl={returnUrl}
/>
)}
<ExportButton yjs={yjs} />
{computing && (
<span className="text-xs text-blue-600">Computing route...</span>