Sanitized mirror from private repository - 2026-04-18 11:19:59 UTC
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m14s
Documentation / Deploy to GitHub Pages (push) Has been skipped

This commit is contained in:
Gitea Mirror Bot
2026-04-18 11:19:59 +00:00
commit fb00a325d1
1418 changed files with 359990 additions and 0 deletions

View File

@@ -0,0 +1,379 @@
"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<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";
}
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<string, string> = {
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<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 { data: cloudflare } = usePoll<CloudflareStats>("/api/network/cloudflare", 120000);
const { data: authentik } = usePoll<AuthentikStats & { users?: Array<{ username: string; last_login: string; active: boolean }> }>("/api/network/authentik", 60000);
const { data: gitea } = usePoll<GiteaActivity>("/api/network/gitea", 60000);
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) => <Copyable text={row.answer} className="text-amber-400 font-mono" />,
},
];
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() : "--"; })()}
sub="DNS queries"
/>
<StatCard
label="Blocked"
value={(() => { const v = adguard?.blocked ?? adguard?.num_blocked_filtering; return v != null ? v.toLocaleString() : "--"; })()}
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` : "--"; })()}
sub="processing time"
/>
</div>
{/* Middle: Headscale nodes grid */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-semibold text-cyan-400">Headscale Nodes</CardTitle>
{nodes.length > 0 && (
<Badge variant="secondary" className="text-xs bg-cyan-500/10 border border-cyan-500/20 text-cyan-400">
{nodes.filter(n => n.online).length}/{nodes.length} online
</Badge>
)}
</CardHeader>
<CardContent>
{nodes.length === 0 ? (
(nodesRaw as Record<string,unknown>)?.error ? <p className="text-sm text-red-400">{String((nodesRaw as Record<string,unknown>).error)}</p> : <CardSkeleton lines={4} />
) : (
<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">
<Copyable text={node.ip_addresses?.[0] ?? node.ip ?? "--"} />
</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>
{/* Cloudflare DNS — full width */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-semibold text-orange-400">Cloudflare DNS</CardTitle>
<div className="flex items-center gap-2">
{cloudflare && (
<>
<Badge variant="secondary" className="text-xs bg-orange-500/10 border border-orange-500/20 text-orange-400">
{cloudflare.proxied} proxied
</Badge>
<Badge variant="secondary" className="text-xs bg-white/[0.06] border border-white/[0.08] text-muted-foreground">
{cloudflare.dns_only} DNS only
</Badge>
<Badge variant="secondary" className="text-xs bg-white/[0.04] border border-white/[0.06]">
{cloudflare.total} total
</Badge>
</>
)}
</div>
</CardHeader>
<CardContent>
{!cloudflare ? (
<CardSkeleton lines={6} />
) : (
<div className="space-y-4">
{/* Type badges */}
<div className="flex flex-wrap gap-2">
{Object.entries(cloudflare.types).map(([type, count]) => (
<Badge
key={type}
variant="secondary"
className={`text-xs bg-white/[0.04] border border-white/[0.08] ${dnsTypeColors[type] ?? "text-foreground"}`}
>
{type}: {count}
</Badge>
))}
</div>
{/* Records table */}
{(cloudflare as CloudflareStats & { records?: { name: string; type: string; content: string; proxied: boolean; ttl: number }[] }).records && (
<div className="rounded-lg border border-white/[0.06] overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-white/[0.04] text-xs text-muted-foreground uppercase tracking-wider">
<th className="text-left px-3 py-2 font-medium">Name</th>
<th className="text-left px-3 py-2 font-medium w-16">Type</th>
<th className="text-left px-3 py-2 font-medium">Content</th>
<th className="text-center px-3 py-2 font-medium w-20">Proxy</th>
</tr>
</thead>
<tbody>
{((cloudflare as CloudflareStats & { records?: { name: string; type: string; content: string; proxied: boolean; ttl: number }[] }).records ?? []).map((rec, i) => (
<tr key={i} className="border-t border-white/[0.04] hover:bg-white/[0.03] transition-colors">
<td className="px-3 py-1.5 text-cyan-400 font-mono text-xs truncate max-w-[220px]">{rec.name}</td>
<td className="px-3 py-1.5">
<span className={`text-xs font-medium ${dnsTypeColors[rec.type] ?? ""}`}>{rec.type}</span>
</td>
<td className="px-3 py-1.5 text-amber-400/80 font-mono text-xs truncate max-w-[200px]"><Copyable text={rec.content} /></td>
<td className="px-3 py-1.5 text-center">
{rec.proxied ? (
<span className="inline-block w-2 h-2 rounded-full bg-orange-400" style={{ boxShadow: "0 0 6px rgba(251,146,60,0.5)" }} title="Proxied" />
) : (
<span className="inline-block w-2 h-2 rounded-full bg-gray-500" title="DNS Only" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Authentik, Gitea -- 2 columns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Authentik SSO */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-semibold text-violet-400">Authentik SSO</CardTitle>
{authentik && (
<Badge variant="secondary" className="text-xs bg-violet-500/10 border border-violet-500/20 text-violet-400">
{authentik.active_sessions} sessions
</Badge>
)}
</CardHeader>
<CardContent>
{!authentik ? (
<CardSkeleton lines={4} />
) : (
<div className="space-y-4">
{/* Users */}
{authentik.users && authentik.users.length > 0 && (
<div>
<p className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium mb-2">Users</p>
<div className="space-y-2">
{authentik.users.map((u, i) => (
<div key={i} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<StatusBadge color={u.active ? "green" : "red"} />
<span className="text-violet-400 font-medium">{u.username}</span>
</div>
<span className="text-xs text-muted-foreground/60">
{u.last_login === "never" ? "never" : u.last_login.slice(0, 10)}
</span>
</div>
))}
</div>
</div>
)}
{(authentik.recent_events ?? []).length > 0 && (
<div>
<p className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium mb-2">Recent Events</p>
<div className="space-y-2">
{(authentik.recent_events ?? []).slice(0, 6).map((evt, i) => (
<div key={i} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 min-w-0">
<StatusBadge
color={String(evt.action).includes("login") ? "green" : String(evt.action).includes("fail") ? "red" : "blue"}
label={String(evt.action)}
/>
<span className="text-xs text-muted-foreground/60 truncate">{String(evt.user ?? "")}</span>
</div>
<span className="text-xs text-muted-foreground/50 shrink-0 ml-2">
{String(evt.created ?? "").slice(11, 16)}
</span>
</div>
))}
</div>
</div>
)}
{(authentik.recent_events ?? []).length === 0 && !(authentik.users?.length) && (
<p className="text-sm text-muted-foreground/60">No recent activity</p>
)}
</div>
)}
</CardContent>
</Card>
{/* Gitea Activity */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-base font-semibold text-green-400">Gitea</CardTitle>
{gitea && gitea.open_prs.length > 0 && (
<Badge variant="secondary" className="text-xs bg-green-500/10 border border-green-500/20 text-green-400">
{gitea.open_prs.length} open PR{gitea.open_prs.length !== 1 ? "s" : ""}
</Badge>
)}
</CardHeader>
<CardContent>
{!gitea ? (
<CardSkeleton lines={4} />
) : (
<div className="space-y-4">
{gitea.commits.length > 0 && (
<div>
<p className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium mb-2">Recent Commits</p>
<div className="space-y-2">
{gitea.commits.slice(0, 6).map((c, i) => (
<div key={i} className="text-sm">
<div className="flex items-center gap-2">
<span className="text-amber-400 font-mono text-xs shrink-0">{c.sha.slice(0, 7)}</span>
<span className="text-foreground truncate">{c.message.split("\n")[0]}</span>
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-muted-foreground/60">{c.author}</span>
<span className="text-xs text-muted-foreground/40">{formatTime(c.date)}</span>
</div>
</div>
))}
</div>
</div>
)}
{gitea.open_prs.length > 0 && (
<div>
<p className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium mb-2">Open PRs</p>
<div className="space-y-1.5">
{gitea.open_prs.map((pr, i) => (
<div key={i} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 min-w-0">
<span className="text-green-400 font-mono text-xs shrink-0">#{pr.number}</span>
<span className="text-foreground truncate">{pr.title}</span>
</div>
<span className="text-xs text-muted-foreground/60 shrink-0 ml-2">{pr.author}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
{/* 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>
);
}