Add dashboard 3D fleet map, matrix stream overlay, and PWA support.

Integrates React Three Fiber topology view and a live matrix stream modal on the dashboard. Adds vite-plugin-pwa with AetherForge manifest and three.js dependencies.
This commit is contained in:
drjones
2026-05-28 22:28:26 -07:00
parent c95a4373de
commit 20eb5a3ba4
8 changed files with 4977 additions and 71 deletions

View File

@@ -4,6 +4,8 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0d0d12" />
<link rel="apple-touch-icon" href="/vite.svg">
<title>AetherForge — LAN Mining Command Deck</title>
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,17 @@
"test": "vitest run"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@types/qrcode": "^1.5.6",
"@types/three": "^0.184.1",
"qrcode": "^1.5.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"recharts": "^2.10.0"
"recharts": "^2.10.0",
"three": "^0.184.0",
"vite-plugin-pwa": "^1.3.0"
},
"devDependencies": {
"@types/react": "^18.2.37",

View File

@@ -0,0 +1,118 @@
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Stars, Line, Sphere, Text } from '@react-three/drei';
import { useMemo, useRef } from 'react';
import * as THREE from 'three';
import { Agent } from '../../../types';
function LaserPulse({ start, end, color }: { start: [number, number, number], end: [number, number, number], color: string }) {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (meshRef.current) {
// Move pulse from start to end
const t = (state.clock.elapsedTime * 2) % 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
);
}
});
return (
<mesh ref={meshRef}>
<sphereGeometry args={[0.08, 8, 8]} />
<meshBasicMaterial color={color} />
</mesh>
);
}
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 ? '#00f5ff' : '#00aa55') : '#ff4444';
const pulseRef = useRef<THREE.Mesh>(null);
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;
}
}
});
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>
<Text position={[0, -0.6, 0]} fontSize={0.2} color="white" anchorX="center" anchorY="middle">
{agent.name}
</Text>
<Text position={[0, -0.85, 0]} fontSize={0.15} color={color} anchorX="center" anchorY="middle">
{isOnline ? `${(agent.hashrate_15m).toFixed(0)} H/s` : 'OFFLINE'}
</Text>
{/* Connection Line */}
<Line points={[[0,0,0], [serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]]} color={isOnline ? '#004455' : '#330000'} lineWidth={1} transparent opacity={0.4} />
{/* Laser Pulse simulating hashing packets */}
{isOnline && isHashing && (
<LaserPulse start={[0,0,0]} end={[serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]} color="#00ffff" />
)}
</group>
);
}
export default function FleetTopologyMap({ agents }: { agents: Agent[] }) {
const serverPos: [number, number, number] = [0, 0, 0];
const agentNodes = useMemo(() => {
return agents.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] };
});
}, [agents]);
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, 245, 255, 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 // {agents.filter(a => a.status === 'online').length} NODES LINKED
</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="#00f5ff" />
<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>
<Text position={[0, -1.2, 0]} fontSize={0.35} color="#ffb020" anchorX="center" anchorY="middle">
MOTHERSHIP
</Text>
</group>
{/* Agent Nodes */}
{agentNodes.map((node) => (
<AgentNode key={node.agent.id} agent={node.agent} position={node.position} serverPos={serverPos} />
))}
<OrbitControls enablePan={true} enableZoom={true} enableRotate={true} autoRotate autoRotateSpeed={0.8} />
</Canvas>
</div>
);
}

View File

@@ -0,0 +1,43 @@
.matrix-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
background: black;
cursor: pointer;
overflow: hidden;
}
.matrix-canvas {
display: block;
}
.matrix-text-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
background: rgba(0, 0, 0, 0.7);
padding: 2rem;
border: 1px solid #0f0;
border-radius: 4px;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.2);
}
.matrix-text-overlay h2 {
color: #0f0;
margin: 0 0 1rem 0;
font-family: monospace;
font-size: 2rem;
text-shadow: 0 0 10px #0f0;
}
.matrix-close-hint {
color: rgba(0, 255, 0, 0.7);
font-family: monospace;
font-size: 1rem;
}

View File

@@ -0,0 +1,77 @@
import { useEffect, useRef } from 'react';
import { useWebSocket } from '../../hooks/useWebSocket';
import './MatrixStreamOverlay.css';
export default function MatrixStreamOverlay({ active, onClose }: { active: boolean; onClose: () => void }) {
const { recentShares } = useWebSocket();
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!active || !canvasRef.current) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
window.addEventListener('resize', resize);
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$+-*/=%""\'#&_(),.;:?!\\|{}<>[]^~';
const fontSize = 16;
let columns = canvas.width / fontSize;
let drops: number[] = Array(Math.floor(columns)).fill(1);
const draw = () => {
// Black BG for the canvas
// translucent BG to show trail
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0F0'; // Green text
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
let text = letters.charAt(Math.floor(Math.random() * letters.length));
// Occasionally drop a raw share payload in the stream
if (Math.random() > 0.99 && recentShares.length > 0) {
const share = recentShares[Math.floor(Math.random() * recentShares.length)];
text = JSON.stringify({ agent: share.agent_id?.substring(0,6), hash: share.hash?.substring(0,8), valid: share.accepted });
ctx.fillStyle = share.accepted ? '#00f5ff' : '#ff4444';
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
ctx.fillStyle = '#0F0'; // Reset color
} else {
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
}
// sending the drop back to the top randomly after it has crossed the screen
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
}
};
const interval = setInterval(draw, 33);
return () => {
clearInterval(interval);
window.removeEventListener('resize', resize);
};
}, [active, recentShares]);
if (!active) return null;
return (
<div className="matrix-overlay fade-in" onClick={onClose}>
<canvas ref={canvasRef} className="matrix-canvas" />
<div className="matrix-text-overlay">
<h2>RAW_SOCKET_STREAM [ACTIVE]</h2>
<div className="matrix-close-hint">Click anywhere to close terminal</div>
</div>
</div>
);
}

View File

@@ -10,6 +10,8 @@ import { FleetPipelineStatus, ActivityPulse } from '../components/Visual/VisualC
import { AlertBanner, PoolStatusPanel, AIActivityPanel, EarningsEstimator } from '../components/Fleet/FleetPanels';
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
import FleetToolbar from '../components/Fleet/FleetToolbar';
import FleetTopologyMap from '../components/Visual/3D/FleetTopologyMap';
import MatrixStreamOverlay from '../components/Visual/MatrixStreamOverlay';
import {
DEFAULT_FLEET_FILTERS,
filterFleetAgents,
@@ -34,6 +36,7 @@ export default function DashboardPage() {
const [filters, setFilters] = useState<FleetFilterState>(DEFAULT_FLEET_FILTERS);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [bulkBusy, setBulkBusy] = useState(false);
const [showMatrix, setShowMatrix] = useState(false);
useEffect(() => {
api.getRecentShares(20).then(setShares).catch(console.error);
api.listBuilds().then((b) => setHasBuilds(b.length > 0)).catch(console.error);
@@ -148,6 +151,9 @@ export default function DashboardPage() {
<span className="font-tech live-label">{isConnected ? 'SIGNAL LOCKED' : 'RECONNECTING'}</span>
<span className="live-sub">{agents.length} nodes registered</span>
</div>
<button className="button matrix-toggle-btn" onClick={() => setShowMatrix(true)} style={{ marginLeft: '1rem', background: 'transparent', border: '1px solid #0f0', color: '#0f0', fontFamily: 'monospace' }}>
[RAW_STREAM]
</button>
</div>
</header>
@@ -239,6 +245,10 @@ export default function DashboardPage() {
<ActivityPulse items={activityItems} />
</NeonCard>
<section className="section">
<FleetTopologyMap agents={agents} />
</section>
<section className="section">
<h2 className="section-title font-display">
<span className="section-ornament"></span> Machine Roster
@@ -354,6 +364,7 @@ export default function DashboardPage() {
</table>
</NeonCard>
</section>
<MatrixStreamOverlay active={showMatrix} onClose={() => setShowMatrix(false)} />
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
</footer>

View File

@@ -1,8 +1,36 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [react()],
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true
},
manifest: {
name: 'AetherForge Command Deck',
short_name: 'AetherForge',
theme_color: '#0d0d12',
background_color: '#000000',
display: 'standalone',
icons: [
{
src: '/vite.svg',
sizes: '192x192',
type: 'image/svg+xml'
},
{
src: '/vite.svg',
sizes: '512x512',
type: 'image/svg+xml'
}
]
}
})
],
server: {
port: 5173,
proxy: {