"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 { Badge } from "@/components/ui/badge"; import { DataTable, Column } from "@/components/data-table"; import type { CloudflareStats, AuthentikStats, GiteaActivity } from "@/lib/types"; import { CardSkeleton } from "@/components/skeleton"; import { Copyable } from "@/components/copyable"; 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 = { 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"; } function formatTime(ts: string): string { try { return new Date(ts).toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return ts; } } const dnsTypeColors: Record = { A: "text-blue-400", AAAA: "text-violet-400", CNAME: "text-cyan-400", MX: "text-amber-400", TXT: "text-green-400", SRV: "text-pink-400", NS: "text-teal-400", }; export default function NetworkPage() { const { data: adguard } = usePoll("/api/network/adguard", 60000); const { data: nodesRaw } = usePoll("/api/network/headscale", 30000); const { data: rewritesRaw } = usePoll("/api/network/adguard/rewrites", 120000); const { data: cloudflare } = usePoll("/api/network/cloudflare", 120000); const { data: authentik } = usePoll }>("/api/network/authentik", 60000); const { data: gitea } = usePoll("/api/network/gitea", 60000); const nodes = Array.isArray(nodesRaw) ? nodesRaw : (nodesRaw?.nodes ?? []); const rewrites = Array.isArray(rewritesRaw) ? rewritesRaw : (rewritesRaw?.rewrites ?? []); const rewriteColumns: Column[] = [ { key: "domain", label: "Domain", render: (row) => {row.domain}, }, { key: "answer", label: "Answer", render: (row) => , }, ]; return (

Network

{/* Top row: AdGuard stats */}
{ const v = adguard?.total_queries ?? adguard?.num_dns_queries; return v != null ? v.toLocaleString() : "--"; })()} sub="DNS queries" /> { const v = adguard?.blocked ?? adguard?.num_blocked_filtering; return v != null ? v.toLocaleString() : "--"; })()} sub="blocked by filters" /> { const v = adguard?.avg_time ?? adguard?.avg_processing_time; return v != null ? `${(v * 1000).toFixed(1)}ms` : "--"; })()} sub="processing time" />
{/* Middle: Headscale nodes grid */} Headscale Nodes {nodes.length > 0 && ( {nodes.filter(n => n.online).length}/{nodes.length} online )} {nodes.length === 0 ? ( (nodesRaw as Record)?.error ?

{String((nodesRaw as Record).error)}

: ) : (
{nodes.map((node) => (
{node.name}

{node.last_seen && (

{new Date(node.last_seen).toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", })}

)}
))}
)}
{/* Cloudflare DNS — full width */} Cloudflare DNS
{cloudflare && ( <> {cloudflare.proxied} proxied {cloudflare.dns_only} DNS only {cloudflare.total} total )}
{!cloudflare ? ( ) : (
{/* Type badges */}
{Object.entries(cloudflare.types).map(([type, count]) => ( {type}: {count} ))}
{/* Records table */} {(cloudflare as CloudflareStats & { records?: { name: string; type: string; content: string; proxied: boolean; ttl: number }[] }).records && (
{((cloudflare as CloudflareStats & { records?: { name: string; type: string; content: string; proxied: boolean; ttl: number }[] }).records ?? []).map((rec, i) => ( ))}
Name Type Content Proxy
{rec.name} {rec.type} {rec.proxied ? ( ) : ( )}
)}
)}
{/* Authentik, Gitea -- 2 columns */}
{/* Authentik SSO */} Authentik SSO {authentik && ( {authentik.active_sessions} sessions )} {!authentik ? ( ) : (
{/* Users */} {authentik.users && authentik.users.length > 0 && (

Users

{authentik.users.map((u, i) => (
{u.username}
{u.last_login === "never" ? "never" : u.last_login.slice(0, 10)}
))}
)} {(authentik.recent_events ?? []).length > 0 && (

Recent Events

{(authentik.recent_events ?? []).slice(0, 6).map((evt, i) => (
{String(evt.user ?? "")}
{String(evt.created ?? "").slice(11, 16)}
))}
)} {(authentik.recent_events ?? []).length === 0 && !(authentik.users?.length) && (

No recent activity

)}
)}
{/* Gitea Activity */} Gitea {gitea && gitea.open_prs.length > 0 && ( {gitea.open_prs.length} open PR{gitea.open_prs.length !== 1 ? "s" : ""} )} {!gitea ? ( ) : (
{gitea.commits.length > 0 && (

Recent Commits

{gitea.commits.slice(0, 6).map((c, i) => (
{c.sha.slice(0, 7)} {c.message.split("\n")[0]}
{c.author} {formatTime(c.date)}
))}
)} {gitea.open_prs.length > 0 && (

Open PRs

{gitea.open_prs.map((pr, i) => (
#{pr.number} {pr.title}
{pr.author}
))}
)}
)}
{/* Bottom: DNS rewrites table */} DNS Rewrites data={rewrites} columns={rewriteColumns} searchKey="domain" />
); }