diff --git a/run.bat b/run.bat index 773f877..82f26dc 100644 --- a/run.bat +++ b/run.bat @@ -15,49 +15,57 @@ echo [1/5] Checking dependencies... where go >nul 2>nul if %ERRORLEVEL% neq 0 ( - echo Go not found. Downloading and installing Go... + echo Go not found. Checking for existing installation... - :: Detect architecture - if "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( - set GO_ARCH=amd64 + :: Check common install paths + if exist "C:\Program Files\Go\bin\go.exe" ( + echo Found Go at C:\Program Files\Go\bin + set "PATH=C:\Program Files\Go\bin;%PATH%" ) else ( - set GO_ARCH=386 + echo Downloading and installing Go... + + :: Detect architecture + if "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( + set GO_ARCH=amd64 + ) else ( + set GO_ARCH=386 + ) + + set GO_VERSION=1.22.2 + set GO_URL=https://go.dev/dl/go%GO_VERSION%.windows-%GO_ARCH%.msi + set GO_MSI=%TEMP%\go-installer.msi + + echo Downloading Go %GO_VERSION% for Windows %GO_ARCH%... + echo (This may take a moment...) + + :: Download using PowerShell (built into Windows) + powershell -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%GO_URL%' -OutFile '%GO_MSI%' }" + + if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to download Go installer. + echo Please manually download from: https://go.dev/dl/ + pause + exit /b 1 + ) + + echo Installing Go (this may require administrator privileges)... + msiexec /i "%GO_MSI%" /quiet /norestart + + if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to install Go. Try running as Administrator. + pause + exit /b 1 + ) + + :: Clean up installer + del "%GO_MSI%" 2>nul + + :: Add Go to PATH for this session + set "PATH=C:\Program Files\Go\bin;%PATH%" + set "PATH=%USERPROFILE%\go\bin;%PATH%" + + echo Go installed successfully! ) - - set GO_VERSION=1.22.2 - set GO_URL=https://go.dev/dl/go%GO_VERSION%.windows-%GO_ARCH%.msi - set GO_MSI=%TEMP%\go-installer.msi - - echo Downloading Go %GO_VERSION% for Windows %GO_ARCH%... - echo (This may take a moment...) - - :: Download using PowerShell (built into Windows) - powershell -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%GO_URL%' -OutFile '%GO_MSI%' }" - - if %ERRORLEVEL% neq 0 ( - echo ERROR: Failed to download Go installer. - echo Please manually download from: https://go.dev/dl/ - pause - exit /b 1 - ) - - echo Installing Go (this may require administrator privileges)... - msiexec /i "%GO_MSI%" /quiet /norestart - - if %ERRORLEVEL% neq 0 ( - echo ERROR: Failed to install Go. Try running as Administrator. - pause - exit /b 1 - ) - - :: Clean up installer - del "%GO_MSI%" 2>nul - - :: Add Go to PATH for this session - set PATH=%PATH%;C:\Program Files\Go\bin - set PATH=%PATH%;%USERPROFILE%\go\bin - - echo Go installed successfully! ) else ( echo Go found: call go version @@ -68,34 +76,41 @@ if %ERRORLEVEL% neq 0 ( :: ============================================================ where node >nul 2>nul if %ERRORLEVEL% neq 0 ( - echo Node.js not found. Downloading and installing Node.js... + echo Node.js not found. Checking for existing installation... - set NODE_VERSION=20.12.2 - set NODE_URL=https://nodejs.org/dist/v%NODE_VERSION%/node-v%NODE_VERSION%-x64.msi - set NODE_MSI=%TEMP%\node-installer.msi - - echo Downloading Node.js v%NODE_VERSION%... - echo (This may take a moment...) - - powershell -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%NODE_URL%' -OutFile '%NODE_MSI%' }" - - if %ERRORLEVEL% neq 0 ( - echo WARNING: Failed to download Node.js. Frontend will not be built. - echo You can manually download from: https://nodejs.org/ - set SKIP_FRONTEND=1 + if exist "C:\Program Files\nodejs\node.exe" ( + echo Found Node.js at C:\Program Files\nodejs + set "PATH=C:\Program Files\nodejs;%PATH%" ) else ( - echo Installing Node.js (this may require administrator privileges)... - msiexec /i "%NODE_MSI%" /quiet /norestart + echo Downloading and installing Node.js... + + set NODE_VERSION=20.12.2 + set NODE_URL=https://nodejs.org/dist/v%NODE_VERSION%/node-v%NODE_VERSION%-x64.msi + set NODE_MSI=%TEMP%\node-installer.msi + + echo Downloading Node.js v%NODE_VERSION%... + echo (This may take a moment...) + + powershell -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%NODE_URL%' -OutFile '%NODE_MSI%' }" if %ERRORLEVEL% neq 0 ( - echo WARNING: Failed to install Node.js. Frontend will not be built. + echo WARNING: Failed to download Node.js. Frontend will not be built. + echo You can manually download from: https://nodejs.org/ set SKIP_FRONTEND=1 ) else ( - :: Add Node to PATH for this session - set PATH=%PATH%;C:\Program Files\nodejs + echo Installing Node.js (this may require administrator privileges)... + msiexec /i "%NODE_MSI%" /quiet /norestart - del "%NODE_MSI%" 2>nul - echo Node.js installed successfully! + if %ERRORLEVEL% neq 0 ( + echo WARNING: Failed to install Node.js. Frontend will not be built. + set SKIP_FRONTEND=1 + ) else ( + :: Add Node to PATH for this session + set "PATH=C:\Program Files\nodejs;%PATH%" + + del "%NODE_MSI%" 2>nul + echo Node.js installed successfully! + ) ) ) ) else ( @@ -109,32 +124,35 @@ if %ERRORLEVEL% neq 0 ( echo [2/5] Creating data directories... if not exist "data\builds" mkdir "data\builds" if not exist "data\logs" mkdir "data\logs" -echo Done. +if not exist "data\blueprints" mkdir "data\blueprints" +echo Created: data\builds, data\logs, data\blueprints :: ============================================================ :: STEP 4: Build frontend :: ============================================================ if "%SKIP_FRONTEND%"=="" ( echo [3/5] Building frontend dashboard... + echo Running: npm install ^&^& npm run build in server\web\ cd server\web if not exist "node_modules" ( echo Installing npm dependencies... call npm install + if %ERRORLEVEL% neq 0 ( + echo ERROR: npm install failed + cd ..\.. + pause + exit /b 1 + ) + echo npm install completed successfully. ) + echo Building frontend with Vite... call npm run build if %ERRORLEVEL% neq 0 ( echo WARNING: Frontend build failed, server will run without dashboard. ) else ( - echo Frontend built: server\web\dist + echo Frontend built successfully: server\web\dist ) cd ..\.. - - :: Copy frontend dist to server's webroot directory - if exist "server\web\dist" ( - if not exist "server\webroot" mkdir "server\webroot" - xcopy /E /I /Y "server\web\dist\*" "server\webroot\" >nul - echo Frontend files copied to server\webroot - ) ) else ( echo [3/5] Skipping frontend build (Node.js not available) ) @@ -143,6 +161,8 @@ if "%SKIP_FRONTEND%"=="" ( :: STEP 5: Build server :: ============================================================ echo [4/5] Building server... +echo Running: go build in server\... + cd server :: Download Go module dependencies @@ -152,6 +172,7 @@ if %ERRORLEVEL% neq 0 ( echo WARNING: go mod download failed, trying build anyway... ) +echo Compiling server binary... go build -ldflags="-s -w" -o "..\bin\miner-server.exe" . if %ERRORLEVEL% neq 0 ( echo ERROR: Build failed @@ -159,36 +180,52 @@ if %ERRORLEVEL% neq 0 ( exit /b 1 ) cd .. -echo Server built: bin\miner-server.exe +echo Server built successfully: bin\miner-server.exe :: ============================================================ :: LAUNCH :: ============================================================ echo [5/5] Starting server on port 8989... echo. + +:: Detect LAN IP for display for /f "usebackq delims=" %%I in (`powershell -NoProfile -Command "(Get-NetIPAddress -AddressFamily IPv4 ^| Where-Object { $_.IPAddress -notlike '127.*' -and $_.PrefixOrigin -ne 'WellKnown' } ^| Select-Object -First 1 -ExpandProperty IPAddress)"`) do set LAN_IP=%%I if not defined LAN_IP set LAN_IP=localhost + echo. -echo ╔══════════════════════════════════════════════════╗ -echo ║ Crypto Miner Control Server ║ -echo ║ ║ -echo ║ Dashboard: http://%LAN_IP%:8989 ║ -echo ║ Local: http://localhost:8989 ║ -echo ║ WebSocket: ws://%LAN_IP%:8989/ws/agent ║ -echo ║ ║ -echo ║ 1. Open dashboard on your LAN ║ -echo ║ 2. Configure Settings ║ -echo ║ 3. Build install-{worker}.exe in Builder ║ -echo ║ 4. Run that exe once on each Windows machine ║ -echo ║ ║ -echo ║ Data: %CD%\data\ ║ -echo ║ Press Ctrl+C to stop the server ║ -echo ╚══════════════════════════════════════════════════╝ +echo ╔══════════════════════════════════════════════════════════════╗ +echo ║ SERVER IS NOW LIVE ║ +echo ║ ║ +echo ║ Dashboard URL: http://0.0.0.0:8989 ║ +echo ║ LAN Access: http://%LAN_IP%:8989 ║ +echo ║ Local: http://localhost:8989 ║ +echo ║ ║ +echo ║ WebSocket (agents): ws://%LAN_IP%:8989/ws/agent ║ +echo ║ WebSocket (dash): ws://%LAN_IP%:8989/ws/dashboard ║ +echo ║ ║ +echo ║ Quick Start: ║ +echo ║ 1. Open Dashboard in your browser ║ +echo ║ 2. Go to Settings - configure pool ^& wallet ║ +echo ║ 3. Go to Builder - build your miner installer .exe ║ +echo ║ 4. Run that .exe on each Windows machine ║ +echo ║ ║ +echo ║ Data Directory: %CD%\data\ ║ +echo ║ Config File: %CD%\data\config.json ║ +echo ║ Blueprints: %CD%\data\blueprints\ ║ +echo ║ ║ +echo ║ Press Ctrl+C in this window to stop the server ║ +echo ╚══════════════════════════════════════════════════════════════╝ echo. +:: Open browser start http://localhost:8989 -:: Run server +:: Run server (this blocks until Ctrl+C) +echo [Server] Starting miner-server.exe on 0.0.0.0:8989... +echo [Server] Log output below (Ctrl+C to stop): +echo. .\bin\miner-server.exe -port 8989 -data ".\data" +echo. +echo [Server] Server stopped. pause diff --git a/server/web/index.html b/server/web/index.html index 3525518..4c1a617 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -4,7 +4,7 @@ - Crypto Miner Command Deck + AetherForge — LAN Mining Command Deck
diff --git a/server/web/src/components/Ambient/AmbientBackground.css b/server/web/src/components/Ambient/AmbientBackground.css new file mode 100644 index 0000000..9dc72e1 --- /dev/null +++ b/server/web/src/components/Ambient/AmbientBackground.css @@ -0,0 +1,103 @@ +.ambient-bg { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + overflow: hidden; +} + +.ambient-grid { + position: absolute; + inset: -50%; + background-image: + linear-gradient(rgba(201, 162, 39, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(201, 162, 39, 0.04) 1px, transparent 1px), + linear-gradient(rgba(0, 245, 255, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 245, 255, 0.02) 1px, transparent 1px); + background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px; + animation: grid-drift 40s linear infinite; + transform: perspective(500px) rotateX(60deg) scale(2); + transform-origin: center top; + opacity: 0.6; +} + +.ambient-vignette { + position: absolute; + inset: 0; + background: radial-gradient(ellipse at center, transparent 0%, var(--bg-void) 75%); +} + +.ambient-orb { + position: absolute; + border-radius: 50%; + filter: blur(80px); + animation: float-orb 12s ease-in-out infinite; +} + +.ambient-orb-cyan { + width: 400px; + height: 400px; + background: rgba(0, 245, 255, 0.12); + top: 10%; + right: 5%; + animation-delay: 0s; +} + +.ambient-orb-magenta { + width: 350px; + height: 350px; + background: rgba(255, 45, 166, 0.08); + bottom: 20%; + left: 10%; + animation-delay: -4s; +} + +.ambient-orb-amber { + width: 300px; + height: 300px; + background: rgba(255, 176, 32, 0.06); + top: 50%; + left: 40%; + animation-delay: -8s; +} + +.ambient-scanline { + position: absolute; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, transparent, rgba(0, 245, 255, 0.15), transparent); + animation: scanline 8s linear infinite; + opacity: 0.5; +} + +.ambient-gear { + position: absolute; + width: 120px; + height: 120px; + border: 3px dashed rgba(201, 162, 39, 0.15); + border-radius: 50%; + opacity: 0.25; +} + +.ambient-gear::before { + content: ''; + position: absolute; + inset: 20%; + border: 2px solid rgba(201, 162, 39, 0.2); + border-radius: 50%; +} + +.ambient-gear-1 { + top: 15%; + left: 5%; + animation: gear-spin 60s linear infinite; +} + +.ambient-gear-2 { + bottom: 10%; + right: 8%; + width: 80px; + height: 80px; + animation: gear-spin 45s linear infinite reverse; +} diff --git a/server/web/src/components/Ambient/AmbientBackground.tsx b/server/web/src/components/Ambient/AmbientBackground.tsx new file mode 100644 index 0000000..bb9b025 --- /dev/null +++ b/server/web/src/components/Ambient/AmbientBackground.tsx @@ -0,0 +1,16 @@ +import './AmbientBackground.css'; + +export default function AmbientBackground() { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/server/web/src/components/Charts/GaugeRing.css b/server/web/src/components/Charts/GaugeRing.css new file mode 100644 index 0000000..a11d777 --- /dev/null +++ b/server/web/src/components/Charts/GaugeRing.css @@ -0,0 +1,56 @@ +.gauge-ring { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.gauge-ring-svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); +} + +.gauge-ring-track { + fill: none; + stroke: rgba(201, 162, 39, 0.15); + stroke-width: 6; +} + +.gauge-ring-fill { + fill: none; + stroke-width: 6; + stroke-linecap: round; + transition: stroke-dashoffset 0.8s cubic-bezier(0.23, 1, 0.32, 1); +} + +.gauge-ring-center { + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 0.5rem; +} + +.gauge-ring-value { + font-size: 1.1rem; + font-weight: 700; + color: var(--neon-cyan); + text-shadow: 0 0 12px rgba(0, 245, 255, 0.5); + line-height: 1.2; +} + +.gauge-ring-label { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--brass-light); + margin-top: 0.15rem; +} + +.gauge-ring-sub { + font-size: 0.6rem; + color: var(--text-muted); + margin-top: 0.1rem; +} diff --git a/server/web/src/components/Charts/GaugeRing.tsx b/server/web/src/components/Charts/GaugeRing.tsx new file mode 100644 index 0000000..2df5af8 --- /dev/null +++ b/server/web/src/components/Charts/GaugeRing.tsx @@ -0,0 +1,66 @@ +import './GaugeRing.css'; + +interface GaugeRingProps { + value: number; + max?: number; + label: string; + sublabel?: string; + color?: string; + size?: number; +} + +export default function GaugeRing({ + value, + max = 100, + label, + sublabel, + color = 'var(--neon-cyan)', + size = 100, +}: GaugeRingProps) { + const pct = Math.min(100, Math.max(0, (value / max) * 100)); + const circumference = 2 * Math.PI * 42; + const offset = circumference - (pct / 100) * circumference; + + return ( +
+ + + + + + + + + + + + + + + + + +
+ {formatValue(value, max)} + {label} + {sublabel && {sublabel}} +
+
+ ); +} + +function formatValue(v: number, max: number): string { + if (max <= 100 && max > 1) return `${v.toFixed(0)}%`; + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`; + if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`; + return v.toFixed(0); +} diff --git a/server/web/src/components/Charts/HashrateChart.css b/server/web/src/components/Charts/HashrateChart.css new file mode 100644 index 0000000..518acb2 --- /dev/null +++ b/server/web/src/components/Charts/HashrateChart.css @@ -0,0 +1,74 @@ +.neon-chart-panel { + position: relative; + min-height: 200px; +} + +.chart-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgba(201, 162, 39, 0.2); +} + +.chart-title { + font-size: 1rem; + font-weight: 700; + color: var(--brass-light); + margin: 0; +} + +.chart-live { + font-family: var(--font-tech); + font-size: 0.65rem; + letter-spacing: 0.15em; + color: var(--neon-green); + text-shadow: 0 0 8px rgba(57, 255, 20, 0.6); +} + +.chart-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 260px; + color: var(--text-muted); + gap: 0.5rem; +} + +.chart-empty-icon { + font-size: 2.5rem; + color: var(--brass); + opacity: 0.5; + animation: pulse-glow 3s ease-in-out infinite; +} + +.neon-tooltip { + background: linear-gradient(145deg, rgba(20, 16, 12, 0.98), rgba(8, 6, 4, 0.99)); + border: 1px solid var(--border-brass); + border-radius: 2px; + padding: 0.75rem 1rem; + box-shadow: 0 0 20px rgba(0, 245, 255, 0.15), var(--shadow-panel); +} + +.neon-tooltip-time { + font-size: 0.65rem; + color: var(--text-muted); + letter-spacing: 0.1em; + margin-bottom: 0.35rem; +} + +.neon-tooltip-value { + font-family: var(--font-tech); + font-size: 1.25rem; + font-weight: 700; +} + +.neon-tooltip-label { + font-size: 0.7rem; + color: var(--brass-light); + margin-top: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} diff --git a/server/web/src/components/Charts/HashrateChart.tsx b/server/web/src/components/Charts/HashrateChart.tsx index 762958a..a79ab48 100644 --- a/server/web/src/components/Charts/HashrateChart.tsx +++ b/server/web/src/components/Charts/HashrateChart.tsx @@ -7,6 +7,7 @@ import { XAxis, YAxis, } from 'recharts'; +import './HashrateChart.css'; export interface ChartPoint { time: string; @@ -19,48 +20,149 @@ interface HashrateChartProps { title?: string; color?: string; unit?: string; + height?: number; } -export default function HashrateChart({ data, title, color = '#06b6d4', unit = 'H/s' }: HashrateChartProps) { +const GRAD_IDS = ['cyan', 'magenta', 'amber', 'green', 'purple'] as const; + +function colorToId(color: string): string { + if (color.includes('f5ff') || color.includes('06b6d4') || color === '#00f5ff') return 'cyan'; + if (color.includes('2da6') || color.includes('8b5cf6')) return 'magenta'; + if (color.includes('b020') || color.includes('eab308')) return 'amber'; + if (color.includes('39ff') || color.includes('22c55e')) return 'green'; + return 'purple'; +} + +export default function HashrateChart({ + data, + title, + color = '#00f5ff', + unit = 'H/s', + height = 280, +}: HashrateChartProps) { + const gradId = colorToId(color); + if (data.length === 0) { return ( -
-

{title || 'Chart'} — waiting for data...

+
+
+

{title || 'Telemetry'}

+ Awaiting signal from fleet...
); } return ( -
- {title &&

{title}

} - - +
+ {title && ( +
+

{title}

+ ● LIVE +
+ )} + + - - - + + + + + + + + + + + - - - formatShort(v, unit)} /> - [formatShort(value, unit), title || 'Value']} + + + formatShort(v, unit)} + /> + ( + + )} + /> + -
); } +function NeonTooltip({ + active, + payload, + label, + unit, + title, + color, +}: { + active?: boolean; + payload?: { value: number }[]; + label?: string; + unit: string; + title?: string; + color: string; +}) { + if (!active || !payload?.length) return null; + const v = payload[0].value; + return ( +
+
{label}
+
+ {formatFull(v, unit)} +
+
{title || 'Reading'}
+
+ ); +} + function formatShort(v: number, unit: string): string { if (unit === 'H/s') { if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`; if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`; return `${v.toFixed(0)}`; } - return `${v.toFixed(1)}${unit === '%' ? '%' : ''}`; + return `${v.toFixed(0)}${unit === '%' ? '%' : ''}`; +} + +function formatFull(v: number, unit: string): string { + if (unit === 'H/s') { + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(3)} MH/s`; + if (v >= 1_000) return `${(v / 1_000).toFixed(2)} KH/s`; + return `${v.toFixed(0)} H/s`; + } + return `${v.toFixed(1)}${unit}`; } diff --git a/server/web/src/components/HelpTip.css b/server/web/src/components/HelpTip.css index d665006..9bb15d6 100644 --- a/server/web/src/components/HelpTip.css +++ b/server/web/src/components/HelpTip.css @@ -12,34 +12,28 @@ justify-content: center; width: 1rem; height: 1rem; - border-radius: 999px; - background: rgba(6, 182, 212, 0.15); - color: var(--accent-cyan); + border-radius: 2px; + background: rgba(0, 245, 255, 0.1); + border: 1px solid rgba(0, 245, 255, 0.35); + color: var(--neon-cyan); font-size: 0.6875rem; font-weight: 700; -} - -.help-tip-label { - font-size: 0.75rem; - color: var(--text-muted); -} - -.cheat-sheet { - display: flex; - flex-direction: column; - gap: 0.75rem; + box-shadow: 0 0 8px rgba(0, 245, 255, 0.2); } .cheat-sheet-item { padding: 0.75rem; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 8px; + background: rgba(8, 6, 4, 0.6); + border: 1px solid var(--border-brass); + border-radius: 2px; + border-left: 3px solid var(--neon-cyan); } .cheat-sheet-item h4 { - font-size: 0.8125rem; - color: var(--accent-cyan); + font-family: var(--font-tech); + font-size: 0.75rem; + letter-spacing: 0.1em; + color: var(--neon-cyan); margin-bottom: 0.25rem; } diff --git a/server/web/src/components/Layout/Layout.css b/server/web/src/components/Layout/Layout.css index d2fe6c3..3335339 100644 --- a/server/web/src/components/Layout/Layout.css +++ b/server/web/src/components/Layout/Layout.css @@ -1,12 +1,14 @@ .layout { display: flex; min-height: 100vh; + position: relative; } .sidebar { - width: 240px; - background: var(--bg-secondary); - border-right: 1px solid var(--border-color); + width: 260px; + background: linear-gradient(180deg, rgba(16, 12, 8, 0.97) 0%, rgba(8, 6, 4, 0.98) 100%); + border-right: 1px solid var(--border-brass); + box-shadow: 4px 0 40px rgba(0, 0, 0, 0.5), inset -1px 0 0 rgba(0, 245, 255, 0.08); display: flex; flex-direction: column; position: fixed; @@ -14,114 +16,217 @@ left: 0; bottom: 0; z-index: 100; + backdrop-filter: blur(16px); +} + +.sidebar::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 1px; + height: 100%; + background: linear-gradient(180deg, var(--neon-cyan), transparent 30%, transparent 70%, var(--neon-magenta)); + opacity: 0.4; } .sidebar-header { - padding: 1.25rem 1rem; - border-bottom: 1px solid var(--border-color); + padding: 1.5rem 1.25rem; + border-bottom: 1px solid var(--border-brass); } .logo { display: flex; align-items: center; - gap: 0.75rem; + gap: 1rem; } -.logo-icon { +.logo-emblem { + position: relative; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.logo-gear { + position: absolute; + inset: 0; + border: 2px dashed rgba(201, 162, 39, 0.4); + border-radius: 50%; + animation: gear-spin 20s linear infinite; +} + +.logo-core { font-size: 1.5rem; + filter: drop-shadow(0 0 8px var(--neon-amber)); + z-index: 1; +} + +.logo-text-block { + display: flex; + flex-direction: column; + gap: 0.15rem; } .logo-text { - font-size: 1.25rem; + font-family: var(--font-display); + font-size: 1.15rem; font-weight: 700; - background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue)); + background: linear-gradient(135deg, var(--brass-light) 0%, var(--neon-cyan) 50%, var(--brass) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + letter-spacing: 0.06em; +} + +.logo-tagline { + font-size: 0.55rem; + letter-spacing: 0.2em; + color: var(--text-muted); } .sidebar-nav { flex: 1; - padding: 0.75rem 0.5rem; + padding: 1rem 0.75rem; display: flex; flex-direction: column; - gap: 0.25rem; + gap: 0.35rem; } .nav-item { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - border-radius: 8px; + gap: 0.85rem; + padding: 0.85rem 1rem; + border-radius: 2px; color: var(--text-secondary); text-decoration: none; - font-size: 0.875rem; - font-weight: 500; - transition: all 0.2s ease; + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.04em; + transition: all 0.25s ease; + position: relative; + border: 1px solid transparent; } .nav-item:hover { - background: var(--bg-hover); - color: var(--text-primary); + background: rgba(201, 162, 39, 0.08); + color: var(--brass-light); + border-color: rgba(201, 162, 39, 0.2); text-decoration: none; } .nav-item.active { - background: rgba(59, 130, 246, 0.15); - color: var(--accent-blue); + background: linear-gradient(90deg, rgba(0, 245, 255, 0.12), transparent); + color: var(--neon-cyan); + border-color: rgba(0, 245, 255, 0.35); + box-shadow: inset 0 0 20px rgba(0, 245, 255, 0.08); } .nav-icon { - font-size: 1.125rem; - width: 1.5rem; - text-align: center; + width: 1.35rem; + height: 1.35rem; + display: flex; + align-items: center; + justify-content: center; } -.nav-label { - white-space: nowrap; +.nav-icon svg { + width: 100%; + height: 100%; +} + +.nav-glow { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: var(--neon-cyan); + box-shadow: 0 0 10px var(--neon-cyan); + border-radius: 0 2px 2px 0; } .sidebar-footer { - padding: 1rem; - border-top: 1px solid var(--border-color); + padding: 1.25rem; + border-top: 1px solid var(--border-brass); +} + +.power-meter { + margin-bottom: 0.75rem; +} + +.power-label { + font-size: 0.6rem; + letter-spacing: 0.2em; + color: var(--text-muted); + display: block; + margin-bottom: 0.35rem; +} + +.power-bar { + height: 4px; + background: rgba(201, 162, 39, 0.15); + border-radius: 2px; + overflow: hidden; +} + +.power-fill { + height: 100%; + width: 78%; + background: linear-gradient(90deg, var(--brass-dark), var(--neon-cyan)); + box-shadow: 0 0 8px rgba(0, 245, 255, 0.4); + animation: shimmer 3s ease-in-out infinite; + background-size: 200% 100%; } .version-badge { - font-size: 0.75rem; + font-size: 0.65rem; color: var(--text-muted); text-align: center; + letter-spacing: 0.15em; } .main-content { flex: 1; - margin-left: 240px; - padding: 1.5rem 2rem; + margin-left: 260px; + padding: 2rem 2.5rem; min-height: 100vh; + position: relative; + z-index: 1; } @media (max-width: 768px) { .sidebar { - width: 60px; + width: 72px; } - .logo-text, + .logo-text-block, .nav-label, - .sidebar-footer { + .power-meter, + .version-badge { display: none; } .sidebar-header { - padding: 1rem 0.75rem; + padding: 1rem 0.5rem; + justify-content: center; + } + + .logo { + justify-content: center; } .nav-item { justify-content: center; - padding: 0.75rem; + padding: 0.85rem; } .main-content { - margin-left: 60px; + margin-left: 72px; padding: 1rem; } } diff --git a/server/web/src/components/Layout/Layout.tsx b/server/web/src/components/Layout/Layout.tsx index 4d5d4cf..5cad7b2 100644 --- a/server/web/src/components/Layout/Layout.tsx +++ b/server/web/src/components/Layout/Layout.tsx @@ -1,49 +1,100 @@ import { ReactNode } from 'react'; -import { NavLink } from 'react-router-dom'; +import { NavLink, useLocation } from 'react-router-dom'; +import AmbientBackground from '../Ambient/AmbientBackground'; import './Layout.css'; interface LayoutProps { children: ReactNode; } +const NAV = [ + { to: '/dashboard', label: 'Command Deck', icon: 'deck' }, + { to: '/agents', label: 'Fleet Roster', icon: 'fleet' }, + { to: '/builder', label: 'Forge', icon: 'forge' }, + { to: '/settings', label: 'Calibrate', icon: 'gear' }, +] as const; + +function NavIcon({ type }: { type: string }) { + switch (type) { + case 'deck': + return ( + + + + + ); + case 'fleet': + return ( + + + + + ); + case 'forge': + return ( + + + + + ); + default: + return ( + + + + + ); + } +} + export default function Layout({ children }: LayoutProps) { + const location = useLocation(); + return (
+ -
- {children} -
+
{children}
); } diff --git a/server/web/src/components/NeonCard/NeonCard.css b/server/web/src/components/NeonCard/NeonCard.css new file mode 100644 index 0000000..6b87848 --- /dev/null +++ b/server/web/src/components/NeonCard/NeonCard.css @@ -0,0 +1,53 @@ +.neon-card { + padding: 1.5rem; +} + +.neon-card-rim { + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient( + 135deg, + var(--brass-light) 0%, + transparent 25%, + transparent 75%, + var(--neon-cyan) 100% + ); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0.6; + z-index: 2; +} + +.neon-card-cyan .neon-card-rim { + background: linear-gradient(135deg, var(--neon-cyan), transparent 40%, var(--brass) 100%); + opacity: 0.8; +} + +.neon-card-magenta .neon-card-rim { + background: linear-gradient(135deg, var(--neon-magenta), transparent 50%, var(--brass) 100%); +} + +.neon-card-amber .neon-card-rim { + background: linear-gradient(135deg, var(--neon-amber), var(--brass-dark) 100%); +} + +.neon-card-green .neon-card-rim { + background: linear-gradient(135deg, var(--neon-green), transparent 50%, var(--brass) 100%); +} + +.neon-card-purple .neon-card-rim { + background: linear-gradient(135deg, var(--neon-purple), var(--neon-cyan) 100%); +} + +.neon-card-cyan:hover { + box-shadow: var(--shadow-panel), var(--shadow-neon-cyan); +} + +.neon-card-magenta:hover { + box-shadow: var(--shadow-panel), var(--shadow-neon-magenta); +} diff --git a/server/web/src/components/NeonCard/NeonCard.tsx b/server/web/src/components/NeonCard/NeonCard.tsx new file mode 100644 index 0000000..ea08961 --- /dev/null +++ b/server/web/src/components/NeonCard/NeonCard.tsx @@ -0,0 +1,32 @@ +import { ReactNode, CSSProperties } from 'react'; +import './NeonCard.css'; + +type Accent = 'cyan' | 'magenta' | 'amber' | 'green' | 'purple' | 'brass'; + +interface NeonCardProps { + children: ReactNode; + className?: string; + accent?: Accent; + tilt3d?: boolean; + hud?: boolean; + style?: CSSProperties; +} + +export default function NeonCard({ + children, + className = '', + accent = 'brass', + tilt3d = true, + hud = false, + style, +}: NeonCardProps) { + return ( +
+
+ {children} +
+ ); +} diff --git a/server/web/src/main.tsx b/server/web/src/main.tsx index 43c906c..4602b15 100644 --- a/server/web/src/main.tsx +++ b/server/web/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './styles/global.css'; +import './styles/steampunk-theme.css'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/server/web/src/pages/AgentsPage.tsx b/server/web/src/pages/AgentsPage.tsx index 3c99edd..14fcd5f 100644 --- a/server/web/src/pages/AgentsPage.tsx +++ b/server/web/src/pages/AgentsPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { api } from '../api/client'; import type { Agent, HashrateSample } from '../types'; import HashrateChart from '../components/Charts/HashrateChart'; +import NeonCard from '../components/NeonCard/NeonCard'; import './Pages.css'; export default function AgentsPage() { @@ -28,29 +29,33 @@ export default function AgentsPage() { }; return ( -
-
-

Agents

- {agents.length} total -
+
+
+
+

FLEET REGISTRY

+

Fleet Roster

+

Inspect each node — hashrate history, hardware, share ledger.

+
+ {agents.length} NODES +
{loading ? ( -
-

Loading agents...

-
+ +

Scanning network...

+
) : agents.length === 0 ? ( -
-
🖥️
+ +

No agents registered

Deploy a miner to a Windows machine and it will appear here automatically.

-
+ ) : (
{agents.map((agent) => (
selectAgent(agent)} >
@@ -76,8 +81,8 @@ export default function AgentsPage() {
{selectedAgent && ( -
-

{selectedAgent.name}

+ +

{selectedAgent.name}

Status @@ -153,11 +158,12 @@ export default function AgentsPage() { {hashrateHistory.length > 0 && (
-

Hashrate History

+

HASHRATE TELEMETRY

({ time: new Date(s.timestamp).toLocaleTimeString(), value: s.hashrate, @@ -165,25 +171,7 @@ export default function AgentsPage() { />
)} - - {hashrateHistory.length > 0 && ( -
-

Raw Samples ({hashrateHistory.length})

-
- {hashrateHistory.reverse().map((sample, i) => ( -
s.hashrate))) * 100)}%`, - }} - title={`${formatHashrate(sample.hashrate)} at ${new Date(sample.timestamp).toLocaleTimeString()}`} - /> - ))} -
-
- )} -
+ )}
)} diff --git a/server/web/src/pages/BuilderPage.tsx b/server/web/src/pages/BuilderPage.tsx index b500991..de15723 100644 --- a/server/web/src/pages/BuilderPage.tsx +++ b/server/web/src/pages/BuilderPage.tsx @@ -141,13 +141,17 @@ export default function BuilderPage() { }); return ( -
-
-

Miner Builder

+
+
+
+

INSTALLER FORGE

+

The Forge

+

Craft fused or standalone miners for your LAN fleet.

+
-
+
diff --git a/server/web/src/pages/DashboardPage.tsx b/server/web/src/pages/DashboardPage.tsx index 0c25385..5d7c8c2 100644 --- a/server/web/src/pages/DashboardPage.tsx +++ b/server/web/src/pages/DashboardPage.tsx @@ -1,8 +1,10 @@ import { useWebSocket } from '../hooks/useWebSocket'; import { api } from '../api/client'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo, type CSSProperties } from 'react'; import type { Share } from '../types'; import HashrateChart from '../components/Charts/HashrateChart'; +import GaugeRing from '../components/Charts/GaugeRing'; +import NeonCard from '../components/NeonCard/NeonCard'; import './Pages.css'; export default function DashboardPage() { @@ -10,6 +12,7 @@ export default function DashboardPage() { const [shares, setShares] = useState([]); const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]); const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]); + const [memHistory, setMemHistory] = useState<{ time: string; value: number }[]>([]); useEffect(() => { api.getRecentShares(20).then(setShares).catch(console.error); @@ -20,77 +23,125 @@ export default function DashboardPage() { const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0); const acceptedShares = agents.reduce((sum, a) => sum + a.shares_good, 0); const rejectedShares = agents.reduce((sum, a) => sum + a.shares_bad, 0); - const acceptRate = totalShares > 0 ? ((acceptedShares / totalShares) * 100).toFixed(1) : '0.0'; + const acceptRate = totalShares > 0 ? (acceptedShares / totalShares) * 100 : 0; const avgCpu = agents.length > 0 ? agents.reduce((s, a) => s + a.cpu_usage_pct, 0) / agents.length : 0; const avgMem = agents.length > 0 ? agents.reduce((s, a) => s + a.memory_usage_pct, 0) / agents.length : 0; + const onlinePct = agents.length > 0 ? (onlineCount / agents.length) * 100 : 0; useEffect(() => { const now = new Date().toLocaleTimeString(); setHashHistory((prev) => [...prev.slice(-59), { time: now, value: totalHashrate }]); setCpuHistory((prev) => [...prev.slice(-59), { time: now, value: avgCpu }]); - }, [totalHashrate, avgCpu]); + setMemHistory((prev) => [...prev.slice(-59), { time: now, value: avgMem }]); + }, [totalHashrate, avgCpu, avgMem]); const topAgents = useMemo( - () => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 6), + () => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 8), [agents] ); - return ( -
-
-
-

Fleet Dashboard

-

Live monitoring for every installed miner on your network

-
-
- - {isConnected ? 'Live feed' : 'Reconnecting...'} -
-
+ const maxAgentHash = Math.max(...topAgents.map((a) => a.hashrate_15m), 1); -
-
-
Fleet Hashrate
-
{formatHashrate(totalHashrate)}
-
{onlineCount} miners active
+ return ( +
+
+
+

PERSONAL NETWORK · LIVE TELEMETRY

+

Command Deck

+

+ Your private mining fleet across the LAN — brass gauges, neon pulse, real-time hashrate. +

-
-
Online
-
{onlineCount} / {agents.length}
-
{agents.length - onlineCount} offline
+
+
+ + +
+
+ {isConnected ? 'SIGNAL LOCKED' : 'RECONNECTING'} + {agents.length} nodes registered +
-
-
Accept Rate
-
{acceptRate}%
-
{acceptedShares} good · {rejectedShares} bad
-
-
-
Resource Avg
+
+ +
+ + + + + + + + + + + + +
+ +
+ +
Total Hashrate
+
{formatHashrate(totalHashrate)}
+
{onlineCount} engines firing
+
+ +
Fleet Online
+
{onlineCount} / {agents.length}
+
{agents.length - onlineCount} dormant
+
+ +
Accept Rate
+
{acceptRate.toFixed(1)}%
+
{acceptedShares} valid · {rejectedShares} rejected
+
+ +
Resources
{avgCpu.toFixed(0)}% CPU
-
{avgMem.toFixed(0)}% RAM fleet average
-
+
{avgMem.toFixed(0)}% memory · fleet mean
+
-
- -
-
- -
+ + + + + +
-
-

Machine Detail

+ + + + +
+

+ Machine Roster + +

{agents.length === 0 && ( -
-

No miners connected yet

-

Build an installer in Miner Builder, run it once on a Windows PC, and it will appear here automatically.

-
+ +
+

No miners on the wire

+

Forge an installer, deploy once per machine — nodes appear here with live neon telemetry.

+
)} - {topAgents.map((agent) => ( -
+ {topAgents.map((agent, i) => ( +
@@ -98,26 +149,33 @@ export default function DashboardPage() {
{agent.status}
-
-
Hashrate 15m{formatHashrate(agent.hashrate_15m)}
-
Hashrate 1m{formatHashrate(agent.hashrate_1m)}
-
Hashrate 15s{formatHashrate(agent.hashrate_15s)}
-
CPU / RAM{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%
-
Cores / RAM{agent.cpu_cores} cores · {agent.memory_gb} GB
-
Shares{agent.shares_good} ok · {agent.shares_bad} bad · {agent.shares_total} total
-
Network{agent.ip || 'unknown'} · v{agent.version || '?'}
-
Uptime{formatUptime(agent.uptime_seconds)}
-
Last seen{new Date(agent.last_seen).toLocaleString()}
+
+
-
+
+
Hash 15m{formatHashrate(agent.hashrate_15m)}
+
Hash 1m{formatHashrate(agent.hashrate_1m)}
+
CPU / RAM{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%
+
Hardware{agent.cpu_cores} cores · {agent.memory_gb} GB
+
Shares{agent.shares_good} ok · {agent.shares_bad} bad
+
Node{agent.ip || '—'} · {agent.id.slice(0, 8)}
+
Uptime{formatUptime(agent.uptime_seconds)}
+
+ ))}
-
+
-
-

Recent Shares

-
- +
+

+ Share Log + +

+ +
@@ -128,24 +186,24 @@ export default function DashboardPage() { {shares.length === 0 && ( - + )} {shares.map((share) => ( - - + + - + ))}
Time
No shares submitted yet
No shares yet — awaiting proof of work...
{formatTime(share.timestamp)}{share.agent_id?.substring(0, 8)}...{formatTime(share.timestamp)}{share.agent_id?.substring(0, 8)}… {share.accepted ? 'Accepted' : 'Rejected'} {share.hash?.substring(0, 20)}...{share.hash?.substring(0, 24)}…
-
-
+ +
); } diff --git a/server/web/src/pages/Pages.css b/server/web/src/pages/Pages.css index 5d27441..12ab7c6 100644 --- a/server/web/src/pages/Pages.css +++ b/server/web/src/pages/Pages.css @@ -231,16 +231,28 @@ .agent-list-item { cursor: pointer; - transition: all 0.2s ease; + transition: all 0.25s ease; + padding: 1rem !important; } .agent-list-item:hover { - border-color: var(--accent-blue); + border-color: var(--neon-cyan); + box-shadow: 0 0 16px rgba(0, 245, 255, 0.12); } .agent-list-item.selected { - border-color: var(--accent-blue); - background: rgba(59, 130, 246, 0.05); + border-color: var(--neon-cyan); + background: rgba(0, 245, 255, 0.06); + box-shadow: inset 0 0 24px rgba(0, 245, 255, 0.08), 0 0 20px rgba(0, 245, 255, 0.15); +} + +.header-count { + font-size: 0.75rem; + letter-spacing: 0.2em; + color: var(--neon-cyan); + padding: 0.5rem 1rem; + border: 1px solid var(--border-brass); + background: rgba(8, 6, 4, 0.8); } .agent-list-header { @@ -692,3 +704,272 @@ .table-card { overflow-x: auto; } + +/* ═══ Steampunk Command Deck ═══ */ +.command-deck { + max-width: 1600px; +} + +.deck-hero { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 2rem; + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-brass); + position: relative; +} + +.deck-hero::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 120px; + height: 2px; + background: linear-gradient(90deg, var(--neon-cyan), transparent); + box-shadow: 0 0 12px var(--neon-cyan); +} + +.deck-eyebrow { + font-size: 0.65rem; + letter-spacing: 0.25em; + color: var(--brass); + margin-bottom: 0.5rem; +} + +.deck-hero h1 { + font-size: 2.25rem; + background: linear-gradient(180deg, var(--brass-light) 20%, var(--text-primary) 80%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.35rem; +} + +.deck-hero-status { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.25rem; + background: rgba(12, 10, 8, 0.8); + border: 1px solid var(--border-brass); + border-radius: 2px; +} + +.live-beacon { + position: relative; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.beacon-ring { + position: absolute; + inset: 0; + border: 2px solid var(--text-muted); + border-radius: 50%; + opacity: 0.4; +} + +.live-beacon.on .beacon-ring { + border-color: var(--neon-green); + animation: pulse-glow 2s ease-in-out infinite; + box-shadow: 0 0 20px rgba(57, 255, 20, 0.3); +} + +.beacon-core { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--text-muted); +} + +.live-beacon.on .beacon-core { + background: var(--neon-green); + box-shadow: 0 0 12px var(--neon-green); +} + +.live-label { + display: block; + font-size: 0.75rem; + letter-spacing: 0.15em; + color: var(--neon-cyan); +} + +.live-sub { + font-size: 0.85rem; + color: var(--text-muted); +} + +.gauge-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.gauge-card { + display: flex; + align-items: center; + justify-content: center; + padding: 1.25rem !important; + min-height: 140px; +} + +.steampunk-stats .stat-card-wrap { + text-align: center; + padding: 1.25rem !important; +} + +.steampunk-stats .stat-label { + color: var(--brass-light); + letter-spacing: 0.15em; +} + +.steampunk-stats .stat-value { + font-family: var(--font-tech); + font-size: 1.85rem; +} + +.stat-dim { + font-size: 1rem; + color: var(--text-muted); + font-weight: 400; +} + +.neon-glow-cyan { + color: var(--neon-cyan) !important; + text-shadow: 0 0 20px rgba(0, 245, 255, 0.5); +} + +.neon-glow-purple { + color: var(--neon-purple) !important; + text-shadow: 0 0 20px rgba(178, 75, 243, 0.5); +} + +.chart-row-full { + margin-bottom: 2rem; +} + +.section-title { + display: flex; + align-items: center; + gap: 1rem; + font-size: 1.25rem; + color: var(--brass-light); + margin-bottom: 1.25rem; +} + +.section-ornament { + color: var(--neon-cyan); + font-size: 0.75rem; + text-shadow: 0 0 8px var(--neon-cyan); +} + +.section-line { + flex: 1; + height: 1px; + background: linear-gradient(90deg, var(--border-brass), transparent); +} + +.machine-panel { + animation: fadeInSteampunk 0.5s ease backwards; +} + +.agent-hash-bar { + height: 4px; + background: rgba(201, 162, 39, 0.15); + border-radius: 2px; + margin: 0.75rem 0 1rem; + overflow: hidden; +} + +.agent-hash-fill { + height: 100%; + background: linear-gradient(90deg, var(--brass-dark), var(--neon-cyan)); + box-shadow: 0 0 10px rgba(0, 245, 255, 0.4); + transition: width 0.6s ease; + border-radius: 2px; +} + +.agent-detail-lines div { + border-bottom-color: rgba(201, 162, 39, 0.15) !important; +} + +.steampunk-table { + width: 100%; + border-collapse: collapse; +} + +.steampunk-table th { + font-family: var(--font-tech); + font-size: 0.7rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--brass); + text-align: left; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-brass); +} + +.steampunk-table td { + padding: 0.65rem 1rem; + border-bottom: 1px solid rgba(201, 162, 39, 0.08); + font-size: 0.9rem; +} + +.steampunk-table tbody tr:hover { + background: rgba(0, 245, 255, 0.04); +} + +.mono-sm { + font-family: var(--font-tech); + font-size: 0.8rem; + letter-spacing: 0.05em; +} + +.empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 3rem !important; +} + +.empty-icon { + font-size: 3rem; + color: var(--brass); + opacity: 0.6; + margin-bottom: 1rem; + animation: gear-spin 30s linear infinite; +} + +.page-header h1 { + font-family: var(--font-display); + font-size: 1.75rem; + color: var(--brass-light); +} + +.page-subtitle { + color: var(--text-secondary); + font-size: 1rem; + margin-top: 0.35rem; +} + +@media (max-width: 1100px) { + .gauge-row { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .deck-hero { + flex-direction: column; + align-items: flex-start; + } + .gauge-row { + grid-template-columns: 1fr; + } +} diff --git a/server/web/src/pages/SettingsPage.tsx b/server/web/src/pages/SettingsPage.tsx index 587f37d..3eb6d07 100644 --- a/server/web/src/pages/SettingsPage.tsx +++ b/server/web/src/pages/SettingsPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { api } from '../api/client'; import type { ServerConfig } from '../types'; import { HelpTip } from '../components/HelpTip'; +import NeonCard from '../components/NeonCard/NeonCard'; import './Pages.css'; export default function SettingsPage() { @@ -47,9 +48,14 @@ export default function SettingsPage() { if (loading) { return ( -
-

Settings

-

Loading settings...

+
+
+
+

CALIBRATION

+

System Calibrate

+
+
+

Loading settings...

); } @@ -64,13 +70,17 @@ export default function SettingsPage() { } return ( -
-
-

Settings

+
+
+
+

CALIBRATION

+

System Calibrate

+

Pool, wallet, defaults, and install paths for the forge.

+
-
+ {saveMessage && (
diff --git a/server/web/src/styles/steampunk-theme.css b/server/web/src/styles/steampunk-theme.css new file mode 100644 index 0000000..3fc4d96 --- /dev/null +++ b/server/web/src/styles/steampunk-theme.css @@ -0,0 +1,300 @@ +/* Steampunk Neon Command Deck — design tokens */ +@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&family=Orbitron:wght@400;500;600;700&family=Rajdhani:wght@400;500;600;700&display=swap'); + +:root { + --bg-void: #060504; + --bg-deep: #0c0a08; + --bg-panel: rgba(18, 14, 10, 0.85); + --bg-panel-solid: #14100c; + --bg-hover: rgba(40, 32, 24, 0.9); + + --brass: #c9a227; + --brass-light: #e8c547; + --brass-dark: #6b4f12; + --copper: #b87333; + --copper-glow: rgba(184, 115, 51, 0.4); + + --neon-cyan: #00f5ff; + --neon-magenta: #ff2da6; + --neon-amber: #ffb020; + --neon-green: #39ff14; + --neon-purple: #b24bf3; + + --text-primary: #f4ebe0; + --text-secondary: #c4b5a0; + --text-muted: #7a6f62; + + --border-brass: rgba(201, 162, 39, 0.35); + --border-neon: rgba(0, 245, 255, 0.25); + --shadow-panel: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 1px rgba(201, 162, 39, 0.3); + --shadow-neon-cyan: 0 0 20px rgba(0, 245, 255, 0.35), 0 0 60px rgba(0, 245, 255, 0.1); + --shadow-neon-magenta: 0 0 20px rgba(255, 45, 166, 0.3); + + --font-display: 'Cinzel Decorative', Georgia, serif; + --font-tech: 'Orbitron', monospace; + --font-body: 'Rajdhani', 'Segoe UI', sans-serif; + + --perspective: 1200px; + --card-tilt: 2deg; +} + +/* Legacy variable bridge */ +:root { + --bg-primary: var(--bg-void); + --bg-secondary: var(--bg-deep); + --bg-card: var(--bg-panel-solid); + --border-color: var(--border-brass); + --accent-green: var(--neon-green); + --accent-red: #ff4466; + --accent-yellow: var(--neon-amber); + --accent-blue: var(--neon-cyan); + --accent-purple: var(--neon-purple); + --accent-cyan: var(--neon-cyan); + --online-color: var(--neon-green); + --offline-color: var(--text-muted); + --error-color: #ff4466; +} + +body { + font-family: var(--font-body); + font-size: 1.05rem; + background: var(--bg-void); + color: var(--text-primary); + overflow-x: hidden; +} + +h1, h2, h3, .font-display { + font-family: var(--font-display); + letter-spacing: 0.04em; +} + +.font-tech { + font-family: var(--font-tech); +} + +/* Brass rivet texture overlay on cards */ +.card, +.neon-card { + position: relative; + background: linear-gradient(145deg, rgba(28, 22, 16, 0.95) 0%, rgba(12, 10, 8, 0.98) 100%); + border: 1px solid var(--border-brass); + border-radius: 4px; + box-shadow: var(--shadow-panel); + backdrop-filter: blur(12px); + overflow: hidden; +} + +.card::before, +.neon-card::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(ellipse 80% 50% at 50% -20%, rgba(0, 245, 255, 0.08), transparent 50%), + repeating-linear-gradient( + 90deg, + transparent, + transparent 48px, + rgba(201, 162, 39, 0.03) 48px, + rgba(201, 162, 39, 0.03) 49px + ); + pointer-events: none; + z-index: 0; +} + +.card > *, +.neon-card > * { + position: relative; + z-index: 1; +} + +/* 3D panel tilt on hover */ +.neon-card-3d { + transform-style: preserve-3d; + transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.4s ease; +} + +.neon-card-3d:hover { + transform: perspective(var(--perspective)) rotateX(2deg) translateY(-4px); + box-shadow: var(--shadow-panel), var(--shadow-neon-cyan); +} + +/* Corner brackets (steampunk HUD) */ +.hud-corners { + position: relative; +} + +.hud-corners::after { + content: ''; + position: absolute; + inset: 8px; + border: 1px solid transparent; + border-image: linear-gradient(135deg, var(--brass) 0%, transparent 30%, transparent 70%, var(--neon-cyan) 100%) 1; + pointer-events: none; + opacity: 0.5; +} + +/* Buttons — brass + neon */ +.btn { + font-family: var(--font-tech); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.75rem; + border-radius: 2px; + position: relative; + overflow: hidden; +} + +.btn::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(255,255,255,0.12) 0%, transparent 50%); + pointer-events: none; +} + +.btn-primary { + background: linear-gradient(180deg, #1a4a52 0%, #0d2a30 100%); + border: 1px solid var(--neon-cyan); + color: var(--neon-cyan); + box-shadow: 0 0 15px rgba(0, 245, 255, 0.2); +} + +.btn-primary:hover { + box-shadow: var(--shadow-neon-cyan); + background: linear-gradient(180deg, #1f5a64 0%, #103540 100%); +} + +.btn-success { + background: linear-gradient(180deg, #1a3d1a 0%, #0d200d 100%); + border: 1px solid var(--neon-green); + color: var(--neon-green); +} + +.btn-success:hover { + box-shadow: 0 0 20px rgba(57, 255, 20, 0.25); +} + +.btn-outline { + border: 1px solid var(--border-brass); + color: var(--brass-light); + background: rgba(20, 16, 12, 0.6); +} + +.btn-outline:hover { + border-color: var(--neon-cyan); + color: var(--neon-cyan); + box-shadow: 0 0 12px rgba(0, 245, 255, 0.15); +} + +/* Form inputs — gauge panel style */ +.input, +.select { + background: rgba(8, 6, 4, 0.9); + border: 1px solid var(--border-brass); + border-radius: 2px; + font-family: var(--font-body); + color: var(--text-primary); +} + +.input:focus, +.select:focus { + border-color: var(--neon-cyan); + box-shadow: 0 0 0 2px rgba(0, 245, 255, 0.15), inset 0 0 20px rgba(0, 245, 255, 0.05); + outline: none; +} + +/* Status badges — neon glow */ +.status-badge.online { + background: rgba(57, 255, 20, 0.12); + color: var(--neon-green); + border: 1px solid rgba(57, 255, 20, 0.4); + box-shadow: 0 0 12px rgba(57, 255, 20, 0.2); +} + +.status-badge.offline { + background: rgba(122, 111, 98, 0.15); + border: 1px solid rgba(122, 111, 98, 0.3); +} + +.status-badge.error { + background: rgba(255, 68, 102, 0.12); + color: #ff6688; + border: 1px solid rgba(255, 68, 102, 0.4); + box-shadow: 0 0 12px rgba(255, 68, 102, 0.2); +} + +.status-dot.online { + background: var(--neon-green); + box-shadow: 0 0 8px var(--neon-green), 0 0 16px rgba(57, 255, 20, 0.5); + animation: pulse-glow 2s ease-in-out infinite; +} + +.status-dot.offline { + background: var(--text-muted); +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(0.92); } +} + +@keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(100vh); } +} + +@keyframes gear-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes float-orb { + 0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.4; } + 50% { transform: translate(20px, -30px) scale(1.1); opacity: 0.7; } +} + +@keyframes grid-drift { + 0% { background-position: 0 0; } + 100% { background-position: 60px 60px; } +} + +@keyframes shimmer { + 0% { background-position: -200% center; } + 100% { background-position: 200% center; } +} + +.fade-in { + animation: fadeInSteampunk 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards; +} + +@keyframes fadeInSteampunk { + from { + opacity: 0; + transform: translateY(16px) perspective(800px) rotateX(4deg); + } + to { + opacity: 1; + transform: translateY(0) perspective(800px) rotateX(0); + } +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--bg-deep); +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--brass-dark), var(--copper)); + border-radius: 2px; + border: 1px solid var(--brass); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--brass); +}