// 3D fleet view — abstract glowing cubes with orbit controls const { useRef: use3DRef, useState: use3DState, useEffect: use3DEffect } = React; function ThreeD({ nodes, onSelect, style3d }) { const canvasRef = use3DRef(null); const wrapRef = use3DRef(null); const [cam, setCam] = use3DState({ yaw: -0.5, pitch: 0.45, zoom: 1, tx: 0, ty: 0 }); const [hover, setHover] = use3DState(null); const rafRef = use3DRef(0); const dragRef = use3DRef(null); // group nodes by their group (Websites / Lovable / Cloudflare / Dgx-spark / Godento) const layout = React.useMemo(() => { const groups = window.GROUPS; const byGroup = {}; nodes.forEach((n, idx) => { if (!byGroup[n.group]) byGroup[n.group] = []; byGroup[n.group].push({ n, origIdx: idx }); }); const positions = new Array(nodes.length); if (style3d === 'lattice') { nodes.forEach((n, i) => { const phi = Math.acos(1 - 2 * (i + 0.5) / nodes.length); const theta = Math.PI * (1 + Math.sqrt(5)) * i; const R = 9; positions[i] = { x: R * Math.sin(phi) * Math.cos(theta), y: R * Math.cos(phi), z: R * Math.sin(phi) * Math.sin(theta) }; }); return positions; } if (style3d === 'grid') { // flat grid organized by group columns groups.forEach((g, gi) => { const list = byGroup[g] || []; list.forEach((item, i) => { const row = Math.floor(i / 3); const col = i % 3; positions[item.origIdx] = { x: (gi - 2) * 7 + (col - 1) * 1.8, y: 0, z: (row - 2) * 2, }; }); }); return positions; } // default: cube clusters — one per group groups.forEach((g, gi) => { const list = byGroup[g] || []; const cx = (gi - 2) * 9; list.forEach((item, i) => { const cols = 3; const rows = Math.ceil(list.length / cols); const col = i % cols; const row = Math.floor(i / cols); positions[item.origIdx] = { x: cx + (col - 1) * 2.2, y: (row - rows/2) * 2.2, z: 0, }; }); }); return positions; }, [nodes, style3d]); function project(p, cw, ch, c) { const { yaw, pitch, zoom, tx, ty } = c; let x = p.x * Math.cos(yaw) - p.z * Math.sin(yaw); let z = p.x * Math.sin(yaw) + p.z * Math.cos(yaw); let y = p.y; const y2 = y * Math.cos(pitch) - z * Math.sin(pitch); const z2 = y * Math.sin(pitch) + z * Math.cos(pitch); const f = 500 * zoom; const d = 34; const sx = cw / 2 + (x * f) / (d + z2) + tx; const sy = ch / 2 - (y2 * f) / (d + z2) + ty; return { sx, sy, depth: z2 }; } function draw() { const canvas = canvasRef.current; if (!canvas) return; const wrap = wrapRef.current; const dpr = window.devicePixelRatio || 1; const cw = wrap.clientWidth, ch = wrap.clientHeight; if (canvas.width !== cw * dpr) { canvas.width = cw * dpr; canvas.height = ch * dpr; canvas.style.width = cw + 'px'; canvas.style.height = ch + 'px'; } const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, cw, ch); drawFloor(ctx, cw, ch, cam); if (style3d === 'cubes') drawGroupLabels(ctx, cw, ch, cam); if (style3d === 'grid') drawGroupLabels(ctx, cw, ch, cam, true); const projected = nodes.map((n, i) => ({ n, p: project(layout[i], cw, ch, cam), pos: layout[i] })) .sort((a, b) => b.p.depth - a.p.depth); drawConnections(ctx, cw, ch, cam, projected); for (const item of projected) drawNode(ctx, item, cam, hover === item.n.id); } function drawFloor(ctx, cw, ch, c) { const grid = 20; const size = 2; ctx.strokeStyle = 'rgba(40, 60, 80, 0.22)'; ctx.lineWidth = 1; for (let i = -grid; i <= grid; i += size) { const a = project({x: i, y: -5, z: -grid}, cw, ch, c); const b = project({x: i, y: -5, z: grid}, cw, ch, c); ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke(); const c1 = project({x: -grid, y: -5, z: i}, cw, ch, c); const c2 = project({x: grid, y: -5, z: i}, cw, ch, c); ctx.beginPath(); ctx.moveTo(c1.sx, c1.sy); ctx.lineTo(c2.sx, c2.sy); ctx.stroke(); } } function drawGroupLabels(ctx, cw, ch, c, flat) { window.GROUPS.forEach((g, gi) => { const p = project({ x: (gi - 2) * 9, y: flat ? -4.5 : 5, z: 0 }, cw, ch, c); ctx.fillStyle = 'rgba(150, 180, 200, 0.55)'; ctx.font = '10px JetBrains Mono, monospace'; ctx.textAlign = 'center'; ctx.fillText(g.toUpperCase(), p.sx, p.sy); }); } function drawConnections(ctx, cw, ch, c, projected) { ctx.lineWidth = 1; for (const g of window.GROUPS) { const group = projected.filter(p => p.n.group === g); for (let i = 0; i < group.length - 1; i++) { const a = group[i], b = group[i+1]; const grad = ctx.createLinearGradient(a.p.sx, a.p.sy, b.p.sx, b.p.sy); grad.addColorStop(0, 'rgba(100, 200, 220, 0.04)'); grad.addColorStop(0.5, 'rgba(100, 200, 220, 0.16)'); grad.addColorStop(1, 'rgba(100, 200, 220, 0.04)'); ctx.strokeStyle = grad; ctx.beginPath(); ctx.moveTo(a.p.sx, a.p.sy); ctx.lineTo(b.p.sx, b.p.sy); ctx.stroke(); } } } function drawNode(ctx, item, c, isHover) { const { n, p, pos } = item; const size = 0.8; const colors = { ok: { core: 'oklch(0.72 0.16 150)', glow: 'rgba(50, 220, 140, 0.35)' }, warn: { core: 'oklch(0.75 0.15 85)', glow: 'rgba(255, 200, 80, 0.35)' }, crit: { core: 'oklch(0.66 0.22 27)', glow: 'rgba(255, 80, 80, 0.55)' }, off: { core: 'oklch(0.35 0 0)', glow: 'rgba(100, 100, 100, 0.15)' }, maint: { core: 'oklch(0.75 0.15 85)', glow: 'rgba(255, 200, 80, 0.35)' }, unknown: { core: 'oklch(0.45 0 0)', glow: 'rgba(120, 120, 120, 0.2)' }, }; // Fallback ensures an unexpected backend-emitted status can never crash the render. const col = colors[n.status] || colors.unknown; const scale = 500 * c.zoom / (34 + p.depth); const pix = size * scale; const haloR = pix * (n.status === 'crit' ? 3.5 : 2.2) * (isHover ? 1.5 : 1); const pulse = n.status === 'crit' ? (0.7 + Math.sin(performance.now() * 0.004) * 0.3) : 1; const g = ctx.createRadialGradient(p.sx, p.sy, 0, p.sx, p.sy, haloR); g.addColorStop(0, col.glow.replace(/[\d.]+\)$/, (0.45 * pulse) + ')')); g.addColorStop(1, col.glow.replace(/[\d.]+\)$/, '0)')); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.sx, p.sy, haloR, 0, Math.PI * 2); ctx.fill(); if (style3d === 'lattice') { const g2 = ctx.createRadialGradient(p.sx - pix*0.3, p.sy - pix*0.3, 0, p.sx, p.sy, pix * 1.4); g2.addColorStop(0, col.core); g2.addColorStop(1, col.core.replace(')', ' / 0.35)')); ctx.fillStyle = g2; ctx.beginPath(); ctx.arc(p.sx, p.sy, pix * 1.1, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(p.sx, p.sy, Math.max(1, pix * 0.25), 0, Math.PI * 2); ctx.fill(); } else { const half = size / 2; const corners = [ {x: pos.x - half, y: pos.y - half, z: pos.z - half}, {x: pos.x + half, y: pos.y - half, z: pos.z - half}, {x: pos.x + half, y: pos.y - half, z: pos.z + half}, {x: pos.x - half, y: pos.y - half, z: pos.z + half}, {x: pos.x - half, y: pos.y + half, z: pos.z - half}, {x: pos.x + half, y: pos.y + half, z: pos.z - half}, {x: pos.x + half, y: pos.y + half, z: pos.z + half}, {x: pos.x - half, y: pos.y + half, z: pos.z + half}, ].map(cc => project(cc, canvasRef.current.clientWidth, canvasRef.current.clientHeight, c)); const fill = (idx, alpha) => { ctx.beginPath(); ctx.moveTo(corners[idx[0]].sx, corners[idx[0]].sy); for (let i = 1; i < idx.length; i++) ctx.lineTo(corners[idx[i]].sx, corners[idx[i]].sy); ctx.closePath(); ctx.fillStyle = col.core.replace(')', ` / ${alpha})`); ctx.fill(); ctx.strokeStyle = col.core; ctx.lineWidth = isHover ? 1.5 : 0.8; ctx.stroke(); }; fill([4,5,6,7], 0.85); fill([1,5,6,2], 0.55); fill([3,2,6,7], 0.7); } if (pix > 6 || isHover) { ctx.fillStyle = isHover ? '#7fe3f5' : 'rgba(200, 220, 240, 0.7)'; ctx.font = `${isHover ? 'bold ' : ''}10px JetBrains Mono, monospace`; ctx.textAlign = 'center'; const label = n.displayName.length > 20 ? n.displayName.slice(0,18) + '…' : n.displayName; ctx.fillText(label, p.sx, p.sy - pix - 10); if (isHover) { ctx.fillStyle = 'rgba(150, 170, 190, 0.8)'; ctx.font = '9px JetBrains Mono, monospace'; ctx.fillText(`${n.latency.toFixed(0)}ms · ${n.group}`, p.sx, p.sy - pix - 22); } } } use3DEffect(() => { function tick() { draw(); rafRef.current = requestAnimationFrame(tick); } tick(); return () => cancelAnimationFrame(rafRef.current); }, [nodes, layout, cam, style3d, hover]); function onDown(e) { dragRef.current = { x: e.clientX, y: e.clientY, yaw: cam.yaw, pitch: cam.pitch, button: e.button }; } function onMove(e) { const rect = canvasRef.current.getBoundingClientRect(); const mx = e.clientX - rect.left, my = e.clientY - rect.top; let best = null; let bestDist = 22; for (let i = 0; i < nodes.length; i++) { const p = project(layout[i], rect.width, rect.height, cam); const d = Math.hypot(p.sx - mx, p.sy - my); if (d < bestDist) { bestDist = d; best = nodes[i].id; } } setHover(best); if (!dragRef.current) return; const dx = e.clientX - dragRef.current.x; const dy = e.clientY - dragRef.current.y; setCam({ ...cam, yaw: dragRef.current.yaw + dx * 0.006, pitch: Math.max(-1.3, Math.min(1.3, dragRef.current.pitch + dy * 0.006)) }); } function onUp(e) { const moved = dragRef.current && (Math.abs(e.clientX - dragRef.current.x) > 4 || Math.abs(e.clientY - dragRef.current.y) > 4); dragRef.current = null; if (!moved && hover) { const n = nodes.find(nn => nn.id === hover); if (n) onSelect(n); } } function onWheel(e) { e.preventDefault(); const d = e.deltaY > 0 ? 0.9 : 1.1; setCam(c => ({ ...c, zoom: Math.max(0.35, Math.min(2.5, c.zoom * d)) })); } const styleInfo = { cubes: 'Group clusters', lattice: 'Lattice sphere', grid: 'Flat grid' }[style3d]; const critical = nodes.filter(n => n.status === 'crit'); const offline = nodes.filter(n => n.status === 'off'); return (