"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 } 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([]) const [events, setEvents] = useState([]) const [health, setHealth] = useState([]) const [selectedLog, setSelectedLog] = useState(null) const [selectedEvent, setSelectedEvent] = useState(null) // Shell const [shellInput, setShellInput] = useState("") const [shellHistory, setShellHistory] = useState([]) const shellEndRef = useRef(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 - 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 ( ) } // ─── Panel ───────────────────────────────────────── const panelHeight = expanded ? "100vh" : `${height}px` return (
{/* Drag Handle */} {!expanded && (
)} {/* Header */}
Overview Logs Events Health Shell
{/* ─── Overview Tab ─────────────────────── */}

API Requests

{overviewStats.totalRequests}

this session

Error Rate

5 ? "text-red-400" : "text-green-400"}`}> {overviewStats.errorRate.toFixed(1)}%

of requests

Avg Latency

{overviewStats.avgLatency}ms

response time

Health

{health.every(h => h.status === "healthy") ? ( <> All healthy ) : ( <> {overviewStats.activeAlerts} issues )}

Quick Actions

{/* ─── Logs Tab ─────────────────────────── */}
{logs.length === 0 ? (

No logs yet

API request logs will appear here as requests are made

Try running commands in the Shell tab

) : ( {logs.map((log) => ( setSelectedLog(log)} > ))}
Status Method Path Latency Time
{log.status} {log.status < 300 ? "OK" : log.status < 500 ? "ERR" : "FAIL"} {log.method} {log.path} {log.latency_ms}ms {timeAgo(log.timestamp)}
)}
{selectedLog && (

Request Details

Method

{selectedLog.method} {selectedLog.path}

Status

{selectedLog.status}

Request ID

{selectedLog.request_id}

Latency

{selectedLog.latency_ms}ms

Time

{new Date(selectedLog.timestamp).toLocaleString()}

{selectedLog.response && (
Response
                          {selectedLog.response}
                        
)}
)}
{/* ─── Events Tab ───────────────────────── */}
{events.length === 0 ? (

No events yet

Webhook events and chain events will appear here

) : (
{events.map((event) => (
setSelectedEvent(event)} >
{event.type} {timeAgo(event.timestamp)}

{event.description}

))}
)}
{selectedEvent && (

Event

Type

{selectedEvent.type}

Event ID

{selectedEvent.id}

Source

{selectedEvent.source}

Time

{new Date(selectedEvent.timestamp).toLocaleString()}

{selectedEvent.data && (
Event Data
                          {JSON.stringify(selectedEvent.data, null, 2)}
                        
)}
)}
{/* ─── Health Tab ────────────────────────── */}

All

{health.length}

Critical

{health.filter(h => h.status === "down").length}

Degraded

{health.filter(h => h.status === "degraded").length}

Healthy

{health.filter(h => h.status === "healthy").length}

{health.length === 0 ? (

Checking services...

) : (
{health.map((check) => ( ))}
Service Status Latency Details
{check.service}
{check.status === "healthy" ? ( ) : check.status === "degraded" ? ( ) : ( )} {check.status}
{check.latency_ms > 0 ? `${check.latency_ms}ms` : "—"} {check.details || "—"}
)}
{/* ─── Shell Tab ─────────────────────────── */}
{shellHistory.length === 0 && (
{`
 ____              _                   _
| __ )  ___   ___ | |_ _ __   ___   __| | ___
|  _ \\ / _ \\ / _ \\| __| '_ \\ / _ \\ / _\` |/ _ \\
| |_) | (_) | (_) | |_| | | | (_) | (_| |  __/
|____/ \\___/ \\___/ \\__|_| |_|\\___/ \\__,_|\\___|
`}

Welcome to Bootnode Shell!

Type help to see available commands.

Use curl /v1/health to test API endpoints.

)} {shellHistory.map((item, i) => (
$ {item.command}
                    {item.output}
                  
))}
$ 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"} />
) }