feat: Build Manager, Crucible ops deck, branding, and portable Cloudflare tunnel
Add Build Manager with pin-to-dropper, Crucible multi-node terminal with SSH probe/wake, Command Deck chart balance and pretty stats, AetherForge logo and sacred geometry UI, Field Guide refresh, and LAUNCH.bat Cloudflare MSI + token service install flow.
This commit is contained in:
@@ -43,25 +43,43 @@
|
||||
|
||||
.logo-emblem {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-gear {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 2px dashed rgba(201, 162, 39, 0.4);
|
||||
inset: -4px;
|
||||
border: 1px solid rgba(201, 162, 39, 0.22);
|
||||
border-radius: 50%;
|
||||
animation: gear-spin 20s linear infinite;
|
||||
animation: gear-spin 30s linear infinite;
|
||||
}
|
||||
|
||||
.logo-core {
|
||||
font-size: 1.5rem;
|
||||
filter: drop-shadow(0 0 8px var(--neon-amber));
|
||||
.logo-gear::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 5px;
|
||||
border: 1px dashed rgba(201, 162, 39, 0.14);
|
||||
border-radius: 50%;
|
||||
animation: gear-spin 18s linear infinite reverse;
|
||||
}
|
||||
|
||||
.logo-af-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
filter: drop-shadow(0 0 10px rgba(201, 162, 39, 0.7)) drop-shadow(0 0 3px rgba(255, 100, 0, 0.5));
|
||||
z-index: 1;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.logo-af-img:hover {
|
||||
filter: drop-shadow(0 0 16px rgba(201, 162, 39, 0.95)) drop-shadow(0 0 6px rgba(255, 140, 0, 0.7));
|
||||
}
|
||||
|
||||
.logo-text-block {
|
||||
|
||||
@@ -4,6 +4,8 @@ import AmbientBackground from '../Ambient/AmbientBackground';
|
||||
import SystemStatusBar from '../Visual/SystemStatusBar';
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import MatrixRain from './MatrixRain';
|
||||
import afLogo from '../../assets/af-logo.png';
|
||||
import CursorFire from '../Visual/CursorFire';
|
||||
import './Layout.css';
|
||||
|
||||
interface LayoutProps {
|
||||
@@ -13,7 +15,9 @@ interface LayoutProps {
|
||||
const NAV = [
|
||||
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
|
||||
{ to: '/agents', label: 'Fleet Roster', icon: 'fleet' },
|
||||
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
|
||||
{ to: '/forge', label: 'Forge', icon: 'forge' },
|
||||
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
||||
{ to: '/guide', label: 'Field Guide', icon: 'guide' },
|
||||
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
||||
] as const;
|
||||
@@ -41,6 +45,15 @@ function NavIcon({ type }: { type: string }) {
|
||||
<path d="M8 16l-2 4 4-2" />
|
||||
</svg>
|
||||
);
|
||||
case 'builds':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<rect x="3" y="14" width="18" height="5" rx="1" />
|
||||
<rect x="3" y="8" width="18" height="5" rx="1" />
|
||||
<rect x="3" y="2" width="18" height="5" rx="1" />
|
||||
<path d="M7 4.5h10M7 10.5h10M7 16.5h10" strokeOpacity="0.35" />
|
||||
</svg>
|
||||
);
|
||||
case 'guide':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
@@ -49,6 +62,15 @@ function NavIcon({ type }: { type: string }) {
|
||||
<path d="M8 7h8M8 11h6" />
|
||||
</svg>
|
||||
);
|
||||
case 'crucible':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M8 3h8l1 5H7L8 3z" />
|
||||
<path d="M7 8c0 5 2 8 5 10c3-2 5-5 5-10" />
|
||||
<path d="M4 21h16" />
|
||||
<path d="M10 12l1.5 2L14 11" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
@@ -118,17 +140,18 @@ export default function Layout({ children }: LayoutProps) {
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<CursorFire />
|
||||
<AmbientBackground />
|
||||
<nav className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<div className="logo">
|
||||
<div className="logo-emblem">
|
||||
<span className="logo-gear" />
|
||||
<span className="logo-core">⛏</span>
|
||||
<img src={afLogo} alt="AetherForge" className="logo-af-img" />
|
||||
</div>
|
||||
<div className="logo-text-block">
|
||||
<span className="logo-text">AetherForge</span>
|
||||
<span className="logo-tagline font-tech">LAN MINING COMMAND</span>
|
||||
<span className="logo-tagline font-tech">COMMAND DECK</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import { useForge } from '../../context/ForgeContext';
|
||||
|
||||
// Full matrix alphabet: katakana + hex + braille dots for visual density
|
||||
const KATAKANA =
|
||||
@@ -18,10 +19,22 @@ interface Column {
|
||||
livePos: number;
|
||||
}
|
||||
|
||||
const FORGE_STRINGS = [
|
||||
'COMPILING', 'LINKING', 'GARBLE', 'GO BUILD', 'INJECT',
|
||||
'STEALTH', 'PERSIST', 'ENCRYPT', 'OBFUSC', 'PACKAGE',
|
||||
'WORKER', 'FORGE', 'SIGN', 'BUNDLE', 'AGENT',
|
||||
'RANDOMX', 'STRATUM', 'C2CONN', 'DEPLOY',
|
||||
];
|
||||
|
||||
export default function MatrixRain() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const { agents, recentShares, commandResults } = useWebSocket();
|
||||
const { forging, stage } = useForge();
|
||||
const forgingRef = useRef(false);
|
||||
const stageRef = useRef('');
|
||||
forgingRef.current = forging;
|
||||
stageRef.current = stage;
|
||||
|
||||
// ── Live data pool ──────────────────────────────────────────────────────────
|
||||
// Collect strings from the fleet that will be injected character-by-character
|
||||
@@ -40,6 +53,13 @@ export default function MatrixRain() {
|
||||
for (const r of (commandResults ?? []).slice(-5)) {
|
||||
if (r.action) pool.push(r.action.toUpperCase().padEnd(8, '_'));
|
||||
}
|
||||
// When forging, flood the pool with build strings so they dominate the rain
|
||||
if (forgingRef.current) {
|
||||
pool.push(...FORGE_STRINGS);
|
||||
if (stageRef.current) {
|
||||
pool.push(stageRef.current.replace(/[^A-Z0-9]/gi, '').toUpperCase().slice(0, 16));
|
||||
}
|
||||
}
|
||||
livePoolRef.current = pool.length > 0 ? pool : ['AETHERFORGE', 'MINING', '00E5FF'];
|
||||
}, [agents, recentShares, commandResults]);
|
||||
|
||||
@@ -109,23 +129,27 @@ export default function MatrixRain() {
|
||||
|
||||
let raf: number;
|
||||
let lastTime = 0;
|
||||
const FPS = 24; // keep CPU gentle
|
||||
const MS_PER_FRAME = 1000 / FPS;
|
||||
|
||||
const draw = (ts: number) => {
|
||||
raf = requestAnimationFrame(draw);
|
||||
if (ts - lastTime < MS_PER_FRAME) return;
|
||||
const isForging = forgingRef.current;
|
||||
const targetFps = isForging ? 50 : 24;
|
||||
const msPerFrame = 1000 / targetFps;
|
||||
if (ts - lastTime < msPerFrame) return;
|
||||
lastTime = ts;
|
||||
|
||||
const W = canvas.width;
|
||||
const H = canvas.height;
|
||||
|
||||
// Fade trail
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.18)';
|
||||
// Forge mode: less fade = longer glowing trails; normal: quick fade
|
||||
ctx.fillStyle = isForging ? 'rgba(0,0,0,0.10)' : 'rgba(0,0,0,0.18)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
ctx.font = `${FONT_SIZE}px 'Courier New', monospace`;
|
||||
|
||||
// Speed multiplier: 3× faster while forging
|
||||
const speedMult = isForging ? 3.2 : 1.0;
|
||||
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
const col = cols[i];
|
||||
const x = i * FONT_SIZE;
|
||||
@@ -140,33 +164,52 @@ export default function MatrixRain() {
|
||||
ch = ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
|
||||
}
|
||||
|
||||
// Head character — bright white flash
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.95)';
|
||||
ctx.fillText(ch, x, y * FONT_SIZE);
|
||||
|
||||
// Second char — bright cyan-green (neon)
|
||||
if (y > 1) {
|
||||
ctx.fillStyle = '#00ff41';
|
||||
ctx.fillText(
|
||||
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
|
||||
x,
|
||||
(y - 1) * FONT_SIZE,
|
||||
);
|
||||
if (isForging) {
|
||||
// Forge palette: bright amber/orange head, orange body
|
||||
ctx.fillStyle = 'rgba(255,220,80,0.98)';
|
||||
ctx.fillText(ch, x, y * FONT_SIZE);
|
||||
if (y > 1) {
|
||||
ctx.fillStyle = '#ff8c00';
|
||||
ctx.fillText(
|
||||
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
|
||||
x,
|
||||
(y - 1) * FONT_SIZE,
|
||||
);
|
||||
}
|
||||
// Extra mid-column glyph density during forge
|
||||
if (Math.random() < 0.12) {
|
||||
const dimY = Math.floor(Math.random() * Math.max(1, y - 2));
|
||||
ctx.fillStyle = 'rgba(255,120,0,0.35)';
|
||||
ctx.fillText(
|
||||
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
|
||||
x,
|
||||
dimY * FONT_SIZE,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Normal palette: white head, cyan-green body
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.95)';
|
||||
ctx.fillText(ch, x, y * FONT_SIZE);
|
||||
if (y > 1) {
|
||||
ctx.fillStyle = '#00ff41';
|
||||
ctx.fillText(
|
||||
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
|
||||
x,
|
||||
(y - 1) * FONT_SIZE,
|
||||
);
|
||||
}
|
||||
if (Math.random() < 0.04) {
|
||||
const dimY = Math.floor(Math.random() * Math.max(1, y - 2));
|
||||
ctx.fillStyle = 'rgba(0,180,60,0.22)';
|
||||
ctx.fillText(
|
||||
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
|
||||
x,
|
||||
dimY * FONT_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dim previous chars handled by fade overlay above.
|
||||
// Occasionally render a mid-column dim glyph for density.
|
||||
if (Math.random() < 0.04) {
|
||||
const dimY = Math.floor(Math.random() * (y - 2));
|
||||
ctx.fillStyle = 'rgba(0,180,60,0.22)';
|
||||
ctx.fillText(
|
||||
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
|
||||
x,
|
||||
dimY * FONT_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
col.y += col.speed;
|
||||
col.y += col.speed * speedMult;
|
||||
if (col.y * FONT_SIZE > H && Math.random() > 0.96) {
|
||||
col.y = Math.random() * -20;
|
||||
col.speed = 0.3 + Math.random() * 0.55;
|
||||
@@ -179,13 +222,16 @@ export default function MatrixRain() {
|
||||
const logs = eventLogRef.current;
|
||||
const lineH = FONT_SIZE + 2;
|
||||
ctx.font = `${FONT_SIZE - 1}px 'Courier New', monospace`;
|
||||
const isForging2 = forgingRef.current;
|
||||
for (let j = 0; j < logs.length; j++) {
|
||||
const entry = logs[logs.length - 1 - j];
|
||||
const oy = H - 6 - j * lineH;
|
||||
if (oy < 0) break;
|
||||
ctx.fillStyle = `rgba(0,255,65,${(entry.alpha * 0.55).toFixed(2)})`;
|
||||
const logColor = isForging2
|
||||
? `rgba(255,160,0,${(entry.alpha * 0.75).toFixed(2)})`
|
||||
: `rgba(0,255,65,${(entry.alpha * 0.55).toFixed(2)})`;
|
||||
ctx.fillStyle = logColor;
|
||||
ctx.fillText(`> ${entry.text}`, 4, oy);
|
||||
// fade over time
|
||||
entry.alpha = Math.max(0, entry.alpha - 0.003);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user