Files
domrichardson c9868b2108
Agent Release / build (push) Has been cancelled
Server Deploy / deploy (push) Has been cancelled
first commit
2026-06-15 13:58:45 +01:00

140 lines
5.0 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { api, Server, ServerStatus } from "@/lib/api";
import { Badge, Button, Card } 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 formatLastSeen(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) return `${diffSec}s ago`;
if (diffMin < 60) return `${diffMin}m ago`;
if (diffHour < 24) return `${diffHour}h ago`;
return `${diffDay}d ago`;
}
export default function ServersPage() {
const { data: servers, isLoading, error } = useQuery({
queryKey: ["servers"],
queryFn: api.listServers,
refetchInterval: 30_000,
});
return (
<div className="p-8">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-text-primary">Servers</h1>
<p className="mt-1 text-sm text-text-secondary">
{servers?.length ?? 0} registered server{servers?.length !== 1 ? "s" : ""}
</p>
</div>
<Link href="/servers/new">
<Button variant="primary">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Add Server
</Button>
</Link>
</div>
<Card padding={false}>
{isLoading ? (
<div className="flex items-center justify-center py-20">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
</div>
) : error ? (
<div className="py-20 text-center text-danger">
Failed to load servers. Is the backend running?
</div>
) : servers && servers.length > 0 ? (
<Table>
<Thead>
<Tr>
<Th>Hostname</Th>
<Th>IP Address</Th>
<Th>OS</Th>
<Th>Status</Th>
<Th>Last Seen</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{servers.map((server: Server) => (
<Tr key={server.server_id}>
<Td>
<span className="font-medium text-text-primary">
{server.hostname}
</span>
</Td>
<Td>
<span className="font-mono text-text-secondary">
{server.ip_address}
</span>
</Td>
<Td>
<span className="text-text-secondary">{server.os_info}</span>
</Td>
<Td>
<Badge variant={statusVariant(server.status)}>
{server.status}
</Badge>
</Td>
<Td>
<span className="text-text-secondary">
{server.last_seen
? formatLastSeen(server.last_seen)
: "Never"}
</span>
</Td>
<Td>
<Link href={`/servers/${server.server_id}`}>
<Button variant="ghost" size="sm">
View
</Button>
</Link>
</Td>
</Tr>
))}
</Tbody>
</Table>
) : (
<div className="py-20 text-center">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-surface-2">
<svg className="h-6 w-6 text-text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
</svg>
</div>
<p className="text-text-secondary">No servers registered yet.</p>
<Link href="/servers/new">
<Button variant="primary" size="sm" className="mt-4">
Add your first server
</Button>
</Link>
</div>
)}
</Card>
</div>
);
}