From 65f859c04db9692f7eb04b27bb1b7ee972b2e48a Mon Sep 17 00:00:00 2001 From: stroblme Date: Tue, 25 Aug 2026 21:24:41 +0200 Subject: [PATCH] Land a paired panel on its own screen instead of a new code Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK --- frontend/src/routes/panel.index.tsx | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/panel.index.tsx b/frontend/src/routes/panel.index.tsx index d872b48..de8db01 100644 --- a/frontend/src/routes/panel.index.tsx +++ b/frontend/src/routes/panel.index.tsx @@ -1,12 +1,32 @@ import { useMutation, useQuery } from "@tanstack/react-query" -import { createFileRoute } from "@tanstack/react-router" +import { createFileRoute, redirect } from "@tanstack/react-router" import { Check, Copy } from "lucide-react" import { useEffect } from "react" import { ApiError, PanelsService } from "@/client" import { Button } from "@/components/ui/button" import { useCopyToClipboard } from "@/hooks/useCopyToClipboard" -import { appPath, isPortal, openPortalSession } from "@/lib/portal" +import { apiToken, appPath, isPortal, openPortalSession } from "@/lib/portal" + +/** + * The panel a credential in hand names, or "" when it names none. + * + * A panel token carries the panel it was minted for; a user token and a + * malformed one both mean "not an adopted screen". Read rather than verified: + * this only decides where to send the browser, and the server still judges the + * credential on the first call it makes there. + */ +function pairedPanel(): string { + try { + const [, payload] = apiToken().split(".") + const claims = JSON.parse( + atob(payload.replace(/-/g, "+").replace(/_/g, "/")), + ) + return typeof claims.panel === "string" ? claims.panel : "" + } catch { + return "" + } +} /** * Adopting a screen that has no keyboard. @@ -16,6 +36,10 @@ import { appPath, isPortal, openPortalSession } from "@/lib/portal" * code into the panels dialog to say which panel this device is, and the * credential it mints arrives here on the next poll. * + * Only once, though: a screen that already holds a credential goes straight to + * the panel it names, so a reboot comes back to the dashboard rather than + * asking the household for another code. + * * Outside both shells like `/view/{name}`: until it is paired, this device has * no session and there is nothing to put around it. * @@ -26,6 +50,13 @@ import { appPath, isPortal, openPortalSession } from "@/lib/portal" */ export const Route = createFileRoute("/panel/")({ component: PairPanel, + beforeLoad: () => { + // Nothing to mint a code for. A credential the server has since stopped + // honouring comes back here with storage cleared, so the next arrival + // pairs normally rather than bouncing. + const paired = pairedPanel() + if (paired) throw redirect({ to: "/panel/$id", params: { id: paired } }) + }, head: () => ({ meta: [{ title: "Pair this panel - Fluksio" }] }), })