Fleet topology epidemiology: strain plague map, interrupt fixes, tests.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 09:20:30 -07:00
parent f0fe34698c
commit 0935756e5f
10 changed files with 1451 additions and 89 deletions

View File

@@ -3,138 +3,217 @@ import { OrbitControls, Stars, Line, Sphere } from '@react-three/drei';
import { useMemo, useRef } from 'react';
import * as THREE from 'three';
import { Agent } from '../../../types';
import {
buildEpidemiologyGraph,
layoutStrainNodes,
nodeEmissiveIntensity,
nodeWireOpacity,
type StrainEdge,
type StrainNode,
} from '../../../help/fleetTopologyEpidemiology';
function LaserPulse({ start, end, color }: { start: [number, number, number], end: [number, number, number], color: string }) {
function PlaguePulse({
start,
end,
color,
speed = 1.8,
}: {
start: [number, number, number];
end: [number, number, number];
color: string;
speed?: number;
}) {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (meshRef.current) {
// Move pulse from start to end
const t = (state.clock.elapsedTime * 2) % 1;
const t = (state.clock.elapsedTime * speed) % 1;
meshRef.current.position.set(
start[0] + (end[0] - start[0]) * t,
start[1] + (end[1] - start[1]) * t,
start[2] + (end[2] - start[2]) * t
start[2] + (end[2] - start[2]) * t,
);
}
});
return (
<mesh ref={meshRef}>
<sphereGeometry args={[0.08, 8, 8]} />
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color={color} />
</mesh>
);
}
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
/** Cap 3D nodes to keep WebGL performant on large fleets. */
const TOPOLOGY_NODE_CAP = 200;
function AgentNode({ agent, position, serverPos }: { agent: Agent, position: [number, number, number], serverPos: [number, number, number] }) {
const isOnline = agent.status === 'online';
const isHashing = agent.hashrate_15m > 0;
const color = isOnline ? (isHashing ? '#00e8f5' : '#00aa55') : '#ff4444';
function StrainNodeMesh({
node,
position,
onGoalPath,
}: {
node: StrainNode;
position: [number, number, number];
onGoalPath: boolean;
}) {
const pulseRef = useRef<THREE.Mesh>(null);
const ringRef = useRef<THREE.Mesh>(null);
// Stale = online status but last_seen older than 5 min (silently dead)
const isStale = isOnline && !!agent.last_seen &&
(Date.now() - new Date(agent.last_seen).getTime()) > STALE_THRESHOLD_MS;
const isSuccess = node.branchStatus === 'success' || node.miningContinuity === 'continuous';
const isFailed = node.branchStatus === 'failed' || node.miningContinuity === 'interrupted';
const baseColor = node.color;
const emissive = isFailed ? '#331111' : baseColor;
const intensity = onGoalPath ? nodeEmissiveIntensity(node) * 1.4 : nodeEmissiveIntensity(node);
const radius = 0.35 + Math.min(node.hostCount, 12) * 0.04;
const opacity = nodeWireOpacity(node);
useFrame((state) => {
if (isOnline && pulseRef.current) {
const scale = 1 + Math.sin(state.clock.elapsedTime * (isHashing ? 5 : 1)) * 0.15 * (isHashing ? 1 : 0.5);
pulseRef.current.scale.set(scale, scale, scale);
if (isHashing) {
pulseRef.current.rotation.y += 0.05;
pulseRef.current.rotation.x += 0.05;
}
}
// Slowly spin the staleness warning ring
if (isStale && ringRef.current) {
ringRef.current.rotation.z += 0.01;
ringRef.current.rotation.x = Math.sin(state.clock.elapsedTime * 0.5) * 0.3;
}
if (!pulseRef.current) return;
const pulse = isSuccess
? 1 + Math.sin(state.clock.elapsedTime * 3) * 0.18
: isFailed
? 1 + Math.sin(state.clock.elapsedTime * 0.6) * 0.04
: 1;
pulseRef.current.scale.set(pulse, pulse, pulse);
if (isSuccess) pulseRef.current.rotation.y += 0.02;
});
return (
<group position={position}>
<Sphere ref={pulseRef} args={[0.3, 16, 16]}>
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={isOnline ? (isHashing ? 2 : 1) : 0.2} wireframe />
<Sphere ref={pulseRef} args={[radius, 16, 16]}>
<meshStandardMaterial
color={baseColor}
emissive={emissive}
emissiveIntensity={intensity}
wireframe
transparent
opacity={opacity}
/>
</Sphere>
{/* Staleness warning ring — amber halo for online-but-silent nodes */}
{isStale && (
<mesh ref={ringRef}>
<torusGeometry args={[0.55, 0.04, 8, 40]} />
<meshBasicMaterial color="#ffb020" opacity={0.85} transparent />
{onGoalPath && node.miningContinuity === 'continuous' && (
<mesh>
<torusGeometry args={[radius + 0.25, 0.03, 8, 32]} />
<meshBasicMaterial color="#39ff14" opacity={0.9} transparent />
</mesh>
)}
</group>
);
}
{/* Connection Line */}
<Line points={[[0,0,0], [serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]]} color={isStale ? '#665500' : isOnline ? '#004455' : '#330000'} lineWidth={1} transparent opacity={0.4} />
{/* Laser Pulse simulating hashing packets */}
{isOnline && isHashing && !isStale && (
<LaserPulse start={[0,0,0]} end={[serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]} color="#00e8f5" />
function StrainEdgeLine({
edge,
positions,
}: {
edge: StrainEdge;
positions: Map<string, [number, number, number]>;
}) {
const start = positions.get(edge.sourceStrain);
const end = positions.get(edge.targetStrain);
if (!start || !end) return null;
const failed = edge.branchStatus === 'failed';
const success = edge.weight > 0;
const color = edge.onGoalPath ? '#39ff14' : failed ? '#442222' : '#b24bf3';
const lineWidth = edge.onGoalPath ? 2.5 : Math.min(4, 0.8 + edge.weight * 0.35);
const opacity = failed && !success ? 0.15 : edge.onGoalPath ? 0.9 : 0.55;
return (
<group>
<Line points={[start, end]} color={color} lineWidth={lineWidth} transparent opacity={opacity} />
{success && !failed && (
<PlaguePulse
start={start}
end={end}
color={edge.onGoalPath ? '#39ff14' : '#ff2da6'}
speed={1.2 + edge.weight * 0.2}
/>
)}
</group>
);
}
export default function FleetTopologyMap({ agents }: { agents: Agent[] }) {
const serverPos: [number, number, number] = [0, 0, 0];
const displayAgents = useMemo(() => {
if (agents.length <= TOPOLOGY_NODE_CAP) return agents;
const online = agents.filter((a) => a.status === 'online');
const pool = online.length >= TOPOLOGY_NODE_CAP ? online : agents;
return pool.slice(0, TOPOLOGY_NODE_CAP);
}, [agents]);
const capped = agents.length > TOPOLOGY_NODE_CAP;
const graph = useMemo(() => buildEpidemiologyGraph(agents), [agents]);
const layouts = useMemo(() => layoutStrainNodes(graph.nodes), [graph.nodes]);
const positionMap = useMemo(() => {
const map = new Map<string, [number, number, number]>();
for (const layout of layouts) {
map.set(layout.strainId, layout.position);
}
return map;
}, [layouts]);
const goalSet = useMemo(() => new Set(graph.goalPath), [graph.goalPath]);
const agentNodes = useMemo(() => {
return displayAgents.map((agent, i) => {
const goldenRatio = (1 + Math.sqrt(5)) / 2;
const angle = i * Math.PI * 2 * goldenRatio;
// Distribute in a spherical/cylindrical rough cluster
const radius = 4 + Math.random() * 3 + (i * 0.05);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = (Math.random() - 0.5) * 6;
return { agent, position: [x, y, z] as [number, number, number] };
});
}, [displayAgents]);
const plagueEdges = graph.edges.filter((e) => e.weight > 0).length;
const continuousStrains = graph.nodes.filter((n) => n.miningContinuity === 'continuous').length;
const interrupted = graph.interrupted.length;
return (
<div className="topology-container" style={{ width: '100%', height: '500px', background: '#050508', borderRadius: '8px', overflow: 'hidden', border: '1px solid var(--neon-cyan)', position: 'relative', boxShadow: '0 0 20px rgba(0, 232, 245, 0.1)' }}>
<div style={{ position: 'absolute', top: 15, left: 15, zIndex: 10, color: 'var(--neon-cyan)', fontFamily: 'monospace', textShadow: '0 0 5px var(--neon-cyan)' }}>
<span className="live-beacon on" style={{ display: 'inline-block', marginRight: 8, verticalAlign: 'middle' }}></span>
3D_MESH_TOPOLOGY // {displayAgents.filter(a => a.status === 'online').length} NODES LINKED
{capped && ` (showing ${TOPOLOGY_NODE_CAP}/${agents.length})`}
<div
className="topology-container epidemiology-topology"
data-testid="fleet-epidemiology-map"
style={{
width: '100%',
height: '500px',
background: '#050508',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid var(--neon-cyan)',
position: 'relative',
boxShadow: '0 0 20px rgba(0, 232, 245, 0.1)',
}}
>
<div
style={{
position: 'absolute',
top: 15,
left: 15,
zIndex: 10,
color: 'var(--neon-cyan)',
fontFamily: 'monospace',
fontSize: 12,
textShadow: '0 0 5px var(--neon-cyan)',
}}
>
<span className="live-beacon on" style={{ display: 'inline-block', marginRight: 8, verticalAlign: 'middle' }} />
STRAIN_EPIDEMIOLOGY // {graph.nodes.length} STRAINS · {plagueEdges} PLAGUE_EDGES
{graph.goalPath.length > 0 && ` · GOAL_PATH ${graph.goalPath.length}`}
{continuousStrains > 0 && ` · ${continuousStrains} MINING`}
{interrupted > 0 && ` · ${interrupted} INTERRUPTED`}
</div>
<div
style={{
position: 'absolute',
bottom: 12,
left: 15,
zIndex: 10,
color: '#888',
fontFamily: 'monospace',
fontSize: 10,
}}
>
glow = successful branch · dim = failed tree · pulses = spread weight (no cures)
</div>
<Canvas camera={{ position: [0, 8, 14], fov: 50 }}>
<color attach="background" args={['#050508']} />
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} intensity={1.5} color="#00e8f5" />
<Stars radius={100} depth={50} count={3000} factor={3} saturation={0.5} fade speed={1} />
{/* Server Node (Mothership) */}
<group position={serverPos}>
<Sphere args={[0.7, 32, 32]}>
<meshStandardMaterial color="#ffb020" emissive="#ffb020" emissiveIntensity={1.2} wireframe />
</Sphere>
<Sphere args={[0.3, 16, 16]}>
<meshStandardMaterial color="#ffffff" emissive="#ffffff" emissiveIntensity={2} />
</Sphere>
</group>
<ambientLight intensity={0.45} />
<pointLight position={[10, 10, 10]} intensity={1.4} color="#ff2da6" />
<pointLight position={[-8, 4, -6]} intensity={0.8} color="#00e8f5" />
<Stars radius={100} depth={50} count={2500} factor={3} saturation={0.4} fade speed={0.8} />
{/* Agent Nodes */}
{agentNodes.map((node) => (
<AgentNode key={node.agent.id} agent={node.agent} position={node.position} serverPos={serverPos} />
{graph.nodes.map((node) => {
const pos = positionMap.get(node.id);
if (!pos) return null;
return (
<StrainNodeMesh
key={node.id}
node={node}
position={pos}
onGoalPath={goalSet.has(node.id)}
/>
);
})}
{graph.edges.map((edge) => (
<StrainEdgeLine key={edge.id} edge={edge} positions={positionMap} />
))}
<OrbitControls enablePan={true} enableZoom={true} enableRotate={true} autoRotate autoRotateSpeed={0.8} />
<OrbitControls enablePan enableZoom enableRotate autoRotate autoRotateSpeed={0.6} />
</Canvas>
</div>
);
}
}

View File

@@ -854,9 +854,28 @@ describe('SystemStatusBar', () => {
describe('FleetTopologyMap', () => {
afterEach(() => cleanup());
it('renders canvas wrapper for agents', () => {
it('renders strain epidemiology canvas for agents', () => {
render(<FleetTopologyMap agents={[mockAgent()]} />);
expect(screen.getByTestId('three-canvas')).toBeInTheDocument();
expect(screen.getByTestId('fleet-epidemiology-map')).toBeInTheDocument();
expect(screen.getByText(/STRAIN_EPIDEMIOLOGY/)).toBeInTheDocument();
});
it('shows plague edge count for spread genealogy', () => {
const parent = mockAgent({
id: 'parent-1',
spread_strain: '#aabbcc',
spread_generation: 0,
});
const child = mockAgent({
id: 'child-1',
spread_strain: '#ddeeff',
parent_agent_id: 'parent-1',
join_lane: 'winrm',
spread_generation: 1,
});
render(<FleetTopologyMap agents={[parent, child]} />);
expect(screen.getByText(/PLAGUE_EDGES/)).toBeInTheDocument();
});
});