Files
zeekayandHanzo Dev 49a1af040e Merge remote-tracking branch 'origin/main'
# Conflicts:
#	console/package.json
#	console/pnpm-lock.yaml
#	console/pnpm-workspace.yaml

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 03:05:54 -07:00

452 lines
17 KiB
TypeScript

"use client"
import { useEffect, 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Activity,
AlertTriangle,
CheckCircle,
Clock,
Loader2,
RefreshCcw,
Server,
Wifi,
XCircle,
Zap,
} from "@luxfi/ui/icons"
import { getApiUrl, getAuthHeaders } from "@/lib/api"
interface HealthCheck {
service: string
status: "healthy" | "degraded" | "down"
latency_ms: number
last_check: string
uptime_pct: number
details?: string
}
interface Alert {
id: string
severity: "critical" | "warning" | "info"
service: string
message: string
started_at: string
resolved_at: string | null
}
interface RPCLog {
id: string
method: string
chain: string
status: number
latency_ms: number
timestamp: string
request_id: string
}
export default function MonitoringPage() {
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [healthChecks, setHealthChecks] = useState<HealthCheck[]>([])
const [alerts, setAlerts] = useState<Alert[]>([])
const [rpcLogs, setRpcLogs] = useState<RPCLog[]>([])
async function fetchMonitoringData() {
const apiBase = getApiUrl()
const headers = getAuthHeaders()
try {
const [healthRes, nodesRes] = await Promise.all([
fetch(`${apiBase}/health`).catch(() => null),
fetch(`${apiBase}/v1/nodes/`, { headers }).catch(() => null),
])
// Build health checks from available services
const checks: HealthCheck[] = []
// API health
if (healthRes?.ok) {
const health = await healthRes.json()
checks.push({
service: "Bootnode API",
status: health.status === "healthy" ? "healthy" : "degraded",
latency_ms: 0,
last_check: new Date().toISOString(),
uptime_pct: 99.9,
details: `v${health.version || "2.0.0"}`,
})
} else {
checks.push({
service: "Bootnode API",
status: "down",
latency_ms: 0,
last_check: new Date().toISOString(),
uptime_pct: 0,
details: "Unreachable",
})
}
// Node health from running nodes.
//
// A failed fetch is NOT "you have no nodes". On a health page that
// distinction is the whole product: silently emitting [] made an
// unreachable API look like an empty fleet. The API check above already
// gets this right (it pushes an explicit "down" row), so do the same here
// rather than dropping the section.
if (!nodesRes?.ok) {
checks.push({
service: "Nodes",
status: "down",
latency_ms: 0,
last_check: new Date().toISOString(),
uptime_pct: 0,
details: nodesRes ? `Unreachable (HTTP ${nodesRes.status})` : "Unreachable",
})
}
const nodesData = nodesRes?.ok ? await nodesRes.json() : []
if (Array.isArray(nodesData)) {
for (const node of nodesData) {
checks.push({
service: `${node.name} (${node.chain}/${node.network})`,
status: node.status === "healthy" ? "healthy" : node.status === "starting" ? "degraded" : "down",
latency_ms: node.metrics?.latency_ms || 0,
last_check: new Date().toISOString(),
uptime_pct: node.status === "healthy" ? 99.5 : 0,
details: node.status,
})
}
}
// Add infrastructure services
const infraServices = [
{ name: "PostgreSQL", endpoint: `${apiBase}/health` },
{ name: "Redis Cache", endpoint: `${apiBase}/health` },
{ name: "IAM (hanzo.id)", endpoint: "https://iam.hanzo.ai/healthz" },
]
for (const svc of infraServices) {
try {
const start = performance.now()
const res = await fetch(svc.endpoint, { signal: AbortSignal.timeout(3000) }).catch(() => null)
const latency = Math.round(performance.now() - start)
checks.push({
service: svc.name,
status: res?.ok ? "healthy" : "degraded",
latency_ms: latency,
last_check: new Date().toISOString(),
uptime_pct: res?.ok ? 99.9 : 95.0,
})
} catch {
checks.push({
service: svc.name,
status: "degraded",
latency_ms: 0,
last_check: new Date().toISOString(),
uptime_pct: 95.0,
details: "Timeout",
})
}
}
setHealthChecks(checks)
} catch (e) {
console.error("Monitoring fetch error:", e)
} finally {
setLoading(false)
setRefreshing(false)
}
}
useEffect(() => {
fetchMonitoringData()
const interval = setInterval(fetchMonitoringData, 30000) // Refresh every 30s
return () => clearInterval(interval)
}, [])
function handleRefresh() {
setRefreshing(true)
fetchMonitoringData()
}
const healthy = healthChecks.filter((h) => h.status === "healthy").length
const degraded = healthChecks.filter((h) => h.status === "degraded").length
const down = healthChecks.filter((h) => h.status === "down").length
const overallStatus = down > 0 ? "down" : degraded > 0 ? "degraded" : "healthy"
const STATUS_ICON = {
healthy: <CheckCircle className="h-5 w-5 text-green-500" />,
degraded: <AlertTriangle className="h-5 w-5 text-yellow-500" />,
down: <XCircle className="h-5 w-5 text-red-500" />,
}
const SEVERITY_BADGE = {
critical: "destructive" as const,
warning: "secondary" as const,
info: "outline" as const,
}
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Monitoring</h1>
<p className="text-muted-foreground">
Service health, alerts, and system performance
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{STATUS_ICON[overallStatus]}
<span className="text-sm font-medium capitalize">{overallStatus === "healthy" ? "All Systems Operational" : overallStatus === "degraded" ? "Some Issues Detected" : "Service Disruption"}</span>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
<RefreshCcw className={`mr-2 h-4 w-4 ${refreshing ? "animate-spin" : ""}`} />
Refresh
</Button>
</div>
</div>
{/* Summary Cards */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Services</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : healthChecks.length}
</div>
<p className="text-xs text-muted-foreground">monitored</p>
</CardContent>
</Card>
<Card className={healthy > 0 ? "border-green-200" : ""}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-500" />
Healthy
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : healthy}
</div>
</CardContent>
</Card>
<Card className={degraded > 0 ? "border-yellow-200" : ""}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-500" />
Degraded
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-600">
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : degraded}
</div>
</CardContent>
</Card>
<Card className={down > 0 ? "border-red-200" : ""}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<XCircle className="h-4 w-4 text-red-500" />
Down
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : down}
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="health">
<TabsList>
<TabsTrigger value="health">Health Checks</TabsTrigger>
<TabsTrigger value="alerts">
Alerts
{alerts.filter((a) => !a.resolved_at).length > 0 && (
<Badge variant="destructive" className="ml-2 h-5 w-5 rounded-full p-0 text-[10px]">
{alerts.filter((a) => !a.resolved_at).length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="logs">RPC Logs</TabsTrigger>
</TabsList>
<TabsContent value="health" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Service Health</CardTitle>
<CardDescription>Real-time health status of all services</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : healthChecks.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<Server className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No services being monitored</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Service</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Latency</TableHead>
<TableHead className="text-right">Uptime</TableHead>
<TableHead>Last Check</TableHead>
<TableHead>Details</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{healthChecks.map((check) => (
<TableRow key={check.service}>
<TableCell className="font-medium">{check.service}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{STATUS_ICON[check.status]}
<span className="capitalize">{check.status}</span>
</div>
</TableCell>
<TableCell className="text-right font-mono">
{check.latency_ms > 0 ? `${check.latency_ms}ms` : "—"}
</TableCell>
<TableCell className="text-right">
<span className={check.uptime_pct >= 99 ? "text-green-600" : check.uptime_pct >= 95 ? "text-yellow-600" : "text-red-600"}>
{check.uptime_pct.toFixed(1)}%
</span>
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{new Date(check.last_check).toLocaleTimeString()}
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{check.details || "—"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="alerts" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Active Alerts</CardTitle>
<CardDescription>Current and recent alerts across all services</CardDescription>
</CardHeader>
<CardContent>
{alerts.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<CheckCircle className="h-12 w-12 mx-auto mb-4 text-green-500 opacity-50" />
<p className="font-medium">No active alerts</p>
<p className="text-sm mt-1">All systems are running normally</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Severity</TableHead>
<TableHead>Service</TableHead>
<TableHead>Message</TableHead>
<TableHead>Started</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{alerts.map((alert) => (
<TableRow key={alert.id}>
<TableCell>
<Badge variant={SEVERITY_BADGE[alert.severity]}>{alert.severity}</Badge>
</TableCell>
<TableCell className="font-medium">{alert.service}</TableCell>
<TableCell>{alert.message}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{new Date(alert.started_at).toLocaleString()}
</TableCell>
<TableCell>
{alert.resolved_at ? (
<Badge variant="outline">Resolved</Badge>
) : (
<Badge variant="destructive">Active</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="logs" className="mt-4">
<Card>
<CardHeader>
<CardTitle>RPC Request Logs</CardTitle>
<CardDescription>Recent API and RPC requests</CardDescription>
</CardHeader>
<CardContent>
{rpcLogs.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<Activity className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">No recent requests</p>
<p className="text-sm mt-1">RPC logs will appear here as requests are made</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead>Chain</TableHead>
<TableHead className="text-right">Latency</TableHead>
<TableHead>Time</TableHead>
<TableHead>Request ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rpcLogs.map((log) => (
<TableRow key={log.id}>
<TableCell>
<Badge variant={log.status < 300 ? "default" : log.status < 500 ? "secondary" : "destructive"}>
{log.status}
</Badge>
</TableCell>
<TableCell className="font-mono text-sm">{log.method}</TableCell>
<TableCell>{log.chain}</TableCell>
<TableCell className="text-right font-mono text-sm">{log.latency_ms}ms</TableCell>
<TableCell className="text-sm text-muted-foreground">
{new Date(log.timestamp).toLocaleTimeString()}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground">{log.request_id}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}