"use client"; import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import { api, ServerStatus, GenerateKeyOptions } from "@/lib/api"; import { Badge, Button, Card, CardHeader, CardTitle } from "@/components/ui"; import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui"; function statusVariant(status: ServerStatus) { switch (status) { case "active": return "success"; case "pending": return "warning"; case "offline": return "danger"; } } function formatDate(dateStr: string) { return new Date(dateStr).toLocaleString(); } const KEY_SIZES: Record = { rsa: [2048, 3072, 4096], ecdsa: [256, 384, 521], }; const DEFAULT_SIZE: Record = { rsa: 4096, ecdsa: 256, }; function GenerateKeyModal({ onClose, onSubmit, isPending, }: { onClose: () => void; onSubmit: (opts: GenerateKeyOptions) => void; isPending: boolean; }) { const [label, setLabel] = useState(""); const [keyType, setKeyType] = useState<"ed25519" | "rsa" | "ecdsa">("ed25519"); const [keySize, setKeySize] = useState(4096); const [passphrase, setPassphrase] = useState(""); const [comment, setComment] = useState(""); function handleKeyTypeChange(t: "ed25519" | "rsa" | "ecdsa") { setKeyType(t); if (t !== "ed25519") { setKeySize(DEFAULT_SIZE[t]); } } function handleSubmit(e: React.FormEvent) { e.preventDefault(); onSubmit({ label: label || "generated", key_type: keyType, key_size: keyType !== "ed25519" ? keySize : undefined, passphrase: passphrase || undefined, comment: comment || undefined, }); } const sizes = KEY_SIZES[keyType]; return (

Generate SSH Key

setLabel(e.target.value)} placeholder="e.g. server-deploy-key" className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" />
{(["ed25519", "rsa", "ecdsa"] as const).map(t => ( ))}
{keyType === "ed25519" && (

Modern, fast, and secure. Recommended for new keys.

)} {keyType === "rsa" && (

Widely compatible with older systems.

)} {keyType === "ecdsa" && (

Elliptic curve — shorter keys, good compatibility.

)}
{sizes && (
)}
setComment(e.target.value)} placeholder="e.g. user@hostname" className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" />
setPassphrase(e.target.value)} placeholder="Optional passphrase" autoComplete="new-password" className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" />
); } export default function ServerDetailPage() { const params = useParams(); const router = useRouter(); const queryClient = useQueryClient(); const serverId = params.id as string; const [confirmDelete, setConfirmDelete] = useState(false); const [showGenerateModal, setShowGenerateModal] = useState(false); const [copiedUpdate, setCopiedUpdate] = useState(false); const { data: server, isLoading, error } = useQuery({ queryKey: ["servers", serverId], queryFn: () => api.getServer(serverId), refetchInterval: 30_000, }); const { mutate: generateKey, isPending: isGenerating } = useMutation({ mutationFn: (opts: GenerateKeyOptions) => api.generateKeyForServer(serverId, opts), onSuccess: () => { setShowGenerateModal(false); queryClient.invalidateQueries({ queryKey: ["servers", serverId] }); queryClient.invalidateQueries({ queryKey: ["keys"] }); }, }); const { mutate: deleteServer, isPending: isDeleting } = useMutation({ mutationFn: () => api.deleteServer(serverId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["servers"] }); router.push("/servers"); }, }); if (isLoading) { return (
); } if (error || !server) { return (
Server not found or failed to load.
); } return (
{showGenerateModal && ( setShowGenerateModal(false)} onSubmit={opts => generateKey(opts)} isPending={isGenerating} /> )}
← Servers

{server.hostname}

{server.status}

{server.ip_address}

{!confirmDelete ? ( ) : (
Are you sure?
)}
Update Agent

Run this command on the server as root to update the agent to the latest version:

              ${" "}
              {api.getUpdateCommand()}
            
Details
Server ID
{server.server_id}
OS
{server.os_info}
Last Seen
{server.last_seen ? formatDate(server.last_seen) : "Never"}
Registered
{formatDate(server.created_at)}

Installed Keys {server.keys?.filter(k => !k.revoked_at).length ?? 0} active

{!server.keys || server.keys.length === 0 ? (

No keys assigned to this server.

) : ( {server.keys.filter(a => a.key).map((assignment) => ( ))}
Label Fingerprint Source Status Assigned
{assignment.key.label} {assignment.key.fingerprint} {assignment.key.source} {assignment.revoked_at ? "revoked" : "active"} {formatDate(assignment.assigned_at)}
)}
); }