"use client"; import { memo, useEffect, useMemo, useState } from "react"; import { motion } from "framer-motion"; type NodeRole = "core" | "gateway" | "worker" | "cache"; type NodeStatus = "ok" | "warm" | "hot" | "offline"; type NodeDatum = { id: number; label: string; role: NodeRole; cpu: number; mem: number; queue: number; status: NodeStatus; }; type Snapshot = { step: number; nodes: NodeDatum[]; latencyMs: number; requestRate: number; action: string; stability: number; }; type Point = { x: number; y: number; }; const POSITIONS: Point[] = [ { x: 480, y: 280 }, { x: 480, y: 92 }, { x: 650, y: 150 }, { x: 722, y: 318 }, { x: 610, y: 466 }, { x: 350, y: 466 }, { x: 238, y: 318 }, { x: 310, y: 150 }, { x: 480, y: 176 }, ]; const NODE_META: Array> = [ { id: 0, label: "control-plane", role: "core" }, { id: 1, label: "ingress-01", role: "gateway" }, { id: 2, label: "worker-02", role: "worker" }, { id: 3, label: "worker-03", role: "worker" }, { id: 4, label: "cache-04", role: "cache" }, { id: 5, label: "worker-05", role: "worker" }, { id: 6, label: "worker-06", role: "worker" }, { id: 7, label: "ingress-07", role: "gateway" }, { id: 8, label: "policy-agent", role: "core" }, ]; const STATUS_STYLE: Record = { ok: { accent: "#2dd4bf", text: "#ccfbf1", fill: "rgba(8, 20, 24, 0.92)", border: "rgba(45, 212, 191, 0.28)", }, warm: { accent: "#fbbf24", text: "#fde68a", fill: "rgba(29, 22, 10, 0.94)", border: "rgba(251, 191, 36, 0.32)", }, hot: { accent: "#fb7185", text: "#fecdd3", fill: "rgba(36, 15, 20, 0.94)", border: "rgba(251, 113, 133, 0.36)", }, offline: { accent: "#71717a", text: "#d4d4d8", fill: "rgba(16, 16, 20, 0.96)", border: "rgba(113, 113, 122, 0.26)", }, }; function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } function smoothNoise(seed: number) { return Math.sin(seed * 12.9898) * 0.5 + Math.sin(seed * 4.1414) * 0.5; } function getStatus(cpu: number, offline: boolean): NodeStatus { if (offline) return "offline"; if (cpu >= 0.82) return "hot"; if (cpu >= 0.62) return "warm"; return "ok"; } function generateSnapshot(step: number): Snapshot { const t = step * 0.16; const phase = step % 96; const surge = phase >= 48 && phase <= 74; const recovery = phase > 74; const nodes = NODE_META.map((node) => { const wave = Math.sin(t + node.id * 0.72); const pulse = Math.max(0, Math.sin((phase - 42) / 12)); const offline = surge && (node.id === 3 || node.id === 6) && phase > 62; const base = node.role === "core" ? 0.38 : node.role === "gateway" ? 0.42 : node.role === "cache" ? 0.33 : 0.31; const stress = surge ? 0.24 + pulse * 0.24 : recovery ? 0.16 : 0.08; const cpu = offline ? 0 : clamp(base + wave * 0.1 + stress + smoothNoise(step + node.id) * 0.018, 0.07, 0.96); const mem = offline ? 0 : clamp(0.34 + Math.sin(t * 0.65 + node.id) * 0.08 + (surge ? 0.12 : 0), 0.16, 0.9); const queue = offline ? 0 : Math.round(clamp(8 + cpu * 28 + (surge ? pulse * 18 : 0), 2, 58)); return { ...node, cpu, mem, queue, status: getStatus(cpu, offline), }; }); const activeNodes = nodes.filter((node) => node.status !== "offline").length; const avgCpu = nodes.reduce((sum, node) => sum + node.cpu, 0) / Math.max(activeNodes, 1); return { step, nodes, latencyMs: clamp(32 + avgCpu * 96 + (surge ? Math.max(0, Math.sin((phase - 48) / 10)) * 64 : 0), 28, 240), requestRate: clamp(148 + Math.sin(t * 0.4) * 32 + (surge ? 170 : recovery ? 82 : 0), 80, 430), action: surge ? "traffic_shift + scale_out" : recovery ? "drain_and_recover" : "steady_state", stability: clamp((activeNodes / nodes.length) * (1 - Math.max(0, avgCpu - 0.58) * 0.75), 0.42, 0.99), }; } function edgePath(from: Point, to: Point, bend = 0) { const midX = (from.x + to.x) / 2; const midY = (from.y + to.y) / 2; const dx = to.x - from.x; const dy = to.y - from.y; const length = Math.max(1, Math.hypot(dx, dy)); const cx = midX + (-dy / length) * bend; const cy = midY + (dx / length) * bend; return `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`; } function Metric({ label, value, tone = "text-zinc-200" }: { label: string; value: string; tone?: string }) { return (

{label}

{value}

); } function Edge({ from, to, traffic, muted, index }: { from: Point; to: Point; traffic: number; muted: boolean; index: number }) { const path = edgePath(from, to, index % 2 === 0 ? 18 : -18); const hot = traffic > 0.78; const color = muted ? "rgba(113, 113, 122, 0.28)" : hot ? "rgba(251, 191, 36, 0.58)" : "rgba(45, 212, 191, 0.42)"; const particleColor = hot ? "#fbbf24" : "#67e8f9"; const duration = clamp(4.1 - traffic * 2.2, 1.45, 3.8); return ( {!muted ? ( <> ) : null} ); } const NodeCard = memo(function NodeCard({ node, position }: { node: NodeDatum; position: Point }) { const style = STATUS_STYLE[node.status]; const core = node.role === "core"; const width = core ? 132 : 116; const height = core ? 74 : 66; const radius = core ? 18 : 15; const circumference = 2 * Math.PI * radius; const dashOffset = circumference * (1 - node.cpu); const opacity = node.status === "offline" ? 0.52 : 1; return ( {node.status !== "offline" ? ( ) : null} {node.status === "offline" ? "off" : `${Math.round(node.cpu * 100)}`} {node.label} {node.role.toUpperCase()} / {node.status.toUpperCase()} MEM {Math.round(node.mem * 100)}% Q {node.queue} ); }); export function ClusterSimulation() { const [snapshot, setSnapshot] = useState(() => generateSnapshot(0)); useEffect(() => { let step = 0; const timer = window.setInterval(() => { step += 1; setSnapshot(generateSnapshot(step)); }, 700); return () => window.clearInterval(timer); }, []); const summary = useMemo(() => { const online = snapshot.nodes.filter((node) => node.status !== "offline").length; const avgCpu = snapshot.nodes.reduce((sum, node) => sum + node.cpu, 0) / Math.max(online, 1); return { online, avgCpu, hotNodes: snapshot.nodes.filter((node) => node.status === "hot").length, }; }, [snapshot.nodes]); return (

Simulation

step {snapshot.step} / {summary.online} of {snapshot.nodes.length} nodes online

{snapshot.nodes.slice(1, 8).map((node, index) => ( ))} {[1, 2, 3, 4, 5, 6, 7].map((id, index, ids) => { const nextId = ids[(index + 1) % ids.length]; const node = snapshot.nodes[id]; const nextNode = snapshot.nodes[nextId]; return ( ); })} {snapshot.nodes.map((node) => ( ))}
150 ? "text-amber-200" : "text-sky-200"} /> 0.72 ? "text-rose-200" : "text-emerald-200"} /> 0 ? "text-amber-200" : "text-zinc-300"} />
); } export default ClusterSimulation;