Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
A panel is one screen and the ordered set of whole dashboards it shows, so a hallway tablet and a workshop tablet carry different sets without either dashboard knowing about the other. More than one and the device draws a rail to switch between them — the same rail the editor puts on screen, because the wall has it and it takes room off the canvas. A screen has no keyboard, so it pairs rather than logs in: it shows a six-character code, somebody approves it against a panel from the dashboards overview, and the credential that mints reaches that panel's published dashboards and the message endpoints its widgets speak, and nothing else. Deleting the panel revokes it. Closes the per-device view and the kiosk credential; supersedes the multi-page/multi-section UI, since a page is now a dashboard of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
import { useMutation, useQuery } from "@tanstack/react-query"
|
|
import { createFileRoute } from "@tanstack/react-router"
|
|
import { useEffect } from "react"
|
|
|
|
import { PanelsService } from "@/client"
|
|
|
|
/**
|
|
* Adopting a screen that has no keyboard.
|
|
*
|
|
* A wall tablet cannot be asked to type an email address and a password, so it
|
|
* asks for a code instead and shows it. Somebody with an account types that
|
|
* code into the panels dialog to say which panel this device is, and the
|
|
* credential it mints arrives here on the next poll.
|
|
*
|
|
* Outside both shells like `/view/{name}`: until it is paired, this device has
|
|
* no session and there is nothing to put around it.
|
|
*/
|
|
export const Route = createFileRoute("/panel/")({
|
|
component: PairPanel,
|
|
head: () => ({ meta: [{ title: "Pair this panel - Fluksio" }] }),
|
|
})
|
|
|
|
function PairPanel() {
|
|
// A code lives ten minutes. Asking for one is the mount, and asking again is
|
|
// what happens when this one is no longer recognised.
|
|
const start = useMutation({ mutationFn: () => PanelsService.startPairing() })
|
|
const request = start.mutate
|
|
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: asked for once, when the screen goes up.
|
|
useEffect(() => {
|
|
request()
|
|
}, [])
|
|
|
|
const started = start.data
|
|
const { data: status, error } = useQuery({
|
|
queryKey: ["pairing", started?.code],
|
|
queryFn: () =>
|
|
PanelsService.pollPairing({
|
|
code: started?.code ?? "",
|
|
secret: started?.secret ?? "",
|
|
}),
|
|
enabled: Boolean(started),
|
|
refetchInterval: 3000,
|
|
// A 404 means this code is finished, not that the request failed.
|
|
retry: false,
|
|
})
|
|
|
|
// Expired, or collected already. Ask for another rather than leaving a
|
|
// number on the wall that no longer works.
|
|
useEffect(() => {
|
|
if (error) request()
|
|
}, [error, request])
|
|
|
|
useEffect(() => {
|
|
if (!status?.access_token || !status.panel) return
|
|
localStorage.setItem("access_token", status.access_token)
|
|
// A full load rather than a route change: everything this page asked for
|
|
// was asked without a credential, and the socket has to dial again holding
|
|
// this one.
|
|
window.location.href = `/panel/${status.panel}`
|
|
}, [status])
|
|
|
|
return (
|
|
<main className="grid h-svh w-full place-items-center p-8">
|
|
<div className="grid max-w-md gap-6 text-center">
|
|
<div className="grid gap-2">
|
|
<h1 className="text-2xl">Pair this panel</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
On a computer, open <span className="font-medium">Dashboards</span>{" "}
|
|
→ <span className="font-medium">Panels</span>, pick which panel this
|
|
is, and enter this code.
|
|
</p>
|
|
</div>
|
|
|
|
<p className="font-mono text-6xl font-medium tracking-[0.2em]">
|
|
<span data-testid="pairing-code" aria-hidden="true">
|
|
{started?.code ?? "······"}
|
|
</span>
|
|
{/* Spelled out, so a reader says the characters rather than trying
|
|
to pronounce them as a word. */}
|
|
<span className="sr-only">
|
|
{(started?.code ?? "").split("").join(" ")}
|
|
</span>
|
|
</p>
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
The code lasts ten minutes; this screen replaces it when it runs out.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|