diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 46408799..9096b4ae 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -203,6 +203,7 @@ export default function EditGatewayPage() { } const updated = (await response.json()) as Gateway; setGateway(updated); + router.push(`/gateways/${updated.id}`); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong."); } finally { diff --git a/frontend/src/app/gateways/[gatewayId]/page.tsx b/frontend/src/app/gateways/[gatewayId]/page.tsx new file mode 100644 index 00000000..63c59a32 --- /dev/null +++ b/frontend/src/app/gateways/[gatewayId]/page.tsx @@ -0,0 +1,311 @@ +"use client"; + +import { useMemo } from "react"; +import { useParams, useRouter } from "next/navigation"; + +import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs"; + +import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; +import { DashboardShell } from "@/components/templates/DashboardShell"; +import { Button } from "@/components/ui/button"; +import { useAuthedQuery } from "@/lib/api-query"; + +type Gateway = { + id: string; + name: string; + url: string; + token?: string | null; + main_session_key: string; + workspace_root: string; + skyll_enabled?: boolean; + created_at: string; + updated_at: string; +}; + +type Agent = { + id: string; + name: string; + status: string; + board_id?: string | null; + last_seen_at?: string | null; + updated_at: string; +}; + +type GatewayStatus = { + connected: boolean; + gateway_url: string; + sessions_count?: number; + error?: string; +}; + +const formatTimestamp = (value?: string | null) => { + if (!value) return "—"; + const date = new Date(`${value}${value.endsWith("Z") ? "" : "Z"}`); + if (Number.isNaN(date.getTime())) return "—"; + return date.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}; + +const maskToken = (value?: string | null) => { + if (!value) return "—"; + if (value.length <= 8) return "••••"; + return `••••${value.slice(-4)}`; +}; + +export default function GatewayDetailPage() { + const router = useRouter(); + const params = useParams(); + const gatewayIdParam = params?.gatewayId; + const gatewayId = Array.isArray(gatewayIdParam) + ? gatewayIdParam[0] + : gatewayIdParam; + + const gatewayQuery = useAuthedQuery( + ["gateway", gatewayId ?? "unknown"], + gatewayId ? `/api/v1/gateways/${gatewayId}` : null, + { refetchInterval: 30_000 } + ); + + const gateway = gatewayQuery.data ?? null; + + const agentsQuery = useAuthedQuery( + ["gateway-agents", gatewayId ?? "unknown"], + gatewayId ? `/api/v1/agents?gateway_id=${gatewayId}` : null, + { refetchInterval: 15_000 } + ); + + const statusPath = gateway + ? (() => { + const params = new URLSearchParams({ gateway_url: gateway.url }); + if (gateway.token) { + params.set("gateway_token", gateway.token); + } + if (gateway.main_session_key) { + params.set("gateway_main_session_key", gateway.main_session_key); + } + return `/api/v1/gateways/status?${params.toString()}`; + })() + : null; + + const statusQuery = useAuthedQuery( + ["gateway-status", gatewayId ?? "unknown"], + statusPath, + { refetchInterval: 15_000, enabled: Boolean(statusPath) } + ); + + const agents = agentsQuery.data ?? []; + const isConnected = statusQuery.data?.connected ?? false; + + const title = useMemo( + () => (gateway?.name ? gateway.name : "Gateway"), + [gateway?.name] + ); + + return ( + + +
+
+

Sign in to view a gateway.

+ + + +
+
+
+ + +
+
+
+
+

+ {title} +

+

+ Gateway configuration and connection details. +

+
+
+ + {gatewayId ? ( + + ) : null} +
+
+
+ +
+ {gatewayQuery.isLoading ? ( +
+ Loading gateway… +
+ ) : gatewayQuery.error ? ( +
+ {gatewayQuery.error.message} +
+ ) : gateway ? ( +
+
+
+
+

+ Connection +

+
+ + + {statusQuery.isLoading + ? "Checking" + : isConnected + ? "Online" + : "Offline"} + +
+
+
+
+

Gateway URL

+

+ {gateway.url} +

+
+
+

Token

+

+ {maskToken(gateway.token)} +

+
+
+

Skyll

+

+ {gateway.skyll_enabled ? "Enabled" : "Not installed"} +

+
+
+
+ +
+

+ Runtime +

+
+
+

+ Main session key +

+

+ {gateway.main_session_key} +

+
+
+

Workspace root

+

+ {gateway.workspace_root} +

+
+
+
+

Created

+

+ {formatTimestamp(gateway.created_at)} +

+
+
+

Updated

+

+ {formatTimestamp(gateway.updated_at)} +

+
+
+
+
+
+ +
+
+

+ Agents +

+ {agentsQuery.isLoading ? ( + Loading… + ) : ( + + {agents.length} total + + )} +
+
+ + + + + + + + + + + {agents.length === 0 && !agentsQuery.isLoading ? ( + + + + ) : ( + agents.map((agent) => ( + + + + + + + )) + )} + +
AgentStatusLast seenUpdated
+ No agents assigned to this gateway. +
+

+ {agent.name} +

+

+ {agent.id} +

+
+ {agent.status} + + {formatTimestamp(agent.last_seen_at ?? null)} + + {formatTimestamp(agent.updated_at)} +
+
+
+
+ ) : null} +
+
+
+
+ ); +} diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index d775d2ae..e9e4dda3 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -161,7 +161,7 @@ export default function NewGatewayPage() { throw new Error("Unable to create gateway."); } const created = (await response.json()) as { id: string }; - router.push(`/gateways/${created.id}/edit`); + router.push(`/gateways/${created.id}`); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong."); } finally { diff --git a/frontend/src/app/gateways/page.tsx b/frontend/src/app/gateways/page.tsx index 534822d2..366f857c 100644 --- a/frontend/src/app/gateways/page.tsx +++ b/frontend/src/app/gateways/page.tsx @@ -119,14 +119,17 @@ export default function GatewaysPage() { accessorKey: "name", header: "Gateway", cell: ({ row }) => ( -
-

+ +

{row.original.name}

{truncate(row.original.url, 36)}

-
+ ), }, {