Files
bootnode/console/components/lux/fleet-dashboard.tsx
zeekayandHanzo Dev da98c0de75 feat(console): one icon set — every glyph comes from @luxfi/ui/icons
74 files imported `lucide-react` directly, pinned ^1.26.0, while @luxfi/ui —
which this console renders alongside — resolves 1.27. Two Lucide copies from two
resolutions in one bundle is how a set stops being one set. Two more glyphs were
not from any icon library at all: the node list drew its network in/out arrows as
the literal characters "↓" and "↑", and the chain onboarding selector drew its
tick as "✓". Those are font glyphs — they take the text metrics, not the icon
box, so they sit on a different baseline and scale with the font, not `size-*`.

One source now: `@luxfi/ui/icons`, Lucide as @hanzo/gui standardises on it and
@luxfi/ui themes it. `lucide-react` is out of package.json, so per-surface drift
is no longer expressible; @luxfi/ui comes in at 7.4.3, which is where `./icons`
lives. The three text glyphs are ArrowDown / ArrowUp / Check.

All 121 distinct icon names this console uses were checked against the 6014
symbols @luxfi/ui/icons exports before the rewrite — zero missing, with a
negative control. `next build` is clean: 78 routes, Next 16.2.11 + Turbopack.

The d3 chart roots (node-globe, node-map, globe-view, donut-chart, fleet-map)
keep their <svg>: those are plots, not glyphs. The command menu's "↑↓" stays —
it is a keyboard legend inside <kbd>, and it means the keys.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:57:55 -07:00

445 lines
15 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Activity,
AlertTriangle,
ChevronDown,
ChevronRight,
Circle,
Cpu,
Heart,
Loader2,
Network as NetworkIcon,
Package,
RefreshCcw,
Server,
Users,
} from "@luxfi/ui/icons"
import { FleetMap, type FleetRegion } from "@/components/lux/fleet-map"
import {
CHAIN_LABEL,
NETWORK_LABEL,
TRACKED_CHAINS,
bestHeight,
chainBootstrapped,
fetchFleetOverview,
fleetStatus,
nodeHealth,
type FleetNetworkName,
type LuxFleetOverview,
type LuxNetworkFleet,
type LuxNode,
} from "@/lib/fleet"
const REFRESH_MS = 15_000
const NETWORK_ACCENT: Record<FleetNetworkName, string> = {
mainnet: "border-green-500/50 text-green-400",
testnet: "border-blue-500/50 text-blue-400",
devnet: "border-orange-500/50 text-orange-400",
}
const STATUS_BADGE: Record<string, { cls: string; label: string }> = {
operational: { cls: "border-green-500/40 text-green-400 bg-green-500/10", label: "Operational" },
degraded: { cls: "border-yellow-500/40 text-yellow-400 bg-yellow-500/10", label: "Degraded" },
down: { cls: "border-red-500/40 text-red-400 bg-red-500/10", label: "Down" },
}
function StatNumber({
icon: Icon,
label,
value,
suffix,
accent,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value: number | string | null | undefined
suffix?: string
accent?: string
}) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<Icon className={`h-5 w-5 ${accent ?? "text-muted-foreground"}`} />
<span className={`text-2xl font-bold ${accent ?? ""}`}>
{value ?? "—"}
{suffix && value != null ? <span className="text-base text-muted-foreground">{suffix}</span> : null}
</span>
</div>
</CardContent>
</Card>
)
}
function healthDot(h: ReturnType<typeof nodeHealth>) {
const cls =
h === "healthy"
? "text-green-500"
: h === "syncing"
? "text-yellow-500"
: h === "down"
? "text-red-500"
: "text-zinc-500"
return <Circle className={`h-2.5 w-2.5 fill-current ${cls}`} />
}
function boolBadge(v: boolean | null, trueLabel = "Yes", falseLabel = "No") {
if (v == null) return <span className="text-xs text-muted-foreground">—</span>
return (
<Badge variant={v ? "success" : "warning"} className="text-[10px]">
{v ? trueLabel : falseLabel}
</Badge>
)
}
function NodeTable({ network, nodes }: { network: FleetNetworkName; nodes: LuxNode[] }) {
if (nodes.length === 0) {
return (
<p className="p-6 text-center text-sm text-muted-foreground">
No luxd pods reported for {NETWORK_LABEL[network]}.
</p>
)
}
return (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Node</TableHead>
<TableHead>Node ID</TableHead>
<TableHead>Version</TableHead>
<TableHead className="text-right">Peers</TableHead>
<TableHead className="text-right">C-Chain</TableHead>
<TableHead className="text-center">C / P / X / D</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{nodes.map((n) => {
const c = n.chains.find((x) => x.chain === "C")
return (
<TableRow key={n.pod_name}>
<TableCell>
<div className="flex items-center gap-2">
{healthDot(nodeHealth(n))}
<span className="font-mono text-xs">{n.pod_name}</span>
{!n.rpc_reachable && n.ready ? (
<span className="text-[10px] text-muted-foreground">(internal RPC)</span>
) : null}
</div>
</TableCell>
<TableCell>
<span className="font-mono text-[11px] text-muted-foreground">
{n.node_id ? `${n.node_id.slice(0, 18)}…` : "—"}
</span>
</TableCell>
<TableCell>
<span className="font-mono text-xs">{n.version ?? "—"}</span>
</TableCell>
<TableCell className="text-right font-mono text-sm">{n.peers ?? "—"}</TableCell>
<TableCell className="text-right font-mono text-sm">
{c?.height != null ? c.height.toLocaleString() : "—"}
</TableCell>
<TableCell>
<div className="flex items-center justify-center gap-1">
{TRACKED_CHAINS.map((ch) => {
const cc = n.chains.find((x) => x.chain === ch)
const cls =
cc?.bootstrapped === true
? "bg-green-500"
: cc?.bootstrapped === false
? "bg-yellow-500"
: "bg-zinc-700"
return (
<span
key={ch}
title={`${CHAIN_LABEL[ch]}: ${
cc?.bootstrapped === true
? "bootstrapped"
: cc?.bootstrapped === false
? "syncing"
: "unknown"
}`}
className={`inline-block h-2.5 w-2.5 rounded-sm ${cls}`}
/>
)
})}
</div>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)
}
function NetworkCard({ fleet }: { fleet: LuxNetworkFleet }) {
const [open, setOpen] = useState(false)
const status = fleetStatus(fleet)
const sb = STATUS_BADGE[status]
const cHeight = bestHeight(fleet, "C")
const dexBoot = fleet.dex.d_chain_bootstrapped ?? chainBootstrapped(fleet, "D")
return (
<Card className="border-zinc-800">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => setOpen((v) => !v)}
aria-label={open ? "Collapse nodes" : "Expand nodes"}
>
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
<div>
<CardTitle className="flex items-center gap-2">
{NETWORK_LABEL[fleet.network]}
<Badge variant="outline" className={NETWORK_ACCENT[fleet.network]}>
network {fleet.network_id ?? "—"}
</Badge>
</CardTitle>
<CardDescription className="mt-1 font-mono text-xs">
{fleet.version ?? fleet.image ?? "version unknown"}
</CardDescription>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline" className={sb.cls}>
{status === "operational" ? (
<Circle className="mr-1 h-2 w-2 fill-green-500" />
) : status === "degraded" ? (
<AlertTriangle className="mr-1 h-3 w-3" />
) : (
<Circle className="mr-1 h-2 w-2 fill-red-500" />
)}
{sb.label}
</Badge>
<div className="text-right text-sm">
<span className="font-bold">{fleet.nodes_ready}</span>
<span className="text-muted-foreground">/{fleet.validators || fleet.nodes_total} validators</span>
</div>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Quick facts row */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Fact icon={Server} label="Nodes ready" value={`${fleet.nodes_ready}/${fleet.nodes_total}`} />
<Fact icon={Users} label="Peers" value={fleet.total_peers} />
<Fact icon={Activity} label="C-Chain height" value={cHeight != null ? cHeight.toLocaleString() : "—"} />
<Fact
icon={Cpu}
label="DEX (D-Chain)"
value={dexBoot === true ? "Live" : dexBoot === false ? "Syncing" : "—"}
valueClass={dexBoot === true ? "text-green-400" : dexBoot === false ? "text-yellow-400" : ""}
/>
</div>
{/* Plugin set + chains bootstrapped */}
<div className="flex flex-wrap items-center gap-2 border-t border-zinc-800 pt-3">
{fleet.plugin_set ? (
<Badge variant="secondary" className="font-mono text-[10px]">
<Package className="mr-1 h-3 w-3" />
{fleet.plugin_set.replace(/^s3:\/\//, "")}
</Badge>
) : null}
{TRACKED_CHAINS.map((ch) => {
const b = chainBootstrapped(fleet, ch)
return (
<span key={ch} className="flex items-center gap-1 text-xs">
<span
className={`inline-block h-2 w-2 rounded-full ${
b === true ? "bg-green-500" : b === false ? "bg-yellow-500" : "bg-zinc-700"
}`}
/>
<span className="text-muted-foreground">{CHAIN_LABEL[ch]}</span>
</span>
)
})}
{fleet.dex.markets != null ? (
<Badge variant="outline" className="text-[10px]">
{fleet.dex.markets} markets
</Badge>
) : null}
</div>
{fleet.error ? (
<div className="rounded-md bg-yellow-500/10 p-3 text-xs text-yellow-500">{fleet.error}</div>
) : null}
{open ? (
<div className="border-t border-zinc-800 pt-3">
<NodeTable network={fleet.network} nodes={fleet.nodes} />
</div>
) : null}
</CardContent>
</Card>
)
}
function Fact({
icon: Icon,
label,
value,
valueClass,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
valueClass?: string
}) {
return (
<div className="rounded-lg border border-zinc-800 p-3">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Icon className="h-3.5 w-3.5" />
{label}
</div>
<div className={`mt-1 font-mono text-lg font-semibold ${valueClass ?? ""}`}>{value}</div>
</div>
)
}
/** Build map regions from the fleet (all luxd nodes shown at the sfo3 region). */
function toRegions(overview: LuxFleetOverview | null): FleetRegion[] {
if (!overview) return []
const total = overview.networks.reduce((s, f) => s + f.nodes_total, 0)
const healthy = overview.networks.reduce((s, f) => s + f.nodes_ready, 0)
if (total === 0) return []
// The managed fleet runs in DigitalOcean SFO3 (San Francisco).
return [
{
slug: "sfo3",
name: "SFO3 · San Francisco",
lat: 37.77,
lon: -122.42,
active: healthy > 0,
nodeCount: total,
healthyNodes: healthy,
networks: overview.networks.map((f) => f.network),
},
]
}
export function FleetDashboard({ compact = false }: { compact?: boolean }) {
const [overview, setOverview] = useState<LuxFleetOverview | null>(null)
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState<string | null>(null)
const [updatedAt, setUpdatedAt] = useState<Date | null>(null)
const load = useCallback(async (silent = false) => {
if (!silent) setRefreshing(true)
const ctrl = new AbortController()
try {
const data = await fetchFleetOverview(ctrl.signal)
setOverview(data)
setError(null)
setUpdatedAt(new Date())
} catch (e) {
setError(String(e))
} finally {
setLoading(false)
setRefreshing(false)
}
}, [])
useEffect(() => {
load()
const t = setInterval(() => load(true), REFRESH_MS)
return () => clearInterval(t)
}, [load])
const totals = overview?.totals ?? {}
const regions = useMemo(() => toRegions(overview), [overview])
if (loading && !overview) {
return (
<div className="flex items-center justify-center p-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-2xl font-bold tracking-tight">Node Fleet</h2>
<p className="text-sm text-muted-foreground">
Live status of the managed luxd fleet across devnet, testnet and mainnet
{updatedAt ? ` · updated ${updatedAt.toLocaleTimeString()}` : ""}
</p>
</div>
<Button variant="outline" size="sm" onClick={() => load()} disabled={refreshing}>
<RefreshCcw className={`mr-2 h-4 w-4 ${refreshing ? "animate-spin" : ""}`} />
Refresh
</Button>
</div>
{error && !overview ? (
<Card>
<CardContent className="p-6">
<div className="rounded-md bg-destructive/10 p-4 text-sm text-destructive">{error}</div>
</CardContent>
</Card>
) : null}
{/* Totals */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<StatNumber icon={NetworkIcon} label="Networks" value={totals.networks ?? overview?.networks.length} />
<StatNumber icon={Server} label="Validators" value={totals.validators} />
<StatNumber
icon={Heart}
label="Nodes ready"
value={totals.nodes_ready}
suffix={totals.nodes_total ? ` / ${totals.nodes_total}` : undefined}
accent="text-green-500"
/>
<StatNumber icon={Users} label="Peer connections" value={totals.total_peers} />
</div>
{/* World map (only when not compact) */}
{!compact && regions.length > 0 ? (
<Card className="border-zinc-800 overflow-hidden">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Fleet topology</CardTitle>
</CardHeader>
<CardContent className="p-0">
<FleetMap regions={regions} />
</CardContent>
</Card>
) : null}
{/* Per-network cards */}
<div className="space-y-4">
{overview?.networks.map((f) => (
<NetworkCard key={f.network} fleet={f} />
))}
</div>
</div>
)
}