From 9d223b81377483fad8ce0e04cc82afba94294f54 Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 27 May 2026 00:43:34 -0700 Subject: [PATCH] Redesign dashboard with steampunk neon theme and live gauges. Ambient background, 3D neon cards, brass HUD styling, glowing charts, gauge rings, and refreshed Command Deck, Fleet Roster, Forge, and Calibrate pages. --- run.bat | 211 +++++++----- server/web/index.html | 2 +- .../components/Ambient/AmbientBackground.css | 103 ++++++ .../components/Ambient/AmbientBackground.tsx | 16 + .../web/src/components/Charts/GaugeRing.css | 56 ++++ .../web/src/components/Charts/GaugeRing.tsx | 66 ++++ .../src/components/Charts/HashrateChart.css | 74 +++++ .../src/components/Charts/HashrateChart.tsx | 140 ++++++-- server/web/src/components/HelpTip.css | 32 +- server/web/src/components/Layout/Layout.css | 179 ++++++++--- server/web/src/components/Layout/Layout.tsx | 97 ++++-- .../web/src/components/NeonCard/NeonCard.css | 53 ++++ .../web/src/components/NeonCard/NeonCard.tsx | 32 ++ server/web/src/main.tsx | 1 + server/web/src/pages/AgentsPage.tsx | 58 ++-- server/web/src/pages/BuilderPage.tsx | 12 +- server/web/src/pages/DashboardPage.tsx | 198 ++++++++---- server/web/src/pages/Pages.css | 289 ++++++++++++++++- server/web/src/pages/SettingsPage.tsx | 26 +- server/web/src/styles/steampunk-theme.css | 300 ++++++++++++++++++ 20 files changed, 1638 insertions(+), 307 deletions(-) create mode 100644 server/web/src/components/Ambient/AmbientBackground.css create mode 100644 server/web/src/components/Ambient/AmbientBackground.tsx create mode 100644 server/web/src/components/Charts/GaugeRing.css create mode 100644 server/web/src/components/Charts/GaugeRing.tsx create mode 100644 server/web/src/components/Charts/HashrateChart.css create mode 100644 server/web/src/components/NeonCard/NeonCard.css create mode 100644 server/web/src/components/NeonCard/NeonCard.tsx create mode 100644 server/web/src/styles/steampunk-theme.css 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); +}