e0b166ad60
ci / rust (push) Has been cancelled
Browser UI for the host's management REST API (mgmt.rs / docs/api/openapi.json). Stack, exactly as specified: - TanStack Start (Vite, SPA mode) — file-based routes, SSR shell + client hydration. - React Query via orval codegen from the checked-in OpenAPI spec: a custom fetch mutator (src/api/fetcher.ts) centralizes the base URL, the bearer token (Settings → localStorage), JSON, and a throwing ApiError; the query client skips retries on 4xx. orval returns the response body directly (includeHttpResponseReturnType:false) so a query's `.data` is the typed payload; GET→useQuery, POST/DELETE→useMutation by method. - shadcn/ui on Tailwind v4 (CSS-first tokens, dark-first) — button/card/badge/input/label/ table/skeleton primitives hand-authored from the canonical source. - Paraglide i18n (en + de) with a reactive useLocale() hook and a language switcher. Pages: dashboard (live status — video/audio/session/stream, stop-session + request-IDR, 2s polling), host (identity/codecs/ports), clients (paired list + unpair), pairing (PIN submit, polls pin_pending), settings (API token + language). Dev server proxies /api → 127.0.0.1:47990 (same-origin, no CORS; PUNKTFUNK_MGMT_URL to override). Generated code (orval client, paraglide runtime, routeTree) is gitignored and reproduced by `pnpm codegen` (prepare/pre* scripts). Validated live against `serve`: API shapes match, dev proxy works, SSR shell renders the localized nav, build + tsc green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { Trash2 } from 'lucide-react'
|
|
import {
|
|
useListPairedClients,
|
|
useUnpairClient,
|
|
getListPairedClientsQueryKey,
|
|
} from '@/api/gen/clients/clients'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { QueryState } from '@/components/query-state'
|
|
import { m } from '@/paraglide/messages'
|
|
import { useLocale } from '@/lib/i18n'
|
|
|
|
export const Route = createFileRoute('/clients')({ component: ClientsPage })
|
|
|
|
function ClientsPage() {
|
|
useLocale()
|
|
const qc = useQueryClient()
|
|
const clients = useListPairedClients()
|
|
const unpair = useUnpairClient()
|
|
const rows = clients.data ?? []
|
|
|
|
const onUnpair = (fingerprint: string) => {
|
|
if (!confirm(m.clients_unpair_confirm())) return
|
|
unpair.mutate(
|
|
{ fingerprint },
|
|
{ onSuccess: () => qc.invalidateQueries({ queryKey: getListPairedClientsQueryKey() }) },
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-2xl font-semibold">{m.clients_title()}</h1>
|
|
<QueryState isLoading={clients.isLoading} error={clients.error} refetch={clients.refetch}>
|
|
{rows.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-8 text-center text-sm text-muted-foreground">
|
|
{m.clients_empty()}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{m.clients_name()}</TableHead>
|
|
<TableHead>{m.clients_fingerprint()}</TableHead>
|
|
<TableHead className="w-12" />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((c) => (
|
|
<TableRow key={c.fingerprint}>
|
|
<TableCell className="font-medium">{c.subject || '—'}</TableCell>
|
|
<TableCell className="font-mono text-xs text-muted-foreground">
|
|
{c.fingerprint.slice(0, 16)}…
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
aria-label={m.action_unpair()}
|
|
disabled={unpair.isPending}
|
|
onClick={() => onUnpair(c.fingerprint)}
|
|
>
|
|
<Trash2 className="size-4 text-destructive" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</QueryState>
|
|
</div>
|
|
)
|
|
}
|