148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { usePoll } from "@/lib/use-poll";
|
|
import { StatCard } from "@/components/stat-card";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { StatusBadge } from "@/components/status-badge";
|
|
import { DataTable, Column } from "@/components/data-table";
|
|
|
|
interface AdGuardStats {
|
|
total_queries?: number;
|
|
num_dns_queries?: number;
|
|
blocked?: number;
|
|
num_blocked_filtering?: number;
|
|
avg_time?: number;
|
|
avg_processing_time?: number;
|
|
}
|
|
|
|
interface HeadscaleNode {
|
|
id: string;
|
|
name: string;
|
|
ip_addresses?: string[];
|
|
ip?: string;
|
|
online: boolean;
|
|
last_seen?: string;
|
|
}
|
|
|
|
interface DnsRewrite {
|
|
domain: string;
|
|
answer: string;
|
|
}
|
|
|
|
const nodeColors: Record<string, string> = {
|
|
atlantis: "text-blue-400",
|
|
calypso: "text-violet-400",
|
|
olares: "text-emerald-400",
|
|
nuc: "text-amber-400",
|
|
rpi5: "text-cyan-400",
|
|
"homelab-vm": "text-green-400",
|
|
"matrix-ubuntu": "text-pink-400",
|
|
guava: "text-orange-400",
|
|
seattle: "text-teal-400",
|
|
jellyfish: "text-indigo-400",
|
|
};
|
|
|
|
function getNodeColor(name: string): string {
|
|
const lower = name.toLowerCase();
|
|
for (const [key, cls] of Object.entries(nodeColors)) {
|
|
if (lower.includes(key)) return cls;
|
|
}
|
|
return "text-foreground";
|
|
}
|
|
|
|
export default function NetworkPage() {
|
|
const { data: adguard } = usePoll<AdGuardStats>("/api/network/adguard", 60000);
|
|
const { data: nodesRaw } = usePoll<HeadscaleNode[] | { nodes: HeadscaleNode[] }>("/api/network/headscale", 30000);
|
|
const { data: rewritesRaw } = usePoll<DnsRewrite[] | { rewrites: DnsRewrite[] }>("/api/network/adguard/rewrites", 120000);
|
|
|
|
const nodes = Array.isArray(nodesRaw) ? nodesRaw : (nodesRaw?.nodes ?? []);
|
|
const rewrites = Array.isArray(rewritesRaw) ? rewritesRaw : (rewritesRaw?.rewrites ?? []);
|
|
|
|
const rewriteColumns: Column<DnsRewrite>[] = [
|
|
{
|
|
key: "domain",
|
|
label: "Domain",
|
|
render: (row) => <span className="font-medium text-cyan-400">{row.domain}</span>,
|
|
},
|
|
{
|
|
key: "answer",
|
|
label: "Answer",
|
|
render: (row) => <span className="text-amber-400 font-mono">{row.answer}</span>,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-lg font-semibold">Network</h1>
|
|
|
|
{/* Top row: AdGuard stats */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-3 gap-5">
|
|
<StatCard
|
|
label="Total Queries"
|
|
value={(() => { const v = adguard?.total_queries ?? adguard?.num_dns_queries; return v != null ? v.toLocaleString() : "\u2014"; })()}
|
|
sub="DNS queries"
|
|
/>
|
|
<StatCard
|
|
label="Blocked"
|
|
value={(() => { const v = adguard?.blocked ?? adguard?.num_blocked_filtering; return v != null ? v.toLocaleString() : "\u2014"; })()}
|
|
sub="blocked by filters"
|
|
/>
|
|
<StatCard
|
|
label="Avg Response"
|
|
value={(() => { const v = adguard?.avg_time ?? adguard?.avg_processing_time; return v != null ? `${(v * 1000).toFixed(1)}ms` : "\u2014"; })()}
|
|
sub="processing time"
|
|
/>
|
|
</div>
|
|
|
|
{/* Middle: Headscale nodes grid */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base font-semibold">Headscale Nodes</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{nodes.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">Loading...</p>
|
|
) : (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
|
{nodes.map((node) => (
|
|
<Card key={node.id} className="overflow-hidden">
|
|
<CardContent className="pt-3 pb-3 px-4">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`w-2 h-2 rounded-full shrink-0 ${node.online ? "bg-green-500 glow-green" : "bg-red-500 glow-red"}`} />
|
|
<span className={`text-sm font-medium truncate ${getNodeColor(node.name)}`}>{node.name}</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground/70 font-mono">
|
|
{node.ip_addresses?.[0] ?? node.ip ?? "\u2014"}
|
|
</p>
|
|
{node.last_seen && (
|
|
<p className="text-xs text-muted-foreground/60 mt-0.5">
|
|
{new Date(node.last_seen).toLocaleString("en-US", {
|
|
month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
})}
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bottom: DNS rewrites table */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base font-semibold">DNS Rewrites</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DataTable<DnsRewrite>
|
|
data={rewrites}
|
|
columns={rewriteColumns}
|
|
searchKey="domain"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|