Land a paired panel on its own screen instead of a new code
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
This commit is contained in:
@@ -1,12 +1,32 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
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 { Check, Copy } from "lucide-react"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
import { ApiError, PanelsService } from "@/client"
|
import { ApiError, PanelsService } from "@/client"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
|
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.
|
* 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
|
* code into the panels dialog to say which panel this device is, and the
|
||||||
* credential it mints arrives here on the next poll.
|
* 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
|
* Outside both shells like `/view/{name}`: until it is paired, this device has
|
||||||
* no session and there is nothing to put around it.
|
* 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/")({
|
export const Route = createFileRoute("/panel/")({
|
||||||
component: PairPanel,
|
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" }] }),
|
head: () => ({ meta: [{ title: "Pair this panel - Fluksio" }] }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user