Files
bootnode/console/components/developer-workbench.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

678 lines
31 KiB
TypeScript

"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Activity,
AlertTriangle,
CheckCircle,
ChevronDown,
ChevronUp,
Code2,
GripHorizontal,
Maximize2,
Minimize2,
RefreshCcw,
Send,
Terminal,
X,
XCircle,
} from "@luxfi/ui/icons"
import { getApiUrl, getAuthHeaders as getAuth } from "@/lib/api"
// ─── Types ─────────────────────────────────────────────
interface LogEntry {
id: string
status: number
method: string
path: string
latency_ms: number
timestamp: string
request_id: string
ip?: string
body?: string
response?: string
}
interface EventEntry {
id: string
type: string
description: string
timestamp: string
source: string
api_version?: string
data?: Record<string, unknown>
}
interface HealthStatus {
service: string
status: "healthy" | "degraded" | "down"
latency_ms: number
details?: string
}
interface ShellHistoryItem {
command: string
output: string
timestamp: string
error?: boolean
}
// ─── Helpers ───────────────────────────────────────────
const getAPIURL = () => getApiUrl()
const getAuthHeaders = () => getAuth()
function statusColor(status: number): string {
if (status < 300) return "text-green-400"
if (status < 400) return "text-yellow-400"
return "text-red-400"
}
function statusBg(status: number): string {
if (status < 300) return "bg-green-500/20 text-green-400 border-green-500/30"
if (status < 400) return "bg-yellow-500/20 text-yellow-400 border-yellow-500/30"
return "bg-red-500/20 text-red-400 border-red-500/30"
}
function timeAgo(timestamp: string): string {
const diff = Date.now() - new Date(timestamp).getTime()
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
return new Date(timestamp).toLocaleTimeString()
}
// ─── Component ─────────────────────────────────────────
export function DeveloperWorkbench() {
const [open, setOpen] = useState(false)
const [expanded, setExpanded] = useState(false)
const [height, setHeight] = useState(350)
const [tab, setTab] = useState("overview")
const [refreshing, setRefreshing] = useState(false)
// Data
const [logs, setLogs] = useState<LogEntry[]>([])
const [events, setEvents] = useState<EventEntry[]>([])
const [health, setHealth] = useState<HealthStatus[]>([])
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null)
const [selectedEvent, setSelectedEvent] = useState<EventEntry | null>(null)
// Shell
const [shellInput, setShellInput] = useState("")
const [shellHistory, setShellHistory] = useState<ShellHistoryItem[]>([])
const shellEndRef = useRef<HTMLDivElement>(null)
// Overview stats
const [overviewStats, setOverviewStats] = useState({
totalRequests: 0,
errorRate: 0,
avgLatency: 0,
activeAlerts: 0,
})
// Drag resize
const dragRef = useRef<{ startY: number; startHeight: number } | null>(null)
const handleMouseDown = useCallback((e: React.MouseEvent) => {
dragRef.current = { startY: e.clientY, startHeight: height }
const onMove = (ev: MouseEvent) => {
if (!dragRef.current) return
const delta = dragRef.current.startY - ev.clientY
const newHeight = Math.max(200, Math.min(window.innerHeight - 100, dragRef.current.startHeight + delta))
setHeight(newHeight)
}
const onUp = () => {
dragRef.current = null
document.removeEventListener("mousemove", onMove)
document.removeEventListener("mouseup", onUp)
}
document.addEventListener("mousemove", onMove)
document.addEventListener("mouseup", onUp)
}, [height])
// ─── Data Fetching ─────────────────────────────────
const fetchData = useCallback(async () => {
const headers = getAuthHeaders()
// Fetch health
try {
const res = await fetch(`${getAPIURL()}/health`)
const data = res.ok ? await res.json() : null
const checks: HealthStatus[] = [{
service: "Bootnode API",
status: data?.status === "healthy" ? "healthy" : "down",
latency_ms: 0,
details: data ? `v${data.version}` : "Unreachable",
}]
// Check nodes
const nodesRes = await fetch(`${getAPIURL()}/v1/nodes/`, { headers }).catch(() => null)
if (nodesRes?.ok) {
const nodes = await nodesRes.json()
if (Array.isArray(nodes)) {
for (const node of nodes.slice(0, 5)) {
checks.push({
service: `${node.name}`,
status: node.status === "running" ? "healthy" : "down",
latency_ms: 0,
details: `${node.chain}/${node.network}`,
})
}
}
}
setHealth(checks)
// Build overview stats
setOverviewStats({
totalRequests: logs.length,
errorRate: logs.length > 0 ? (logs.filter(l => l.status >= 400).length / logs.length) * 100 : 0,
avgLatency: logs.length > 0 ? Math.round(logs.reduce((a, l) => a + l.latency_ms, 0) / logs.length) : 0,
activeAlerts: checks.filter(c => c.status !== "healthy").length,
})
} catch {
setHealth([{
service: "Bootnode API",
status: "down",
latency_ms: 0,
details: "Connection refused",
}])
}
}, [logs])
useEffect(() => {
if (!open) return
fetchData()
const interval = setInterval(fetchData, 10000)
return () => clearInterval(interval)
}, [open, fetchData])
const handleRefresh = useCallback(() => {
setRefreshing(true)
fetchData().finally(() => setRefreshing(false))
}, [fetchData])
// ─── Shell ─────────────────────────────────────────
async function handleShellSubmit(e: React.FormEvent) {
e.preventDefault()
if (!shellInput.trim()) return
const cmd = shellInput.trim()
setShellInput("")
try {
let output = ""
// Parse simple bootnode commands
if (cmd === "help" || cmd === "bootnode help") {
output = `Available commands:
bootnode chains list - List supported chains
bootnode nodes list - List deployed nodes
bootnode keys list - List API keys
bootnode health - Check API health
bootnode webhooks list - List webhooks
curl <endpoint> - Make API request`
} else if (cmd === "bootnode health" || cmd === "health") {
const res = await fetch(`${getAPIURL()}/health`)
output = JSON.stringify(await res.json(), null, 2)
} else if (cmd === "bootnode chains list" || cmd === "chains") {
const res = await fetch(`${getAPIURL()}/v1/chains`, { headers: getAuthHeaders() })
const data = res.ok ? await res.json() : { error: res.statusText }
output = JSON.stringify(data, null, 2)
} else if (cmd === "bootnode nodes list" || cmd === "nodes") {
const res = await fetch(`${getAPIURL()}/v1/nodes/`, { headers: getAuthHeaders() })
const data = res.ok ? await res.json() : { error: res.statusText }
output = JSON.stringify(data, null, 2)
} else if (cmd === "bootnode keys list" || cmd === "keys") {
const res = await fetch(`${getAPIURL()}/v1/auth/keys`, { headers: getAuthHeaders() })
const data = res.ok ? await res.json() : { error: res.statusText }
output = JSON.stringify(data, null, 2)
} else if (cmd === "bootnode webhooks list" || cmd === "webhooks") {
const res = await fetch(`${getAPIURL()}/v1/webhooks`, { headers: getAuthHeaders() })
const data = res.ok ? await res.json() : { error: res.statusText }
output = JSON.stringify(data, null, 2)
} else if (cmd.startsWith("curl ")) {
const endpoint = cmd.replace("curl ", "").trim()
const url = endpoint.startsWith("http") ? endpoint : `${getAPIURL()}${endpoint.startsWith("/") ? "" : "/"}${endpoint}`
const res = await fetch(url, { headers: getAuthHeaders() })
const data = await res.json().catch(() => res.text())
output = typeof data === "string" ? data : JSON.stringify(data, null, 2)
} else {
output = `Unknown command: ${cmd}\nType "help" for available commands.`
}
setShellHistory(prev => [...prev, { command: cmd, output, timestamp: new Date().toISOString() }])
} catch (err) {
setShellHistory(prev => [...prev, {
command: cmd,
output: `Error: ${err instanceof Error ? err.message : String(err)}`,
timestamp: new Date().toISOString(),
error: true,
}])
}
setTimeout(() => shellEndRef.current?.scrollIntoView({ behavior: "smooth" }), 50)
}
// ─── Toggle Button (when closed) ──────────────────
if (!open) {
return (
<button
onClick={() => setOpen(true)}
className="fixed bottom-0 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 rounded-t-lg border border-b-0 border-zinc-700 bg-zinc-900 px-4 py-1.5 text-sm text-zinc-300 shadow-lg transition hover:bg-zinc-800 hover:text-white"
>
<Code2 className="h-4 w-4" />
Developers
</button>
)
}
// ─── Panel ─────────────────────────────────────────
const panelHeight = expanded ? "100vh" : `${height}px`
return (
<div
className="fixed bottom-0 left-0 right-0 z-50 flex flex-col border-t border-zinc-700 bg-zinc-950 text-zinc-100 shadow-2xl"
style={{ height: panelHeight }}
>
{/* Drag Handle */}
{!expanded && (
<div
className="flex h-2 cursor-row-resize items-center justify-center hover:bg-zinc-800"
onMouseDown={handleMouseDown}
>
<GripHorizontal className="h-4 w-4 text-zinc-600" />
</div>
)}
{/* Header */}
<div className="flex items-center justify-between border-b border-zinc-800 px-4 py-1">
<Tabs value={tab} onValueChange={setTab} className="flex-1">
<div className="flex items-center justify-between">
<TabsList className="h-8 bg-transparent p-0 gap-0">
<TabsTrigger
value="overview"
className="h-8 rounded-none border-b-2 border-transparent px-3 text-xs data-[state=active]:border-white data-[state=active]:bg-transparent data-[state=active]:text-white text-zinc-400"
>
Overview
</TabsTrigger>
<TabsTrigger
value="logs"
className="h-8 rounded-none border-b-2 border-transparent px-3 text-xs data-[state=active]:border-white data-[state=active]:bg-transparent data-[state=active]:text-white text-zinc-400"
>
Logs
</TabsTrigger>
<TabsTrigger
value="events"
className="h-8 rounded-none border-b-2 border-transparent px-3 text-xs data-[state=active]:border-white data-[state=active]:bg-transparent data-[state=active]:text-white text-zinc-400"
>
Events
</TabsTrigger>
<TabsTrigger
value="health"
className="h-8 rounded-none border-b-2 border-transparent px-3 text-xs data-[state=active]:border-white data-[state=active]:bg-transparent data-[state=active]:text-white text-zinc-400"
>
Health
</TabsTrigger>
<TabsTrigger
value="shell"
className="h-8 rounded-none border-b-2 border-transparent px-3 text-xs data-[state=active]:border-white data-[state=active]:bg-transparent data-[state=active]:text-white text-zinc-400"
>
Shell
</TabsTrigger>
</TabsList>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7 text-zinc-400 hover:text-white hover:bg-zinc-800" onClick={handleRefresh}>
<RefreshCcw className={`h-3.5 w-3.5 ${refreshing ? "animate-spin" : ""}`} />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-zinc-400 hover:text-white hover:bg-zinc-800" onClick={() => setExpanded(!expanded)}>
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-zinc-400 hover:text-white hover:bg-zinc-800" onClick={() => { setOpen(false); setExpanded(false) }}>
<X className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* ─── Overview Tab ─────────────────────── */}
<TabsContent value="overview" className="mt-0 flex-1 overflow-auto p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<p className="text-xs text-zinc-500 mb-1">API Requests</p>
<p className="text-2xl font-bold">{overviewStats.totalRequests}</p>
<p className="text-xs text-zinc-500">this session</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<p className="text-xs text-zinc-500 mb-1">Error Rate</p>
<p className={`text-2xl font-bold ${overviewStats.errorRate > 5 ? "text-red-400" : "text-green-400"}`}>
{overviewStats.errorRate.toFixed(1)}%
</p>
<p className="text-xs text-zinc-500">of requests</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<p className="text-xs text-zinc-500 mb-1">Avg Latency</p>
<p className="text-2xl font-bold">{overviewStats.avgLatency}ms</p>
<p className="text-xs text-zinc-500">response time</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<p className="text-xs text-zinc-500 mb-1">Health</p>
<div className="flex items-center gap-2">
{health.every(h => h.status === "healthy") ? (
<>
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">All healthy</span>
</>
) : (
<>
<AlertTriangle className="h-5 w-5 text-yellow-500" />
<span className="text-yellow-400 font-medium">{overviewStats.activeAlerts} issues</span>
</>
)}
</div>
</div>
</div>
<div className="mt-4 rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<h3 className="text-sm font-medium mb-3">Quick Actions</h3>
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" className="border-zinc-700 text-zinc-300 hover:bg-zinc-800" onClick={() => setTab("logs")}>
View Logs
</Button>
<Button size="sm" variant="outline" className="border-zinc-700 text-zinc-300 hover:bg-zinc-800" onClick={() => setTab("health")}>
Health Check
</Button>
<Button size="sm" variant="outline" className="border-zinc-700 text-zinc-300 hover:bg-zinc-800" onClick={() => setTab("shell")}>
Open Shell
</Button>
<Button size="sm" variant="outline" className="border-zinc-700 text-zinc-300 hover:bg-zinc-800" onClick={() => window.open(`${getAPIURL()}/docs`, "_blank")}>
API Docs
</Button>
</div>
</div>
</TabsContent>
{/* ─── Logs Tab ─────────────────────────── */}
<TabsContent value="logs" className="mt-0 flex-1 overflow-auto">
<div className="flex h-full">
<div className={`flex-1 overflow-auto ${selectedLog ? "border-r border-zinc-800" : ""}`}>
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-zinc-500">
<Activity className="h-8 w-8 mb-3 opacity-50" />
<p className="text-sm font-medium">No logs yet</p>
<p className="text-xs mt-1">API request logs will appear here as requests are made</p>
<p className="text-xs mt-3 text-zinc-600">Try running commands in the Shell tab</p>
</div>
) : (
<table className="w-full text-xs">
<thead className="sticky top-0 bg-zinc-950 border-b border-zinc-800">
<tr>
<th className="px-3 py-2 text-left text-zinc-500 font-medium">Status</th>
<th className="px-3 py-2 text-left text-zinc-500 font-medium">Method</th>
<th className="px-3 py-2 text-left text-zinc-500 font-medium">Path</th>
<th className="px-3 py-2 text-right text-zinc-500 font-medium">Latency</th>
<th className="px-3 py-2 text-right text-zinc-500 font-medium">Time</th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr
key={log.id}
className={`cursor-pointer border-b border-zinc-900 hover:bg-zinc-900/50 ${selectedLog?.id === log.id ? "bg-zinc-900" : ""}`}
onClick={() => setSelectedLog(log)}
>
<td className="px-3 py-2">
<span className={`inline-flex items-center rounded border px-1.5 py-0.5 font-mono text-[10px] font-medium ${statusBg(log.status)}`}>
{log.status} {log.status < 300 ? "OK" : log.status < 500 ? "ERR" : "FAIL"}
</span>
</td>
<td className="px-3 py-2 font-mono text-zinc-300">{log.method}</td>
<td className="px-3 py-2 font-mono text-zinc-400 max-w-[300px] truncate">{log.path}</td>
<td className="px-3 py-2 text-right font-mono text-zinc-400">{log.latency_ms}ms</td>
<td className="px-3 py-2 text-right text-zinc-500">{timeAgo(log.timestamp)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{selectedLog && (
<div className="w-96 overflow-auto p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium">Request Details</h3>
<Button variant="ghost" size="icon" className="h-6 w-6 text-zinc-400" onClick={() => setSelectedLog(null)}>
<X className="h-3.5 w-3.5" />
</Button>
</div>
<div className="space-y-3 text-xs">
<div>
<span className="text-zinc-500">Method</span>
<p className="font-mono text-zinc-200">{selectedLog.method} {selectedLog.path}</p>
</div>
<div>
<span className="text-zinc-500">Status</span>
<p className={statusColor(selectedLog.status)}>{selectedLog.status}</p>
</div>
<div>
<span className="text-zinc-500">Request ID</span>
<p className="font-mono text-zinc-400">{selectedLog.request_id}</p>
</div>
<div>
<span className="text-zinc-500">Latency</span>
<p className="font-mono text-zinc-300">{selectedLog.latency_ms}ms</p>
</div>
<div>
<span className="text-zinc-500">Time</span>
<p className="text-zinc-300">{new Date(selectedLog.timestamp).toLocaleString()}</p>
</div>
{selectedLog.response && (
<div>
<span className="text-zinc-500">Response</span>
<pre className="mt-1 rounded bg-zinc-900 p-2 text-[10px] text-zinc-400 overflow-auto max-h-48">
{selectedLog.response}
</pre>
</div>
)}
</div>
</div>
)}
</div>
</TabsContent>
{/* ─── Events Tab ───────────────────────── */}
<TabsContent value="events" className="mt-0 flex-1 overflow-auto">
<div className="flex h-full">
<div className={`flex-1 overflow-auto ${selectedEvent ? "border-r border-zinc-800" : ""}`}>
{events.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-zinc-500">
<Activity className="h-8 w-8 mb-3 opacity-50" />
<p className="text-sm font-medium">No events yet</p>
<p className="text-xs mt-1">Webhook events and chain events will appear here</p>
</div>
) : (
<div className="divide-y divide-zinc-900">
{events.map((event) => (
<div
key={event.id}
className={`px-4 py-3 cursor-pointer hover:bg-zinc-900/50 ${selectedEvent?.id === event.id ? "bg-zinc-900" : ""}`}
onClick={() => setSelectedEvent(event)}
>
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-zinc-200">{event.type}</span>
<span className="text-[10px] text-zinc-500">{timeAgo(event.timestamp)}</span>
</div>
<p className="text-xs text-zinc-500 mt-0.5">{event.description}</p>
</div>
))}
</div>
)}
</div>
{selectedEvent && (
<div className="w-96 overflow-auto p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium">Event</h3>
<Button variant="ghost" size="icon" className="h-6 w-6 text-zinc-400" onClick={() => setSelectedEvent(null)}>
<X className="h-3.5 w-3.5" />
</Button>
</div>
<div className="space-y-3 text-xs">
<div>
<span className="text-zinc-500">Type</span>
<p className="font-mono text-zinc-200">{selectedEvent.type}</p>
</div>
<div>
<span className="text-zinc-500">Event ID</span>
<p className="font-mono text-zinc-400">{selectedEvent.id}</p>
</div>
<div>
<span className="text-zinc-500">Source</span>
<p className="text-zinc-300">{selectedEvent.source}</p>
</div>
<div>
<span className="text-zinc-500">Time</span>
<p className="text-zinc-300">{new Date(selectedEvent.timestamp).toLocaleString()}</p>
</div>
{selectedEvent.data && (
<div>
<span className="text-zinc-500">Event Data</span>
<pre className="mt-1 rounded bg-zinc-900 p-2 text-[10px] text-zinc-400 overflow-auto max-h-48">
{JSON.stringify(selectedEvent.data, null, 2)}
</pre>
</div>
)}
</div>
</div>
)}
</div>
</TabsContent>
{/* ─── Health Tab ────────────────────────── */}
<TabsContent value="health" className="mt-0 flex-1 overflow-auto p-4">
<div className="grid grid-cols-4 gap-3 mb-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900 p-3 text-center">
<p className="text-xs text-zinc-500">All</p>
<p className="text-lg font-bold">{health.length}</p>
</div>
<div className="rounded-lg border border-red-900/30 bg-red-950/20 p-3 text-center">
<p className="text-xs text-zinc-500">Critical</p>
<p className="text-lg font-bold text-red-400">{health.filter(h => h.status === "down").length}</p>
</div>
<div className="rounded-lg border border-yellow-900/30 bg-yellow-950/20 p-3 text-center">
<p className="text-xs text-zinc-500">Degraded</p>
<p className="text-lg font-bold text-yellow-400">{health.filter(h => h.status === "degraded").length}</p>
</div>
<div className="rounded-lg border border-green-900/30 bg-green-950/20 p-3 text-center">
<p className="text-xs text-zinc-500">Healthy</p>
<p className="text-lg font-bold text-green-400">{health.filter(h => h.status === "healthy").length}</p>
</div>
</div>
{health.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-zinc-500">
<CheckCircle className="h-8 w-8 mb-3 opacity-50" />
<p className="text-sm">Checking services...</p>
</div>
) : (
<div className="rounded-lg border border-zinc-800 overflow-hidden">
<table className="w-full text-xs">
<thead className="bg-zinc-900">
<tr>
<th className="px-4 py-2 text-left text-zinc-500 font-medium">Service</th>
<th className="px-4 py-2 text-left text-zinc-500 font-medium">Status</th>
<th className="px-4 py-2 text-right text-zinc-500 font-medium">Latency</th>
<th className="px-4 py-2 text-left text-zinc-500 font-medium">Details</th>
</tr>
</thead>
<tbody>
{health.map((check) => (
<tr key={check.service} className="border-t border-zinc-800">
<td className="px-4 py-2 font-medium text-zinc-200">{check.service}</td>
<td className="px-4 py-2">
<div className="flex items-center gap-1.5">
{check.status === "healthy" ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
) : check.status === "degraded" ? (
<AlertTriangle className="h-3.5 w-3.5 text-yellow-500" />
) : (
<XCircle className="h-3.5 w-3.5 text-red-500" />
)}
<span className="capitalize">{check.status}</span>
</div>
</td>
<td className="px-4 py-2 text-right font-mono text-zinc-400">
{check.latency_ms > 0 ? `${check.latency_ms}ms` : "—"}
</td>
<td className="px-4 py-2 text-zinc-500">{check.details || "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</TabsContent>
{/* ─── Shell Tab ─────────────────────────── */}
<TabsContent value="shell" className="mt-0 flex flex-col flex-1 overflow-hidden">
<div className="flex-1 overflow-auto p-4 font-mono text-xs">
{shellHistory.length === 0 && (
<div className="text-zinc-600 mb-4">
<pre className="text-zinc-500 mb-2">{`
____ _ _
| __ ) ___ ___ | |_ _ __ ___ __| | ___
| _ \\ / _ \\ / _ \\| __| '_ \\ / _ \\ / _\` |/ _ \\
| |_) | (_) | (_) | |_| | | | (_) | (_| | __/
|____/ \\___/ \\___/ \\__|_| |_|\\___/ \\__,_|\\___|
`}</pre>
<p>Welcome to Bootnode Shell!</p>
<p>Type <span className="text-zinc-300">help</span> to see available commands.</p>
<p>Use <span className="text-zinc-300">curl /v1/health</span> to test API endpoints.</p>
</div>
)}
{shellHistory.map((item, i) => (
<div key={i} className="mb-3">
<div className="flex items-center gap-2 text-zinc-300">
<span className="text-green-500">$</span>
<span>{item.command}</span>
</div>
<pre className={`mt-1 whitespace-pre-wrap ${item.error ? "text-red-400" : "text-zinc-500"}`}>
{item.output}
</pre>
</div>
))}
<div ref={shellEndRef} />
</div>
<form
onSubmit={handleShellSubmit}
className="flex items-center gap-2 border-t border-zinc-800 px-4 py-2"
>
<span className="text-green-500 text-xs font-mono">$</span>
<Input
value={shellInput}
onChange={(e) => setShellInput(e.target.value)}
placeholder="Enter a command..."
className="flex-1 h-8 border-0 bg-transparent text-xs font-mono text-zinc-200 placeholder:text-zinc-600 focus-visible:ring-0 focus-visible:ring-offset-0"
autoFocus={tab === "shell"}
/>
<Button type="submit" variant="ghost" size="icon" className="h-7 w-7 text-zinc-500 hover:text-white">
<Send className="h-3.5 w-3.5" />
</Button>
</form>
</TabsContent>
</Tabs>
</div>
</div>
)
}