import { swrFetcher } from "@/api/api" import { cancelServerTransfer, retryServerTransfer } from "@/api/transfer" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { useAuth } from "@/hooks/useAuth" import { ModelServerTransfer, ModelServerTransferStatus, ModelServerTransferStatusCancelled, ModelServerTransferStatusFailed, ModelServerTransferStatusPending, ModelServerTransferStatusTimeout, ModelServerTransferStatusVerified, } from "@/types" import { useCallback, useEffect, useMemo, useRef } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import useSWR from "swr" // Map status enum to a label key + a Badge variant. Centralised here so the // table cell and any future drawer agree. Verified is "secondary" because // the row is no longer actionable; Failed/Timeout use "destructive" to draw // the operator's attention. Cancelled is "outline" — soft signal, expected. const statusMeta: Record< ModelServerTransferStatus, { key: string; variant: "default" | "secondary" | "destructive" | "outline" } > = { [ModelServerTransferStatusPending]: { key: "Transfer.StatusPending", variant: "default" }, [ModelServerTransferStatusVerified]: { key: "Transfer.StatusVerified", variant: "secondary" }, [ModelServerTransferStatusFailed]: { key: "Transfer.StatusFailed", variant: "destructive" }, [ModelServerTransferStatusTimeout]: { key: "Transfer.StatusTimeout", variant: "destructive" }, [ModelServerTransferStatusCancelled]: { key: "Transfer.StatusCancelled", variant: "outline" }, } export default function TransferPage() { const { t } = useTranslation() const { profile } = useAuth() const isAdmin = profile?.role === 0 const callerID = profile?.id const { data, mutate, error } = useSWR( "/api/v1/transfer", swrFetcher, ) useEffect(() => { if (error) { toast(t("Error"), { description: t("Results.UnExpectedError", { error: error.message }), }) } }, [error, t]) // Live updates: subscribe to /ws/transfer and patch the SWR cache on each // pushed event. We don't refetch on every event because the WS payload IS // the latest row — refetching would just cost a round trip. // // Reconnect contract: the original implementation opened the socket once // and never reconnected, so a dashboard restart, deploy, or transient // network blip silently froze this view — rows stayed Pending forever // even after the agent had reconnected and the backend had transitioned // them, and operators only discovered it on a hard refresh. Reconnect // with capped exponential backoff (1s → 30s) and trigger a SWR revalidate // on each open so any transitions that broadcast while we were offline // are reconciled. const wsRef = useRef(null) useEffect(() => { let cancelled = false let reconnectTimer: ReturnType | null = null let backoffMs = 1000 const maxBackoffMs = 30000 const connect = () => { if (cancelled) return const proto = window.location.protocol === "https:" ? "wss:" : "ws:" const ws = new WebSocket(`${proto}//${window.location.host}/api/v1/ws/transfer`) wsRef.current = ws ws.onopen = () => { backoffMs = 1000 // Backfill anything the broker fanned out while we were // reconnecting — the broker drops events for full subscribers // and there is no replay on the wire. mutate() } ws.onmessage = (ev) => { try { const t: ModelServerTransfer = JSON.parse(ev.data) mutate((prev) => { const list = prev ?? [] const idx = list.findIndex((x) => x.id === t.id) if (idx === -1) return [t, ...list] const next = list.slice() next[idx] = t return next }, false) } catch (e) { // Malformed payload — log and keep the socket open; a refetch // on next mount will re-sync. Closing the WS would be louder // than the actual problem. console.error("transfer ws parse failed", e) } } const scheduleReconnect = () => { if (cancelled) return if (reconnectTimer !== null) return const delay = backoffMs backoffMs = Math.min(backoffMs * 2, maxBackoffMs) reconnectTimer = setTimeout(() => { reconnectTimer = null connect() }, delay) } ws.onclose = scheduleReconnect ws.onerror = () => { // onerror fires before onclose and may not produce a close // event in every browser when the handshake is rejected. // Force-close so onclose's reconnect path runs deterministically. ws.close() } } connect() return () => { cancelled = true if (reconnectTimer !== null) { clearTimeout(reconnectTimer) reconnectTimer = null } const ws = wsRef.current wsRef.current = null if (ws) { // Detach handlers so the close we trigger here doesn't bounce // into a reconnect attempt against an unmounted component. ws.onopen = null ws.onmessage = null ws.onclose = null ws.onerror = null ws.close() } } }, [mutate]) const onCancel = useCallback( async (id: number) => { try { await cancelServerTransfer(id) toast(t("Transfer.CancelRequested")) mutate() } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e) toast(t("Error"), { description: msg }) } }, [mutate, t], ) const onRetry = useCallback( async (id: number) => { try { await retryServerTransfer(id) toast(t("Transfer.RetryRequested")) mutate() } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e) toast(t("Error"), { description: msg }) } }, [mutate, t], ) const rows = useMemo(() => data ?? [], [data]) return (

{t("Transfer.Title")}

{t("Transfer.PageHint")}

ID {t("ServerID")} {t("Transfer.From")} {t("Transfer.To")} {t("Transfer.Initiator")} {t("Status")} {t("Transfer.LastError")} {t("CreatedAt")} {t("Actions")} {rows.length === 0 ? ( {t("NoResults")} ) : ( rows.map((row) => { const meta = statusMeta[row.status] const isPending = row.status === ModelServerTransferStatusPending const isTerminalRevert = row.status === ModelServerTransferStatusFailed || row.status === ModelServerTransferStatusTimeout || row.status === ModelServerTransferStatusCancelled return ( {row.id} {row.server_id} {row.from_user_id} {row.to_user_id} {row.initiator_id} {t(meta.key)} {row.last_error || "-"} {new Date(row.created_at).toLocaleString()} {isPending && (isAdmin || row.from_user_id === callerID) && ( )} {isTerminalRevert && isAdmin && ( )} ) }) )}
) }