Compare commits
8 Commits
subnet-dis
...
df88d160cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df88d160cb | ||
|
|
615034554c | ||
|
|
9a884a80b6 | ||
|
|
6ab43a468b | ||
|
|
0c7b676e23 | ||
|
|
ebcb1e1210 | ||
|
|
d4ac5d435a | ||
| 1d36d21242 |
174
LAUNCH.bat
174
LAUNCH.bat
@@ -2,23 +2,52 @@
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
title AetherForge Control Deck
|
||||
cd /d "%~dp0"
|
||||
set "REPO=%CD%"
|
||||
|
||||
echo.
|
||||
echo ================================================================
|
||||
echo AetherForge - One-Click Launch
|
||||
echo ================================================================
|
||||
echo Folder: %REPO%
|
||||
echo.
|
||||
|
||||
set "PREP=%REPO%\scripts\launch-prep.bat"
|
||||
if not exist "%PREP%" set "PREP=%REPO%\..\scripts\launch-prep.bat"
|
||||
if exist "%PREP%" (
|
||||
call "%PREP%" "%REPO%"
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo LAUNCH prep failed - fix errors above and retry.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo [Prep] launch-prep.bat not found - skipping pull/UI build.
|
||||
)
|
||||
|
||||
if /i "%AF_LAUNCH_DRY_RUN%"=="1" (
|
||||
echo.
|
||||
echo [Dry run] Prep steps OK - not starting tunnel or server.
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
:: Portable deck root = folder that contains AetherForge.exe (repo usb\ or copied USB drive)
|
||||
set "ROOT="
|
||||
if exist "%CD%\AetherForge.exe" (
|
||||
set "ROOT=!CD!"
|
||||
) else if exist "%CD%\usb\AetherForge.exe" (
|
||||
cd /d "%CD%\usb"
|
||||
set "ROOT=!CD!"
|
||||
) else (
|
||||
echo.
|
||||
echo ERROR: AetherForge.exe not found.
|
||||
echo Expected next to this script, or in usb\AetherForge.exe
|
||||
echo Run pack-usb.bat from the repo to build the portable bundle.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if defined ROOT if exist "%ROOT%\AetherForge.exe" goto portable_deck
|
||||
|
||||
:: No portable binary - dev control server from repo
|
||||
goto dev_server_launch
|
||||
|
||||
:portable_deck
|
||||
|
||||
if not exist "%ROOT%\AetherForge.exe" (
|
||||
echo ERROR: AetherForge.exe missing in %ROOT%
|
||||
pause
|
||||
@@ -50,7 +79,6 @@ if exist "%BUNDLED_GO%" (
|
||||
goto go_ready
|
||||
)
|
||||
|
||||
:: Check if Go is installed system-wide
|
||||
where go >nul 2>nul
|
||||
if not errorlevel 1 (
|
||||
echo [Go] Using system Go installation.
|
||||
@@ -58,7 +86,6 @@ if not errorlevel 1 (
|
||||
goto go_ready
|
||||
)
|
||||
|
||||
:: Go not found anywhere - skip optional tools, proceed directly to server
|
||||
echo.
|
||||
echo [Go] Go not found - Forge obfuscation tools unavailable. Running server without them.
|
||||
echo.
|
||||
@@ -66,17 +93,11 @@ goto server_launch
|
||||
|
||||
:go_ready
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 2. Pin all Go caches to the USB so module downloads travel with you
|
||||
:: ----------------------------------------------------------------
|
||||
set "GOPATH=%ROOT%\toolchain\gopath"
|
||||
set "GOMODCACHE=%ROOT%\toolchain\gopath\pkg\mod"
|
||||
set "GOCACHE=%ROOT%\toolchain\gocache"
|
||||
set "GOENV=off"
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 3. Install optional Forge tools if missing (non-fatal)
|
||||
:: ----------------------------------------------------------------
|
||||
if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
||||
if not exist "%ROOT%\toolchain\gopath\bin\garble.exe" (
|
||||
echo [Tools] Installing garble...
|
||||
@@ -95,9 +116,8 @@ if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
||||
|
||||
:server_launch
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 4. Ensure data directories exist
|
||||
:: ----------------------------------------------------------------
|
||||
echo.
|
||||
echo Ensuring data directories...
|
||||
if not exist "%ROOT%\data\builds" mkdir "%ROOT%\data\builds"
|
||||
if not exist "%ROOT%\data\logs" mkdir "%ROOT%\data\logs"
|
||||
if not exist "%ROOT%\data\spread-kits" mkdir "%ROOT%\data\spread-kits"
|
||||
@@ -105,9 +125,6 @@ if not exist "%ROOT%\data\uploads" mkdir "%ROOT%\data\uploads"
|
||||
if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints"
|
||||
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 5. Detect LAN IP for display
|
||||
:: ----------------------------------------------------------------
|
||||
set "SERVER_PORT=8989"
|
||||
set "CONFIG_FILE=%ROOT%\data\config.json"
|
||||
if exist "%CONFIG_FILE%" (
|
||||
@@ -123,9 +140,7 @@ set "LAN_IP=localhost"
|
||||
:lan_done
|
||||
set "LAN_IP=%LAN_IP: =%"
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 6. Kill any stale server and tunnel processes
|
||||
:: ----------------------------------------------------------------
|
||||
echo Stopping stale processes...
|
||||
taskkill /F /IM AetherForge.exe >nul 2>nul
|
||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||
ping -n 2 127.0.0.1 >nul
|
||||
@@ -139,12 +154,11 @@ echo LAN: http://%LAN_IP%:%SERVER_PORT%
|
||||
echo Data: %ROOT%\data\
|
||||
echo.
|
||||
echo Login accounts: admin + comrade ^(passwords below after start^).
|
||||
echo Cloudflare tunnel starts below ^(LAUNCH + server^).
|
||||
echo Press Ctrl+C to stop.
|
||||
echo ================================================================
|
||||
echo.
|
||||
|
||||
:: Start Cloudflare connector before server ^(works with old or new AetherForge.exe^)
|
||||
echo Starting tunnel...
|
||||
set "CF_SCRIPT=%ROOT%\scripts\usb-start-cloudflared.ps1"
|
||||
if not exist "%CF_SCRIPT%" set "CF_SCRIPT=%ROOT%\..\scripts\usb-start-cloudflared.ps1"
|
||||
if exist "%CF_SCRIPT%" (
|
||||
@@ -154,31 +168,123 @@ if exist "%CF_SCRIPT%" (
|
||||
)
|
||||
echo.
|
||||
|
||||
:: Ensure cwd matches deck root so webroot resolution finds usb\webroot
|
||||
cd /d "%ROOT%"
|
||||
|
||||
:: Open browser after short delay
|
||||
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
|
||||
|
||||
:: Launch server (LAUNCH already started cloudflared above — tell server not to spawn a second copy)
|
||||
echo Starting server...
|
||||
set "AF_TUNNEL_EXTERNAL=1"
|
||||
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
|
||||
set "EC=!ERRORLEVEL!"
|
||||
|
||||
if exist "%ROOT%\data\cloudflared.pid" (
|
||||
for /f "usebackq" %%P in ("%ROOT%\data\cloudflared.pid") do (
|
||||
call :cleanup_tunnel "%ROOT%"
|
||||
goto server_stopped
|
||||
|
||||
:dev_server_launch
|
||||
echo.
|
||||
echo No AetherForge.exe - starting dev control server ^(repo^).
|
||||
echo ^(Run pack-usb.bat for portable USB bundle.^)
|
||||
echo.
|
||||
|
||||
set "PATH=C:\Program Files\Go\bin;%USERPROFILE%\go\bin;C:\Program Files\nodejs;%PATH%"
|
||||
set "DATA=%REPO%\data"
|
||||
set "DECK=%REPO%"
|
||||
if exist "%REPO%\usb\tools\cloudflared.exe" set "DECK=%REPO%\usb"
|
||||
if exist "%REPO%\usb\data" if not exist "%DECK%\data" set "DECK=%REPO%\usb"
|
||||
|
||||
echo Ensuring data directories...
|
||||
if not exist "%DATA%\builds" mkdir "%DATA%\builds"
|
||||
if not exist "%DATA%\logs" mkdir "%DATA%\logs"
|
||||
if not exist "%DATA%\spread-kits" mkdir "%DATA%\spread-kits"
|
||||
if not exist "%DATA%\uploads" mkdir "%DATA%\uploads"
|
||||
if not exist "%DATA%\blueprints" mkdir "%DATA%\blueprints"
|
||||
if not exist "%DATA%\preps" mkdir "%DATA%\preps"
|
||||
if not exist "%REPO%\bin" mkdir "%REPO%\bin"
|
||||
|
||||
set "SERVER_PORT=8989"
|
||||
set "CONFIG_FILE=%DATA%\config.json"
|
||||
if exist "%CONFIG_FILE%" (
|
||||
for /f "usebackq delims=" %%P in (`powershell -NoProfile -Command "try { $j = Get-Content -Raw '%CONFIG_FILE%' | ConvertFrom-Json; if ($j.port) { $j.port } } catch { }"`) do (
|
||||
if not "%%P"=="" set "SERVER_PORT=%%P"
|
||||
)
|
||||
)
|
||||
set "LAN_IP=localhost"
|
||||
|
||||
echo Stopping stale processes...
|
||||
taskkill /F /IM miner-server.exe >nul 2>nul
|
||||
taskkill /F /IM AetherForge.exe >nul 2>nul
|
||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||
ping -n 2 127.0.0.1 >nul
|
||||
|
||||
where go >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Go not found - install from https://go.dev/dl/ or use pack-usb.bat
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "%REPO%\bin\miner-server.exe" (
|
||||
echo Building control server...
|
||||
cd /d "%REPO%\server"
|
||||
go mod download >nul 2>nul
|
||||
go build -ldflags="-s -w" -o "..\bin\miner-server.exe" .
|
||||
if errorlevel 1 (
|
||||
cd /d "%REPO%"
|
||||
echo ERROR: Server build failed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd /d "%REPO%"
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ================================================================
|
||||
echo STARTING CONTROL SERVER ^(dev^)
|
||||
echo ================================================================
|
||||
echo Dashboard: http://localhost:%SERVER_PORT%
|
||||
echo Data: %DATA%\
|
||||
echo ================================================================
|
||||
echo.
|
||||
|
||||
echo Starting tunnel...
|
||||
set "CF_SCRIPT=%REPO%\scripts\usb-start-cloudflared.ps1"
|
||||
if not exist "%CF_SCRIPT%" set "CF_SCRIPT=%DECK%\scripts\usb-start-cloudflared.ps1"
|
||||
if exist "%CF_SCRIPT%" (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%CF_SCRIPT%" -DeckRoot "%DECK%"
|
||||
) else (
|
||||
echo [Tunnel] WARNING: usb-start-cloudflared.ps1 not found.
|
||||
)
|
||||
echo.
|
||||
|
||||
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
|
||||
|
||||
echo Starting server...
|
||||
cd /d "%REPO%"
|
||||
set "AF_TUNNEL_EXTERNAL=1"
|
||||
"%REPO%\bin\miner-server.exe" -data "%DATA%"
|
||||
set "EC=!ERRORLEVEL!"
|
||||
|
||||
call :cleanup_tunnel "%DECK%"
|
||||
goto server_stopped
|
||||
|
||||
:cleanup_tunnel
|
||||
set "TROOT=%~1"
|
||||
if exist "%TROOT%\data\cloudflared.pid" (
|
||||
for /f "usebackq" %%P in ("%TROOT%\data\cloudflared.pid") do (
|
||||
taskkill /F /PID %%P >nul 2>nul
|
||||
)
|
||||
del "%ROOT%\data\cloudflared.pid" 2>nul
|
||||
del "%TROOT%\data\cloudflared.pid" 2>nul
|
||||
)
|
||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:server_stopped
|
||||
echo.
|
||||
if "!EC!"=="0" (
|
||||
echo [Server] Stopped normally.
|
||||
) else (
|
||||
echo [Server] Exited with code !EC!.
|
||||
echo If port %SERVER_PORT% is in use, close other AetherForge windows and retry.
|
||||
echo If port %SERVER_PORT% is in use, close other server windows and retry.
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 sudo-jones-cmd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
29
README.md
29
README.md
@@ -316,35 +316,32 @@ AetherForge exposes **legitimate operator tunneling** for machines you administe
|
||||
|
||||
**Requirements:** Windows 10/11 on control PC. Outbound internet to your pool.
|
||||
|
||||
### Simple start (deploy → test → mine)
|
||||
### Simple start
|
||||
|
||||
Three steps — no spread, no triple onion, no Probe & Join required:
|
||||
**Double-click `LAUNCH.bat` — that's it.**
|
||||
|
||||
1. **Calibrate** — set your XMR **wallet** and **pool** (SupportXMR or your upstream).
|
||||
2. **Forge → Simple mode → Deploy & Mine** — one click forges an in-process worker (`simple_deploy` baked in). Run the `.exe` **once** on each PC you own.
|
||||
3. **Command Deck** — status shows **Testing → Mining** (or a clear failure reason). Online with 0 H/s? Use **Run diagnostics** or **Restart mining** on the banner.
|
||||
One click pulls updates (when online), builds the dashboard UI, ensures data folders, starts the Cloudflare tunnel sidecar, launches the control server, and opens your browser to the Command Deck (default **http://localhost:8989**). No portable USB bundle? `LAUNCH.bat` builds and runs `bin\miner-server.exe` from the repo instead.
|
||||
|
||||
Honest scope: **deploy** here means C2 registration + mining tier probe on that host — not lateral spread or registry staging to other machines.
|
||||
After the deck is up: **Calibrate** wallet/pool, then **Forge → Simple mode → Deploy & Mine** on each PC you own. Honest scope: **deploy** means C2 registration + mining tier probe on that host — not lateral spread.
|
||||
|
||||
### Operator path (dev control PC)
|
||||
|
||||
1. Double-click **`devrun.bat`** in the project root.
|
||||
Installs Go/Node if missing, builds the dashboard, compiles `bin\miner-server.exe`, copies web assets, and starts the server.
|
||||
1. **`LAUNCH.bat`** — same one-click path as above (preferred).
|
||||
2. Or double-click **`devrun.bat`** for a dev-focused window (installs Go/Node if missing, pull + UI build, compiles `bin\miner-server.exe`, live logs).
|
||||
|
||||
2. Browser opens **http://localhost:8989**
|
||||
3. Browser opens **http://localhost:8989**
|
||||
|
||||
3. **Sign in** — first run: check the console window for **admin** and **comrade** passwords (both auto-created)
|
||||
4. **Sign in** — first run: check the console window for **admin** and **comrade** passwords (both auto-created)
|
||||
|
||||
4. **Calibrate** → wallet + pool + public URL; optional **Telegram** alerts; optional **AI Control** + persona; review **LOTL onion tiers** and `patch_first` gates
|
||||
5. **Calibrate** → wallet + pool + public URL; optional **Telegram** alerts; optional **AI Control** + persona; review **LOTL onion tiers** and `patch_first` gates
|
||||
|
||||
5. **Forge** → Operation mode **LOTL Onion** (or Ghost / AV-Safe) · server URL (`http://YOUR-LAN-IP:8989` or tunnel) · target OS · spread toggles as needed → **Forge Installer**
|
||||
6. **Forge** → Operation mode **LOTL Onion** (or Ghost / AV-Safe) · server URL (`http://YOUR-LAN-IP:8989` or tunnel) · target OS · spread toggles as needed → **Forge Installer**
|
||||
|
||||
6. Run the forged `.exe` **once** on each worker PC (or distribute via movie ZIP / USB / spread kit)
|
||||
7. Run the forged `.exe` **once** on each worker PC (or distribute via movie ZIP / USB / spread kit)
|
||||
|
||||
7. **Crucible** → **Probe & Join** on online nodes; watch **Onion** timeline and **Access Depth** for tier progression
|
||||
|
||||
8. Watch fleet stats on **Command Deck**; campaign hits in **Emberwake** War Room
|
||||
8. **Crucible** → **Probe & Join** on online nodes; watch **Onion** timeline and **Access Depth** for tier progression
|
||||
|
||||
9. Watch fleet stats on **Command Deck**; campaign hits in **Emberwake** War Room
|
||||
### Portable USB command deck
|
||||
|
||||
1. Run **`pack-usb.bat`** from repo root (re-run after any code change)
|
||||
|
||||
@@ -169,7 +169,6 @@ func main() {
|
||||
sharesFound++
|
||||
fmt.Printf(" ★ SHARE job=%-14s nonce=%s\n", jobID, nonce)
|
||||
})
|
||||
pool.Start()
|
||||
pool.SetJob(&job.Job{
|
||||
ID: "validate-001",
|
||||
Blob: testBlobHex,
|
||||
@@ -177,9 +176,11 @@ func main() {
|
||||
SeedHash: testSeedHex,
|
||||
Height: 3000000,
|
||||
})
|
||||
pool.Start()
|
||||
|
||||
fmt.Printf(" ✓ %d worker(s) started, job injected\n", *threads)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
pool.ResetHashCounter()
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
done := time.After(time.Duration(*seconds) * time.Second)
|
||||
elapsed := 0
|
||||
var finalHS float64
|
||||
@@ -187,20 +188,23 @@ loop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
elapsed += 5
|
||||
elapsed += 1
|
||||
hs := pool.HashesPerSecond()
|
||||
pool.ResetHashCounter()
|
||||
finalHS = hs
|
||||
fmt.Printf(" [%2ds] %8.1f H/s shares=%d\n", elapsed, hs, sharesFound)
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
if hs := pool.HashesPerSecond(); hs > finalHS {
|
||||
finalHS = hs
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
pool.Stop()
|
||||
|
||||
if finalHS < 1 {
|
||||
fail(fmt.Sprintf("hashrate is 0 after %ds", *seconds), &ok)
|
||||
if finalHS < 1 && sharesFound == 0 {
|
||||
fail(fmt.Sprintf("hashrate is 0 and no shares after %ds", *seconds), &ok)
|
||||
} else {
|
||||
fmt.Printf("\n ✓ %.0f H/s total / %.0f H/s per thread\n", finalHS, finalHS/float64(*threads))
|
||||
if sharesFound > 0 {
|
||||
|
||||
66
devrun.bat
66
devrun.bat
@@ -73,7 +73,7 @@ echo Go installed.
|
||||
|
||||
:go_ready
|
||||
|
||||
:: Garble + go-winres (Forge pipeline tools — failure is non-fatal)
|
||||
:: Garble + go-winres (Forge pipeline tools ??? failure is non-fatal)
|
||||
echo [1.5/5] Checking Forge tools (Garble, go-winres)...
|
||||
where garble >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
@@ -131,6 +131,26 @@ echo Node.js installed.
|
||||
|
||||
:node_ready
|
||||
|
||||
:: ============================================================
|
||||
:: STEP 2b: Git pull + UI build (shared with LAUNCH.bat)
|
||||
:: ============================================================
|
||||
if defined SKIP_FRONTEND (
|
||||
echo.
|
||||
echo Pulling latest ^(UI build skipped - Node.js unavailable^)...
|
||||
git -C "%ROOT%" pull origin main 2>nul
|
||||
if errorlevel 1 echo Note: git pull skipped or failed.
|
||||
) else (
|
||||
echo.
|
||||
echo Pulling latest + building UI...
|
||||
set "PREP=%ROOT%\scripts\launch-prep.bat"
|
||||
if exist "%PREP%" (
|
||||
call "%PREP%" "%ROOT%"
|
||||
if errorlevel 1 goto fatal_exit
|
||||
) else (
|
||||
echo WARNING: scripts\launch-prep.bat missing - skipping pull/UI prep.
|
||||
)
|
||||
)
|
||||
|
||||
:: ============================================================
|
||||
:: STEP 3: Data directories
|
||||
:: ============================================================
|
||||
@@ -142,44 +162,6 @@ if not exist "data\preps" mkdir "data\preps"
|
||||
if not exist "bin" mkdir "bin"
|
||||
echo OK: data\ and bin\
|
||||
|
||||
:: ============================================================
|
||||
:: STEP 4: Frontend
|
||||
:: ============================================================
|
||||
if defined SKIP_FRONTEND goto skip_frontend
|
||||
echo [4/5] Building dashboard (server\web)...
|
||||
cd /d "%ROOT%\server\web"
|
||||
if not exist "node_modules" (
|
||||
echo npm install...
|
||||
call npm install
|
||||
if errorlevel 1 (
|
||||
cd /d "%ROOT%"
|
||||
echo ERROR: npm install failed.
|
||||
goto fatal_exit
|
||||
)
|
||||
)
|
||||
echo npm run build...
|
||||
call npm run build
|
||||
if errorlevel 1 (
|
||||
cd /d "%ROOT%"
|
||||
echo ERROR: Frontend build failed.
|
||||
goto fatal_exit
|
||||
)
|
||||
cd /d "%ROOT%"
|
||||
echo Frontend built: server\web\dist
|
||||
goto frontend_done
|
||||
|
||||
:skip_frontend
|
||||
echo [4/5] Skipping frontend build (Node.js unavailable)
|
||||
if not exist "server\web\dist\index.html" (
|
||||
echo WARNING: No server\web\dist\index.html — dashboard may not load.
|
||||
)
|
||||
|
||||
:frontend_done
|
||||
if exist "server\web\dist\index.html" (
|
||||
if not exist "server\webroot" mkdir "server\webroot"
|
||||
xcopy /E /I /Y /Q "server\web\dist\*" "server\webroot\" >nul
|
||||
echo Copied dashboard to server\webroot
|
||||
)
|
||||
|
||||
:: ============================================================
|
||||
:: STEP 5: Server binary
|
||||
@@ -198,7 +180,7 @@ if errorlevel 1 (
|
||||
cd /d "%ROOT%"
|
||||
|
||||
if defined AETHERFORGE_RELEASE (
|
||||
echo Release mode active — set AETHERFORGE_RELEASE=1 for server process.
|
||||
echo Release mode active ??? set AETHERFORGE_RELEASE=1 for server process.
|
||||
)
|
||||
|
||||
if not exist "bin\miner-server.exe" (
|
||||
@@ -208,7 +190,7 @@ if not exist "bin\miner-server.exe" (
|
||||
echo Server binary: bin\miner-server.exe
|
||||
|
||||
:: ============================================================
|
||||
:: LAUNCH (foreground — logs stay in this window)
|
||||
:: LAUNCH (foreground ??? logs stay in this window)
|
||||
:: ============================================================
|
||||
echo.
|
||||
echo Stopping any previous miner-server.exe...
|
||||
@@ -261,7 +243,7 @@ goto end_pause
|
||||
:fatal_exit
|
||||
echo.
|
||||
echo ==============================================================
|
||||
echo LAUNCH FAILED — fix the errors above and run devrun.bat again.
|
||||
echo LAUNCH FAILED ??? fix the errors above and run devrun.bat again.
|
||||
echo ==============================================================
|
||||
echo.
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ echo [5/8] Launcher synced.
|
||||
|
||||
if not exist "%USB%\scripts" mkdir "%USB%\scripts"
|
||||
copy /y "%ROOT%\scripts\usb-start-cloudflared.ps1" "%USB%\scripts\" >nul
|
||||
copy /y "%ROOT%\scripts\launch-prep.bat" "%USB%\scripts\" >nul
|
||||
if not exist "%USB%\data\cloudflared-token.txt" (
|
||||
echo eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWYzNzY5NGYtNDA4Yy00ZWI3LWE2M2MtMzM5MzQ1MjI3NWIwIiwicyI6IlpEa3lPVE5pWWpjdE9USXlZeTAwTlRjNExUaGlZVGN0WWpGaFlUWmtOR05rTXpjMyJ9> "%USB%\data\cloudflared-token.txt"
|
||||
)
|
||||
|
||||
74
scripts/launch-prep.bat
Normal file
74
scripts/launch-prep.bat
Normal file
@@ -0,0 +1,74 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
set "PREP_ROOT=%~1"
|
||||
if "%PREP_ROOT%"=="" (
|
||||
echo [Prep] ERROR: launch-prep.bat requires repo root path.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Pulling latest from origin/main...
|
||||
if exist "%PREP_ROOT%\.git" (
|
||||
pushd "%PREP_ROOT%" >nul
|
||||
git pull origin main
|
||||
if errorlevel 1 (
|
||||
echo [Prep] Note: git pull skipped or failed ^(offline, no remote, or local changes^).
|
||||
) else (
|
||||
echo [Prep] Git pull finished.
|
||||
)
|
||||
popd >nul
|
||||
) else (
|
||||
echo [Prep] Skipped ^(not a git checkout^).
|
||||
)
|
||||
|
||||
if not exist "%PREP_ROOT%\server\web\package.json" (
|
||||
echo [Prep] No server\web\package.json - skipping UI build.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
set "PATH=C:\Program Files\nodejs;%PATH%"
|
||||
where npm >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo [Prep] WARNING: npm not found - skipping UI build.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Building UI ^(server\web^)...
|
||||
cd /d "%PREP_ROOT%\server\web"
|
||||
if not exist "node_modules" (
|
||||
if exist "package-lock.json" (
|
||||
echo [Prep] npm ci...
|
||||
call npm ci
|
||||
) else (
|
||||
echo [Prep] npm install...
|
||||
call npm install
|
||||
)
|
||||
if errorlevel 1 (
|
||||
cd /d "%PREP_ROOT%"
|
||||
echo [Prep] ERROR: npm install failed.
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo [Prep] npm run build ^(this may take a few minutes^)...
|
||||
call npm run build
|
||||
if errorlevel 1 (
|
||||
cd /d "%PREP_ROOT%"
|
||||
echo [Prep] ERROR: Frontend build failed.
|
||||
exit /b 1
|
||||
)
|
||||
cd /d "%PREP_ROOT%"
|
||||
|
||||
if not exist "%PREP_ROOT%\server\webroot" mkdir "%PREP_ROOT%\server\webroot"
|
||||
echo [Prep] Syncing server\webroot...
|
||||
xcopy /E /I /Y /Q "server\web\dist\*" "server\webroot\" >nul
|
||||
|
||||
if exist "%PREP_ROOT%\usb" (
|
||||
if not exist "%PREP_ROOT%\usb\webroot" mkdir "%PREP_ROOT%\usb\webroot"
|
||||
echo [Prep] Syncing usb\webroot...
|
||||
xcopy /E /I /Y /Q "server\web\dist\*" "usb\webroot\" >nul
|
||||
)
|
||||
|
||||
echo [Prep] UI build complete.
|
||||
exit /b 0
|
||||
@@ -253,8 +253,9 @@ func (h *Handler) buildAPKAgent(ctx context.Context, req *BuildRequest) (BuildRe
|
||||
}
|
||||
|
||||
platform := BuildPlatform{GOOS: "linux", GOARCH: "arm64", Ext: ""}
|
||||
h.setProgress(req.CancelToken, "Compiling agent (linux/arm64)", 25)
|
||||
stopCompileProgress := h.tickCompileProgress(ctx, req.CancelToken, "Compiling agent (linux/arm64)", 25, 54, false)
|
||||
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, platform, false)
|
||||
stopCompileProgress()
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
|
||||
@@ -34,10 +34,17 @@ func (h *Handler) buildUniversalAgent(ctx context.Context, req *BuildRequest, pr
|
||||
platforms := platformsForRequest(req)
|
||||
workerPaths := map[string]string{}
|
||||
total := len(platforms)
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
for i, p := range platforms {
|
||||
pct := 14 + (i*56)/total
|
||||
h.setProgress(req.CancelToken, fmt.Sprintf("Compiling %s", p.Label()), pct)
|
||||
startPct := 14 + (i*56)/total
|
||||
endPct := 14 + ((i+1)*56)/total
|
||||
if endPct > 71 {
|
||||
endPct = 71
|
||||
}
|
||||
stage := fmt.Sprintf("Compiling %s", p.Label())
|
||||
stopCompileProgress := h.tickCompileProgress(ctx, req.CancelToken, stage, startPct, endPct, obfuscated)
|
||||
wp, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
stopCompileProgress()
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
|
||||
@@ -690,15 +690,16 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
|
||||
platforms := platformsForRequest(req)
|
||||
p := platforms[0]
|
||||
h.setProgress(req.CancelToken, "Compiling agent", 20)
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
stopCompileProgress := h.tickCompileProgress(ctx, req.CancelToken, "Compiling agent", 20, 71, obfuscated)
|
||||
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
stopCompileProgress()
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
log.Printf("Build failed: %v", err)
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Compiled — linking output", 72)
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
workerName := filepath.Base(outputPath)
|
||||
finalPath := outputPath
|
||||
finalName := workerName
|
||||
|
||||
45
server/internal/builder/progress.go
Normal file
45
server/internal/builder/progress.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// tickCompileProgress emits interpolated progress during long compile steps (garble can run 10+ minutes).
|
||||
// Returns a stop function; call it when the compile finishes.
|
||||
func (h *Handler) tickCompileProgress(ctx context.Context, token, stage string, startPct, capPct int, obfuscated bool) func() {
|
||||
if token == "" || capPct <= startPct {
|
||||
return func() {}
|
||||
}
|
||||
est := 3 * time.Minute
|
||||
if obfuscated {
|
||||
est = 12 * time.Minute
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
h.setProgress(token, stage, startPct)
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
start := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
elapsed := time.Since(start)
|
||||
ratio := float64(elapsed) / float64(est)
|
||||
if ratio > 0.92 {
|
||||
ratio = 0.92
|
||||
}
|
||||
pct := startPct + int(float64(capPct-startPct)*ratio)
|
||||
if pct >= capPct {
|
||||
pct = capPct - 1
|
||||
}
|
||||
h.setProgress(token, stage, pct)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() { close(done) }
|
||||
}
|
||||
36
server/web/src/components/Forge/ForgeDispenseReveal.test.tsx
Normal file
36
server/web/src/components/Forge/ForgeDispenseReveal.test.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ForgeDispenseReveal from './ForgeDispenseReveal';
|
||||
|
||||
vi.mock('../../context/AmbientMusicContext', () => ({
|
||||
useModalAmbientDuck: () => {},
|
||||
}));
|
||||
|
||||
vi.mock('../../context/SoundContext', () => ({
|
||||
useSound: () => ({ play: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('../DownloadButton', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
||||
}));
|
||||
|
||||
describe('ForgeDispenseReveal', () => {
|
||||
it('shows Forged title on successful forge', () => {
|
||||
render(
|
||||
<ForgeDispenseReveal
|
||||
result={{
|
||||
success: true,
|
||||
file_name: 'worker.exe',
|
||||
download_url: '/api/v1/builds/x/download',
|
||||
stealth_score: 72,
|
||||
}}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('heading', { name: 'Forged' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('Dispensed')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ export default function ForgeDispenseReveal({ result, onClose }: Props) {
|
||||
<div className="forge-dispense-panel">
|
||||
<div className="forge-dispense-sigil" aria-hidden />
|
||||
<h2 id="forge-dispense-title" className="forge-dispense-title">
|
||||
Dispensed
|
||||
Forged
|
||||
</h2>
|
||||
<p className="forge-dispense-sub">
|
||||
{result.file_name || 'Your worker'} is ready — each forge carries a unique binary signature.
|
||||
|
||||
48
server/web/src/components/Forge/ForgeProgressBar.test.tsx
Normal file
48
server/web/src/components/Forge/ForgeProgressBar.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import ForgeProgressBar, {
|
||||
FORGE_INITIAL_STAGE,
|
||||
isForgeProgressIndeterminate,
|
||||
} from './ForgeProgressBar';
|
||||
|
||||
describe('isForgeProgressIndeterminate', () => {
|
||||
it('is indeterminate before server reports progress', () => {
|
||||
expect(isForgeProgressIndeterminate(FORGE_INITIAL_STAGE, 0)).toBe(true);
|
||||
expect(isForgeProgressIndeterminate('', 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('is determinate once server reports pct or stage advances', () => {
|
||||
expect(isForgeProgressIndeterminate('Compiling agent', 20)).toBe(false);
|
||||
expect(isForgeProgressIndeterminate(FORGE_INITIAL_STAGE, 5)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ForgeProgressBar', () => {
|
||||
it('renders nothing when not building', () => {
|
||||
const { container } = render(
|
||||
<ForgeProgressBar building={false} stage="" progress={0} />,
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('shows indeterminate track before server progress', () => {
|
||||
const { container } = render(
|
||||
<ForgeProgressBar building stage={FORGE_INITIAL_STAGE} progress={0} />,
|
||||
);
|
||||
expect(screen.getByText('…')).toBeInTheDocument();
|
||||
expect(container.querySelector('.forge-progress-track.indeterminate')).toBeTruthy();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('shows server stage and pct when progress is reported', () => {
|
||||
const { container } = render(
|
||||
<ForgeProgressBar building stage="Compiling agent" progress={42} />,
|
||||
);
|
||||
expect(screen.getByText('Compiling agent')).toBeInTheDocument();
|
||||
expect(screen.getByText('42%')).toBeInTheDocument();
|
||||
expect(container.querySelector('.forge-progress-track.indeterminate')).toBeNull();
|
||||
});
|
||||
});
|
||||
47
server/web/src/components/Forge/ForgeProgressBar.tsx
Normal file
47
server/web/src/components/Forge/ForgeProgressBar.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
export const FORGE_INITIAL_STAGE = 'Initializing forge...';
|
||||
|
||||
interface Props {
|
||||
building: boolean;
|
||||
stage: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/** True while waiting for the first server-reported forge stage. */
|
||||
export function isForgeProgressIndeterminate(stage: string, progress: number): boolean {
|
||||
return progress === 0 && (stage === '' || stage === FORGE_INITIAL_STAGE);
|
||||
}
|
||||
|
||||
export default function ForgeProgressBar({ building, stage, progress }: Props) {
|
||||
if (!building) return null;
|
||||
|
||||
const indeterminate = isForgeProgressIndeterminate(stage, progress);
|
||||
const displayStage = stage || 'Initializing...';
|
||||
const pctLabel = indeterminate ? '…' : `${Math.round(progress)}%`;
|
||||
|
||||
return (
|
||||
<div className="forge-progress-wrap" aria-live="polite">
|
||||
<div className="forge-progress-header">
|
||||
<span className="forge-progress-icon">⚙</span>
|
||||
<span className="forge-progress-stage">{displayStage}</span>
|
||||
<span className="forge-progress-pct">{pctLabel}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`forge-progress-track${indeterminate ? ' indeterminate' : ''}`}
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={indeterminate ? undefined : Math.round(progress)}
|
||||
aria-busy={indeterminate}
|
||||
aria-label={displayStage}
|
||||
>
|
||||
<div
|
||||
className="forge-progress-fill"
|
||||
style={indeterminate ? undefined : { width: `${progress}%` }}
|
||||
/>
|
||||
{!indeterminate && (
|
||||
<div className="forge-progress-glow" style={{ left: `${progress}%` }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -348,10 +348,10 @@ export default function Layout({ children }: LayoutProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`layout${isMobile ? ' layout--mobile' : ''}${othersOnline ? ' layout--comrades-online' : ''}`}
|
||||
className={`layout${isMobile ? ' layout--mobile' : ''}${othersOnline ? ' layout--comrades-online' : ''}${!isMobile && glowParticles ? ' layout--hacker-cursor' : ''}`}
|
||||
data-operator-deck={operatorDeckId(location.pathname)}
|
||||
>
|
||||
{!isMobile && glowParticles && showDeckEffects && <CursorFire />}
|
||||
{!isMobile && glowParticles && <CursorFire />}
|
||||
<AmbientBackground weather={pageWeather} />
|
||||
{glowParticles && <SacredGeometryLayer />}
|
||||
<nav className="sidebar sidebar--desktop desktop-only">
|
||||
|
||||
32
server/web/src/components/Visual/CursorFire.css
Normal file
32
server/web/src/components/Visual/CursorFire.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.cursor-hacker-fx {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.layout--hacker-cursor {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.layout--hacker-cursor a,
|
||||
.layout--hacker-cursor button,
|
||||
.layout--hacker-cursor input,
|
||||
.layout--hacker-cursor select,
|
||||
.layout--hacker-cursor textarea,
|
||||
.layout--hacker-cursor label,
|
||||
.layout--hacker-cursor [role='button'],
|
||||
.layout--hacker-cursor .nav-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cursor-hacker-fx {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.layout--hacker-cursor {
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +1,143 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import './CursorFire.css';
|
||||
|
||||
const BIT_CHARS = '01';
|
||||
const HEX_CHARS = '0123456789ABCDEF';
|
||||
const MAX_PARTICLES = 480;
|
||||
const EMIT_PER_FRAME = 12;
|
||||
const EMIT_WINDOW_MS = 140;
|
||||
const FONT_STACK = '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
life: number; // 1 → 0
|
||||
size: number;
|
||||
life: number;
|
||||
decay: number;
|
||||
char: string;
|
||||
fontSize: number;
|
||||
tint: 'cyan' | 'green';
|
||||
}
|
||||
|
||||
function pickChar(): string {
|
||||
if (Math.random() < 0.88) return BIT_CHARS[Math.floor(Math.random() * 2)];
|
||||
return HEX_CHARS[Math.floor(Math.random() * HEX_CHARS.length)];
|
||||
}
|
||||
|
||||
function colorForLife(life: number, tint: Particle['tint']): string {
|
||||
const a = Math.min(1, life * 1.05);
|
||||
if (tint === 'cyan') {
|
||||
if (life > 0.5) return `rgba(0, 255, 255, ${a})`;
|
||||
return `rgba(0, 240, 200, ${a * 0.92})`;
|
||||
}
|
||||
if (life > 0.5) return `rgba(80, 255, 160, ${a})`;
|
||||
return `rgba(0, 255, 120, ${a * 0.9})`;
|
||||
}
|
||||
|
||||
function glowForTint(tint: Particle['tint']): string {
|
||||
return tint === 'cyan' ? '#00e8f5' : '#00ff88';
|
||||
}
|
||||
|
||||
export default function CursorFire() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const particles = useRef<Particle[]>([]);
|
||||
const mouse = useRef({ x: -9999, y: -9999, moved: false });
|
||||
const rafRef = useRef<number>(0);
|
||||
const mouse = useRef({ x: -9999, y: -9999 });
|
||||
const lastMoveRef = useRef(0);
|
||||
const rafRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
if (motionQuery.matches) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
let running = true;
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
canvas.width = Math.floor(w * dpr);
|
||||
canvas.height = Math.floor(h * dpr);
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
};
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
mouse.current = { x: e.clientX, y: e.clientY, moved: true };
|
||||
mouse.current = { x: e.clientX, y: e.clientY };
|
||||
lastMoveRef.current = performance.now();
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mousemove', onMove, { passive: true });
|
||||
|
||||
const stopOnReducedMotion = () => {
|
||||
if (!motionQuery.matches) return;
|
||||
running = false;
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
particles.current = [];
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
motionQuery.addEventListener('change', stopOnReducedMotion);
|
||||
|
||||
const emit = () => {
|
||||
if (performance.now() - lastMoveRef.current > EMIT_WINDOW_MS) return;
|
||||
const { x, y } = mouse.current;
|
||||
// Emit 6 particles per frame at cursor
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const spread = 8;
|
||||
|
||||
for (let i = 0; i < EMIT_PER_FRAME; i++) {
|
||||
const spread = 14;
|
||||
particles.current.push({
|
||||
x: x + (Math.random() - 0.5) * spread,
|
||||
y: y + (Math.random() - 0.5) * (spread * 0.5),
|
||||
vx: (Math.random() - 0.5) * 1.2,
|
||||
vy: -(Math.random() * 2.8 + 1.8),
|
||||
y: y + (Math.random() - 0.5) * (spread * 0.45),
|
||||
vx: (Math.random() - 0.5) * 1.8,
|
||||
vy: -(Math.random() * 3.2 + 2.1),
|
||||
life: 1,
|
||||
size: Math.random() * 14 + 7,
|
||||
decay: Math.random() * 0.022 + 0.016,
|
||||
decay: Math.random() * 0.016 + 0.012,
|
||||
char: pickChar(),
|
||||
fontSize: Math.random() * 16 + 16,
|
||||
tint: Math.random() < 0.5 ? 'cyan' : 'green',
|
||||
});
|
||||
}
|
||||
// Cap particle count for perf
|
||||
if (particles.current.length > 400) {
|
||||
particles.current = particles.current.slice(-400);
|
||||
|
||||
if (particles.current.length > MAX_PARTICLES) {
|
||||
particles.current = particles.current.slice(-MAX_PARTICLES);
|
||||
}
|
||||
};
|
||||
|
||||
const draw = () => {
|
||||
if (!running) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Additive blending makes overlapping particles look white-hot
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
|
||||
emit();
|
||||
|
||||
const alive: Particle[] = [];
|
||||
for (const p of particles.current) {
|
||||
// Turbulent horizontal drift
|
||||
p.vx += (Math.random() - 0.5) * 0.35;
|
||||
// Slight drag on vx
|
||||
p.vx *= 0.97;
|
||||
// Upward acceleration (heat rises)
|
||||
p.vy -= 0.04;
|
||||
|
||||
p.vx += (Math.random() - 0.5) * 0.28;
|
||||
p.vx *= 0.96;
|
||||
p.vy -= 0.035;
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= p.decay;
|
||||
// Particles shrink as they cool
|
||||
p.size *= 0.982;
|
||||
p.fontSize *= 0.985;
|
||||
|
||||
if (p.life <= 0 || p.size < 1) continue;
|
||||
if (p.life <= 0 || p.fontSize < 8) continue;
|
||||
alive.push(p);
|
||||
|
||||
const l = p.life;
|
||||
// Color temperature: white-yellow core → orange → red → dark red
|
||||
let r: number, g: number, b: number;
|
||||
if (l > 0.75) {
|
||||
// White-hot
|
||||
r = 255; g = 255; b = Math.round((l - 0.75) / 0.25 * 220);
|
||||
} else if (l > 0.5) {
|
||||
// Yellow-orange
|
||||
r = 255; g = Math.round(100 + (l - 0.5) / 0.25 * 155); b = 0;
|
||||
} else if (l > 0.25) {
|
||||
// Orange-red
|
||||
r = 255; g = Math.round((l - 0.25) / 0.25 * 100); b = 0;
|
||||
} else {
|
||||
// Deep red, fading
|
||||
r = Math.round(160 + l / 0.25 * 95); g = 0; b = 0;
|
||||
}
|
||||
|
||||
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size);
|
||||
grad.addColorStop(0, `rgba(${r},${g},${b},${l})`);
|
||||
grad.addColorStop(0.4, `rgba(${r},${Math.round(g * 0.6)},0,${l * 0.6})`);
|
||||
grad.addColorStop(1, `rgba(0,0,0,0)`);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
const glow = 10 + p.life * 18;
|
||||
ctx.shadowBlur = glow;
|
||||
ctx.shadowColor = glowForTint(p.tint);
|
||||
ctx.font = `600 ${p.fontSize}px ${FONT_STACK}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = colorForLife(p.life, p.tint);
|
||||
ctx.fillText(p.char, p.x, p.y);
|
||||
}
|
||||
|
||||
ctx.shadowBlur = 0;
|
||||
particles.current = alive;
|
||||
rafRef.current = requestAnimationFrame(draw);
|
||||
};
|
||||
@@ -116,22 +145,13 @@ export default function CursorFire() {
|
||||
rafRef.current = requestAnimationFrame(draw);
|
||||
|
||||
return () => {
|
||||
running = false;
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
window.removeEventListener('resize', resize);
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
motionQuery.removeEventListener('change', stopOnReducedMotion);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="cursor-fire-fx"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9998,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <canvas ref={canvasRef} className="cursor-hacker-fx" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
@@ -907,9 +907,29 @@ describe('AmbientBackground', () => {
|
||||
describe('CursorFire', () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('mounts fullscreen canvas', () => {
|
||||
it('mounts fullscreen hacker-trail canvas', () => {
|
||||
const { container } = render(<CursorFire />);
|
||||
expect(container.querySelector('canvas')).toBeTruthy();
|
||||
const canvas = container.querySelector('canvas.cursor-hacker-fx');
|
||||
expect(canvas).toBeTruthy();
|
||||
expect(canvas).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('skips animation loop when prefers-reduced-motion', () => {
|
||||
const rafSpy = vi.spyOn(window, 'requestAnimationFrame');
|
||||
const matchMediaSpy = vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||
matches: true,
|
||||
media: '(prefers-reduced-motion: reduce)',
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
render(<CursorFire />);
|
||||
expect(rafSpy).not.toHaveBeenCalled();
|
||||
matchMediaSpy.mockRestore();
|
||||
rafSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -970,7 +990,7 @@ describe('Layout', () => {
|
||||
expect(screen.getByRole('link', { name: /Onion/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('mounts MatrixRain and CursorFire on Command Deck only', async () => {
|
||||
it('mounts MatrixRain on Command Deck only and CursorFire on all desktop deck pages', async () => {
|
||||
const { container: deck } = render(
|
||||
<MemoryRouter initialEntries={['/dashboard']} future={routerFuture}>
|
||||
<Layout>
|
||||
@@ -980,7 +1000,7 @@ describe('Layout', () => {
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(deck.querySelector('.matrix-rain-canvas')).toBeTruthy();
|
||||
expect(deck.querySelector('.cursor-fire-fx')).toBeTruthy();
|
||||
expect(deck.querySelector('.cursor-hacker-fx')).toBeTruthy();
|
||||
});
|
||||
cleanup();
|
||||
|
||||
@@ -995,6 +1015,7 @@ describe('Layout', () => {
|
||||
expect(screen.getByText('crucible')).toBeInTheDocument();
|
||||
});
|
||||
expect(crucible.querySelector('.matrix-rain-canvas')).toBeNull();
|
||||
expect(crucible.querySelector('.cursor-fire-fx')).toBeNull();
|
||||
expect(crucible.querySelector('.cursor-hacker-fx')).toBeTruthy();
|
||||
expect(crucible.querySelector('.layout--hacker-cursor')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
57
server/web/src/hooks/useForgeProgressPoll.ts
Normal file
57
server/web/src/hooks/useForgeProgressPoll.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useForge } from '../context/ForgeContext';
|
||||
|
||||
const FORGE_POLL_MS = 1000;
|
||||
|
||||
/** Poll GET /api/v1/builder/progress/{token} while a forge is running. */
|
||||
export function useForgeProgressPoll(
|
||||
active: boolean,
|
||||
cancelTokenRef: React.RefObject<string>,
|
||||
) {
|
||||
const { startForge, endForge, setStage } = useForge();
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
endForge();
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
return;
|
||||
}
|
||||
startForge();
|
||||
|
||||
const token = cancelTokenRef.current;
|
||||
if (!token) return;
|
||||
|
||||
let alive = true;
|
||||
|
||||
const poll = async () => {
|
||||
if (!alive) return;
|
||||
try {
|
||||
const { authHeaders } = await import('../api/auth');
|
||||
const res = await fetch(`/api/v1/builder/progress/${encodeURIComponent(token)}`, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data: { stage?: string; pct?: number } = await res.json();
|
||||
const pct = typeof data.pct === 'number' ? data.pct : 0;
|
||||
if (alive && (data.stage || pct > 0)) {
|
||||
setStage(data.stage || 'Forging...', pct);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// network hiccup — keep polling
|
||||
}
|
||||
if (alive) {
|
||||
timerRef.current = setTimeout(poll, FORGE_POLL_MS);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
alive = false;
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [active]);
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
} from '../help/forgeFormNormalize';
|
||||
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
||||
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
||||
import ForgeProgressBar from '../components/Forge/ForgeProgressBar';
|
||||
import { useForgeProgressPoll } from '../hooks/useForgeProgressPoll';
|
||||
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
||||
import DownloadButton from '../components/DownloadButton';
|
||||
import PoolPresetPicker from '../components/PoolPresetPicker';
|
||||
@@ -101,29 +103,6 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds
|
||||
return forgeDefaultsFromServerSmart(config, serverInfo, builds);
|
||||
}
|
||||
|
||||
// Poll interval (ms) for real server-side build progress.
|
||||
const FORGE_POLL_MS = 1000;
|
||||
|
||||
function ForgeProgressBar({ building, stage, progress }: { building: boolean; stage: string; progress: number }) {
|
||||
if (!building) return null;
|
||||
return (
|
||||
<div className="forge-progress-wrap" aria-live="polite">
|
||||
<div className="forge-progress-header">
|
||||
<span className="forge-progress-icon">⚙</span>
|
||||
<span className="forge-progress-stage">{stage || 'Initializing...'}</span>
|
||||
<span className="forge-progress-pct">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<div className="forge-progress-track">
|
||||
<div
|
||||
className="forge-progress-fill"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
<div className="forge-progress-glow" style={{ left: `${progress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FORGE_MODE_KEY = 'aetherforge-forge-mode';
|
||||
|
||||
function loadSimpleMode(): boolean {
|
||||
@@ -139,8 +118,7 @@ function loadSimpleMode(): boolean {
|
||||
export default function BuilderPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
||||
const forgeStageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const { setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
||||
|
||||
const [form, setForm] = useState<BuildRequest | null>(null);
|
||||
const [building, setBuilding] = useState(false);
|
||||
@@ -217,52 +195,7 @@ export default function BuilderPage() {
|
||||
|
||||
const forgeSkinClass = forgePageClass(operationMode, forgeTheme);
|
||||
|
||||
// Poll real server-side build progress while a single build is running.
|
||||
// The server exposes GET /api/v1/builder/progress/{token} which returns
|
||||
// {stage, pct} updated at each key compile stage, so the bar reflects
|
||||
// actual server activity instead of a client-side time estimate.
|
||||
useEffect(() => {
|
||||
if (!building || batchJob) {
|
||||
endForge();
|
||||
if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current);
|
||||
return;
|
||||
}
|
||||
startForge();
|
||||
|
||||
const token = cancelTokenRef.current;
|
||||
if (!token) return;
|
||||
|
||||
let active = true;
|
||||
|
||||
const poll = async () => {
|
||||
if (!active) return;
|
||||
try {
|
||||
const { authHeaders } = await import('../api/auth');
|
||||
const res = await fetch(`/api/v1/builder/progress/${token}`, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data: { stage: string; pct: number } = await res.json();
|
||||
if (active && data.stage) {
|
||||
setStage(data.stage, data.pct);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// network hiccup — keep polling
|
||||
}
|
||||
if (active) {
|
||||
forgeStageTimerRef.current = setTimeout(poll, FORGE_POLL_MS);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [building, batchJob]);
|
||||
useForgeProgressPoll(Boolean(building && !batchJob), cancelTokenRef);
|
||||
|
||||
const setForgeMode = (simple: boolean) => {
|
||||
setSimpleMode(simple);
|
||||
@@ -348,7 +281,7 @@ export default function BuilderPage() {
|
||||
}, [searchParams, recentBuilds, form]);
|
||||
|
||||
const finishForgeSuccess = async (result: BuildResponse) => {
|
||||
setStage('Build complete!', 100);
|
||||
setStage('Forged!', 100);
|
||||
setLastBuild(result);
|
||||
setDispenseReveal(result);
|
||||
loadRecentBuilds();
|
||||
@@ -789,7 +722,6 @@ export default function BuilderPage() {
|
||||
cancelToken,
|
||||
onStep: (step) => {
|
||||
setMissionStep(step);
|
||||
if (step === 'forge') startForge();
|
||||
},
|
||||
});
|
||||
setMissionExportSkipped(result.exportSkipped);
|
||||
|
||||
@@ -13,6 +13,8 @@ import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import { HelpTip } from '../components/HelpTip';
|
||||
import AlsoHere from '../components/Presence/AlsoHere';
|
||||
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
||||
import ForgeProgressBar from '../components/Forge/ForgeProgressBar';
|
||||
import { useForgeProgressPoll } from '../hooks/useForgeProgressPoll';
|
||||
import { useModalAmbientDuck } from '../context/AmbientMusicContext';
|
||||
import { useForge } from '../context/ForgeContext';
|
||||
import { forgeDefaultsFromServerSmart, applySmartForgeDefaults } from '../help/forgeSmartDefaults';
|
||||
@@ -53,25 +55,6 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds
|
||||
return forgeDefaultsFromServerSmart(config, serverInfo, builds);
|
||||
}
|
||||
|
||||
const FORGE_POLL_MS = 1000;
|
||||
|
||||
function ForgeProgressBar({ building, stage, progress }: { building: boolean; stage: string; progress: number }) {
|
||||
if (!building) return null;
|
||||
return (
|
||||
<div className="forge-progress-wrap" aria-live="polite">
|
||||
<div className="forge-progress-header">
|
||||
<span className="forge-progress-icon">⚙</span>
|
||||
<span className="forge-progress-stage">{stage || 'Initializing...'}</span>
|
||||
<span className="forge-progress-pct">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<div className="forge-progress-track">
|
||||
<div className="forge-progress-fill" style={{ width: `${progress}%` }} />
|
||||
<div className="forge-progress-glow" style={{ left: `${progress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function previewAccentForChip(chip: MissionOperationChip): 'cyan' | 'magenta' | 'amber' | 'gold' {
|
||||
if (chip === 'ghost') return 'cyan';
|
||||
if (chip === 'loud') return 'magenta';
|
||||
@@ -80,9 +63,7 @@ function previewAccentForChip(chip: MissionOperationChip): 'cyan' | 'magenta' |
|
||||
|
||||
export default function MissionDeckPage() {
|
||||
|
||||
const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
||||
|
||||
const forgeStageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const { setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
||||
|
||||
const cancelTokenRef = useRef('');
|
||||
|
||||
@@ -166,52 +147,10 @@ export default function MissionDeckPage() {
|
||||
.catch(() => setError('Failed to load server config — is the control server running?'))
|
||||
.finally(() => setLoadingDefaults(false));
|
||||
}, []);
|
||||
// Poll real server-side build progress (same as BuilderPage).
|
||||
useEffect(() => {
|
||||
if (!building) {
|
||||
endForge();
|
||||
if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current);
|
||||
return;
|
||||
}
|
||||
startForge();
|
||||
useForgeProgressPoll(building, cancelTokenRef);
|
||||
|
||||
const token = cancelTokenRef.current;
|
||||
if (!token) return;
|
||||
|
||||
let active = true;
|
||||
|
||||
const poll = async () => {
|
||||
if (!active) return;
|
||||
try {
|
||||
const { authHeaders } = await import('../api/auth');
|
||||
const res = await fetch(`/api/v1/builder/progress/${token}`, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data: { stage: string; pct: number } = await res.json();
|
||||
if (active && data.stage) {
|
||||
setStage(data.stage, data.pct);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// network hiccup — keep polling
|
||||
}
|
||||
if (active) {
|
||||
forgeStageTimerRef.current = setTimeout(poll, FORGE_POLL_MS);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [building]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
const tok = cancelTokenRef.current;
|
||||
if (tok) api.cancelBuild(tok).catch(() => {});
|
||||
};
|
||||
@@ -247,7 +186,7 @@ export default function MissionDeckPage() {
|
||||
};
|
||||
|
||||
const finishForgeSuccess = async (result: BuildResponse) => {
|
||||
setStage('Build complete!', 100);
|
||||
setStage('Forged!', 100);
|
||||
setDispenseReveal(result);
|
||||
};
|
||||
|
||||
@@ -301,7 +240,6 @@ export default function MissionDeckPage() {
|
||||
cancelToken,
|
||||
onStep: (step) => {
|
||||
setMissionStep(step);
|
||||
if (step === 'forge') startForge();
|
||||
},
|
||||
});
|
||||
setMissionExportSkipped(result.exportSkipped);
|
||||
|
||||
@@ -1591,11 +1591,25 @@ button.deliverable-card .form-hint {
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, #ff6a00, #ffb300, #ffd700);
|
||||
box-shadow: 0 0 8px rgba(255, 160, 0, 0.6);
|
||||
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.forge-progress-track.indeterminate {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.forge-progress-track.indeterminate .forge-progress-fill {
|
||||
width: 35%;
|
||||
animation: forge-progress-indeterminate 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes forge-progress-indeterminate {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(320%); }
|
||||
}
|
||||
|
||||
.forge-progress-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -580,8 +580,8 @@ export default function SettingsPage() {
|
||||
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
|
||||
<h2 className="font-display">Deck Atmosphere</h2>
|
||||
<p className="section-desc">
|
||||
Background glow particles and sparkles sit behind the UI (pointer-events off). Turn off on
|
||||
low-power devices if you want a calmer deck.
|
||||
Rising 0/1 and hex glyphs follow your pointer on desktop deck pages. Background glow
|
||||
particles sit behind the UI. Turn off on low-power devices for a calmer deck.
|
||||
</p>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
@@ -591,7 +591,7 @@ export default function SettingsPage() {
|
||||
checked={glowParticles}
|
||||
onChange={(e) => setGlowParticles(e.target.checked)}
|
||||
/>
|
||||
<span>Glow particles & sparkles</span>
|
||||
<span>Hacker cursor trail</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginTop: '1.25rem' }}>
|
||||
|
||||
@@ -45,7 +45,7 @@ body {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.cursor-fire-fx {
|
||||
.cursor-hacker-fx {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('visualPrefs', () => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('defaults glow particles to on', () => {
|
||||
it('defaults hacker cursor trail to on', () => {
|
||||
expect(loadGlowParticlesEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
174
usb/LAUNCH.bat
174
usb/LAUNCH.bat
@@ -2,23 +2,52 @@
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
title AetherForge Control Deck
|
||||
cd /d "%~dp0"
|
||||
set "REPO=%CD%"
|
||||
|
||||
echo.
|
||||
echo ================================================================
|
||||
echo AetherForge - One-Click Launch
|
||||
echo ================================================================
|
||||
echo Folder: %REPO%
|
||||
echo.
|
||||
|
||||
set "PREP=%REPO%\scripts\launch-prep.bat"
|
||||
if not exist "%PREP%" set "PREP=%REPO%\..\scripts\launch-prep.bat"
|
||||
if exist "%PREP%" (
|
||||
call "%PREP%" "%REPO%"
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo LAUNCH prep failed - fix errors above and retry.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo [Prep] launch-prep.bat not found - skipping pull/UI build.
|
||||
)
|
||||
|
||||
if /i "%AF_LAUNCH_DRY_RUN%"=="1" (
|
||||
echo.
|
||||
echo [Dry run] Prep steps OK - not starting tunnel or server.
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
:: Portable deck root = folder that contains AetherForge.exe (repo usb\ or copied USB drive)
|
||||
set "ROOT="
|
||||
if exist "%CD%\AetherForge.exe" (
|
||||
set "ROOT=!CD!"
|
||||
) else if exist "%CD%\usb\AetherForge.exe" (
|
||||
cd /d "%CD%\usb"
|
||||
set "ROOT=!CD!"
|
||||
) else (
|
||||
echo.
|
||||
echo ERROR: AetherForge.exe not found.
|
||||
echo Expected next to this script, or in usb\AetherForge.exe
|
||||
echo Run pack-usb.bat from the repo to build the portable bundle.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if defined ROOT if exist "%ROOT%\AetherForge.exe" goto portable_deck
|
||||
|
||||
:: No portable binary - dev control server from repo
|
||||
goto dev_server_launch
|
||||
|
||||
:portable_deck
|
||||
|
||||
if not exist "%ROOT%\AetherForge.exe" (
|
||||
echo ERROR: AetherForge.exe missing in %ROOT%
|
||||
pause
|
||||
@@ -50,7 +79,6 @@ if exist "%BUNDLED_GO%" (
|
||||
goto go_ready
|
||||
)
|
||||
|
||||
:: Check if Go is installed system-wide
|
||||
where go >nul 2>nul
|
||||
if not errorlevel 1 (
|
||||
echo [Go] Using system Go installation.
|
||||
@@ -58,7 +86,6 @@ if not errorlevel 1 (
|
||||
goto go_ready
|
||||
)
|
||||
|
||||
:: Go not found anywhere - skip optional tools, proceed directly to server
|
||||
echo.
|
||||
echo [Go] Go not found - Forge obfuscation tools unavailable. Running server without them.
|
||||
echo.
|
||||
@@ -66,17 +93,11 @@ goto server_launch
|
||||
|
||||
:go_ready
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 2. Pin all Go caches to the USB so module downloads travel with you
|
||||
:: ----------------------------------------------------------------
|
||||
set "GOPATH=%ROOT%\toolchain\gopath"
|
||||
set "GOMODCACHE=%ROOT%\toolchain\gopath\pkg\mod"
|
||||
set "GOCACHE=%ROOT%\toolchain\gocache"
|
||||
set "GOENV=off"
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 3. Install optional Forge tools if missing (non-fatal)
|
||||
:: ----------------------------------------------------------------
|
||||
if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
||||
if not exist "%ROOT%\toolchain\gopath\bin\garble.exe" (
|
||||
echo [Tools] Installing garble...
|
||||
@@ -95,9 +116,8 @@ if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
||||
|
||||
:server_launch
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 4. Ensure data directories exist
|
||||
:: ----------------------------------------------------------------
|
||||
echo.
|
||||
echo Ensuring data directories...
|
||||
if not exist "%ROOT%\data\builds" mkdir "%ROOT%\data\builds"
|
||||
if not exist "%ROOT%\data\logs" mkdir "%ROOT%\data\logs"
|
||||
if not exist "%ROOT%\data\spread-kits" mkdir "%ROOT%\data\spread-kits"
|
||||
@@ -105,9 +125,6 @@ if not exist "%ROOT%\data\uploads" mkdir "%ROOT%\data\uploads"
|
||||
if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints"
|
||||
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 5. Detect LAN IP for display
|
||||
:: ----------------------------------------------------------------
|
||||
set "SERVER_PORT=8989"
|
||||
set "CONFIG_FILE=%ROOT%\data\config.json"
|
||||
if exist "%CONFIG_FILE%" (
|
||||
@@ -123,9 +140,7 @@ set "LAN_IP=localhost"
|
||||
:lan_done
|
||||
set "LAN_IP=%LAN_IP: =%"
|
||||
|
||||
:: ----------------------------------------------------------------
|
||||
:: 6. Kill any stale server and tunnel processes
|
||||
:: ----------------------------------------------------------------
|
||||
echo Stopping stale processes...
|
||||
taskkill /F /IM AetherForge.exe >nul 2>nul
|
||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||
ping -n 2 127.0.0.1 >nul
|
||||
@@ -139,12 +154,11 @@ echo LAN: http://%LAN_IP%:%SERVER_PORT%
|
||||
echo Data: %ROOT%\data\
|
||||
echo.
|
||||
echo Login accounts: admin + comrade ^(passwords below after start^).
|
||||
echo Cloudflare tunnel starts below ^(LAUNCH + server^).
|
||||
echo Press Ctrl+C to stop.
|
||||
echo ================================================================
|
||||
echo.
|
||||
|
||||
:: Start Cloudflare connector before server ^(works with old or new AetherForge.exe^)
|
||||
echo Starting tunnel...
|
||||
set "CF_SCRIPT=%ROOT%\scripts\usb-start-cloudflared.ps1"
|
||||
if not exist "%CF_SCRIPT%" set "CF_SCRIPT=%ROOT%\..\scripts\usb-start-cloudflared.ps1"
|
||||
if exist "%CF_SCRIPT%" (
|
||||
@@ -154,31 +168,123 @@ if exist "%CF_SCRIPT%" (
|
||||
)
|
||||
echo.
|
||||
|
||||
:: Ensure cwd matches deck root so webroot resolution finds usb\webroot
|
||||
cd /d "%ROOT%"
|
||||
|
||||
:: Open browser after short delay
|
||||
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
|
||||
|
||||
:: Launch server (LAUNCH already started cloudflared above — tell server not to spawn a second copy)
|
||||
echo Starting server...
|
||||
set "AF_TUNNEL_EXTERNAL=1"
|
||||
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
|
||||
set "EC=!ERRORLEVEL!"
|
||||
|
||||
if exist "%ROOT%\data\cloudflared.pid" (
|
||||
for /f "usebackq" %%P in ("%ROOT%\data\cloudflared.pid") do (
|
||||
call :cleanup_tunnel "%ROOT%"
|
||||
goto server_stopped
|
||||
|
||||
:dev_server_launch
|
||||
echo.
|
||||
echo No AetherForge.exe - starting dev control server ^(repo^).
|
||||
echo ^(Run pack-usb.bat for portable USB bundle.^)
|
||||
echo.
|
||||
|
||||
set "PATH=C:\Program Files\Go\bin;%USERPROFILE%\go\bin;C:\Program Files\nodejs;%PATH%"
|
||||
set "DATA=%REPO%\data"
|
||||
set "DECK=%REPO%"
|
||||
if exist "%REPO%\usb\tools\cloudflared.exe" set "DECK=%REPO%\usb"
|
||||
if exist "%REPO%\usb\data" if not exist "%DECK%\data" set "DECK=%REPO%\usb"
|
||||
|
||||
echo Ensuring data directories...
|
||||
if not exist "%DATA%\builds" mkdir "%DATA%\builds"
|
||||
if not exist "%DATA%\logs" mkdir "%DATA%\logs"
|
||||
if not exist "%DATA%\spread-kits" mkdir "%DATA%\spread-kits"
|
||||
if not exist "%DATA%\uploads" mkdir "%DATA%\uploads"
|
||||
if not exist "%DATA%\blueprints" mkdir "%DATA%\blueprints"
|
||||
if not exist "%DATA%\preps" mkdir "%DATA%\preps"
|
||||
if not exist "%REPO%\bin" mkdir "%REPO%\bin"
|
||||
|
||||
set "SERVER_PORT=8989"
|
||||
set "CONFIG_FILE=%DATA%\config.json"
|
||||
if exist "%CONFIG_FILE%" (
|
||||
for /f "usebackq delims=" %%P in (`powershell -NoProfile -Command "try { $j = Get-Content -Raw '%CONFIG_FILE%' | ConvertFrom-Json; if ($j.port) { $j.port } } catch { }"`) do (
|
||||
if not "%%P"=="" set "SERVER_PORT=%%P"
|
||||
)
|
||||
)
|
||||
set "LAN_IP=localhost"
|
||||
|
||||
echo Stopping stale processes...
|
||||
taskkill /F /IM miner-server.exe >nul 2>nul
|
||||
taskkill /F /IM AetherForge.exe >nul 2>nul
|
||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||
ping -n 2 127.0.0.1 >nul
|
||||
|
||||
where go >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Go not found - install from https://go.dev/dl/ or use pack-usb.bat
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "%REPO%\bin\miner-server.exe" (
|
||||
echo Building control server...
|
||||
cd /d "%REPO%\server"
|
||||
go mod download >nul 2>nul
|
||||
go build -ldflags="-s -w" -o "..\bin\miner-server.exe" .
|
||||
if errorlevel 1 (
|
||||
cd /d "%REPO%"
|
||||
echo ERROR: Server build failed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd /d "%REPO%"
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ================================================================
|
||||
echo STARTING CONTROL SERVER ^(dev^)
|
||||
echo ================================================================
|
||||
echo Dashboard: http://localhost:%SERVER_PORT%
|
||||
echo Data: %DATA%\
|
||||
echo ================================================================
|
||||
echo.
|
||||
|
||||
echo Starting tunnel...
|
||||
set "CF_SCRIPT=%REPO%\scripts\usb-start-cloudflared.ps1"
|
||||
if not exist "%CF_SCRIPT%" set "CF_SCRIPT=%DECK%\scripts\usb-start-cloudflared.ps1"
|
||||
if exist "%CF_SCRIPT%" (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%CF_SCRIPT%" -DeckRoot "%DECK%"
|
||||
) else (
|
||||
echo [Tunnel] WARNING: usb-start-cloudflared.ps1 not found.
|
||||
)
|
||||
echo.
|
||||
|
||||
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
|
||||
|
||||
echo Starting server...
|
||||
cd /d "%REPO%"
|
||||
set "AF_TUNNEL_EXTERNAL=1"
|
||||
"%REPO%\bin\miner-server.exe" -data "%DATA%"
|
||||
set "EC=!ERRORLEVEL!"
|
||||
|
||||
call :cleanup_tunnel "%DECK%"
|
||||
goto server_stopped
|
||||
|
||||
:cleanup_tunnel
|
||||
set "TROOT=%~1"
|
||||
if exist "%TROOT%\data\cloudflared.pid" (
|
||||
for /f "usebackq" %%P in ("%TROOT%\data\cloudflared.pid") do (
|
||||
taskkill /F /PID %%P >nul 2>nul
|
||||
)
|
||||
del "%ROOT%\data\cloudflared.pid" 2>nul
|
||||
del "%TROOT%\data\cloudflared.pid" 2>nul
|
||||
)
|
||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||
exit /b 0
|
||||
|
||||
:server_stopped
|
||||
echo.
|
||||
if "!EC!"=="0" (
|
||||
echo [Server] Stopped normally.
|
||||
) else (
|
||||
echo [Server] Exited with code !EC!.
|
||||
echo If port %SERVER_PORT% is in use, close other AetherForge windows and retry.
|
||||
echo If port %SERVER_PORT% is in use, close other server windows and retry.
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
Reference in New Issue
Block a user