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
|
setlocal EnableExtensions EnableDelayedExpansion
|
||||||
title AetherForge Control Deck
|
title AetherForge Control Deck
|
||||||
cd /d "%~dp0"
|
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)
|
:: Portable deck root = folder that contains AetherForge.exe (repo usb\ or copied USB drive)
|
||||||
|
set "ROOT="
|
||||||
if exist "%CD%\AetherForge.exe" (
|
if exist "%CD%\AetherForge.exe" (
|
||||||
set "ROOT=!CD!"
|
set "ROOT=!CD!"
|
||||||
) else if exist "%CD%\usb\AetherForge.exe" (
|
) else if exist "%CD%\usb\AetherForge.exe" (
|
||||||
cd /d "%CD%\usb"
|
cd /d "%CD%\usb"
|
||||||
set "ROOT=!CD!"
|
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" (
|
if not exist "%ROOT%\AetherForge.exe" (
|
||||||
echo ERROR: AetherForge.exe missing in %ROOT%
|
echo ERROR: AetherForge.exe missing in %ROOT%
|
||||||
pause
|
pause
|
||||||
@@ -50,7 +79,6 @@ if exist "%BUNDLED_GO%" (
|
|||||||
goto go_ready
|
goto go_ready
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Check if Go is installed system-wide
|
|
||||||
where go >nul 2>nul
|
where go >nul 2>nul
|
||||||
if not errorlevel 1 (
|
if not errorlevel 1 (
|
||||||
echo [Go] Using system Go installation.
|
echo [Go] Using system Go installation.
|
||||||
@@ -58,7 +86,6 @@ if not errorlevel 1 (
|
|||||||
goto go_ready
|
goto go_ready
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Go not found anywhere - skip optional tools, proceed directly to server
|
|
||||||
echo.
|
echo.
|
||||||
echo [Go] Go not found - Forge obfuscation tools unavailable. Running server without them.
|
echo [Go] Go not found - Forge obfuscation tools unavailable. Running server without them.
|
||||||
echo.
|
echo.
|
||||||
@@ -66,17 +93,11 @@ goto server_launch
|
|||||||
|
|
||||||
:go_ready
|
:go_ready
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
:: 2. Pin all Go caches to the USB so module downloads travel with you
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
set "GOPATH=%ROOT%\toolchain\gopath"
|
set "GOPATH=%ROOT%\toolchain\gopath"
|
||||||
set "GOMODCACHE=%ROOT%\toolchain\gopath\pkg\mod"
|
set "GOMODCACHE=%ROOT%\toolchain\gopath\pkg\mod"
|
||||||
set "GOCACHE=%ROOT%\toolchain\gocache"
|
set "GOCACHE=%ROOT%\toolchain\gocache"
|
||||||
set "GOENV=off"
|
set "GOENV=off"
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
:: 3. Install optional Forge tools if missing (non-fatal)
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
||||||
if not exist "%ROOT%\toolchain\gopath\bin\garble.exe" (
|
if not exist "%ROOT%\toolchain\gopath\bin\garble.exe" (
|
||||||
echo [Tools] Installing garble...
|
echo [Tools] Installing garble...
|
||||||
@@ -95,9 +116,8 @@ if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
|||||||
|
|
||||||
:server_launch
|
:server_launch
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
echo.
|
||||||
:: 4. Ensure data directories exist
|
echo Ensuring data directories...
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
if not exist "%ROOT%\data\builds" mkdir "%ROOT%\data\builds"
|
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\logs" mkdir "%ROOT%\data\logs"
|
||||||
if not exist "%ROOT%\data\spread-kits" mkdir "%ROOT%\data\spread-kits"
|
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\blueprints" mkdir "%ROOT%\data\blueprints"
|
||||||
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
|
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
:: 5. Detect LAN IP for display
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
set "SERVER_PORT=8989"
|
set "SERVER_PORT=8989"
|
||||||
set "CONFIG_FILE=%ROOT%\data\config.json"
|
set "CONFIG_FILE=%ROOT%\data\config.json"
|
||||||
if exist "%CONFIG_FILE%" (
|
if exist "%CONFIG_FILE%" (
|
||||||
@@ -123,9 +140,7 @@ set "LAN_IP=localhost"
|
|||||||
:lan_done
|
:lan_done
|
||||||
set "LAN_IP=%LAN_IP: =%"
|
set "LAN_IP=%LAN_IP: =%"
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
echo Stopping stale processes...
|
||||||
:: 6. Kill any stale server and tunnel processes
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
taskkill /F /IM AetherForge.exe >nul 2>nul
|
taskkill /F /IM AetherForge.exe >nul 2>nul
|
||||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||||
ping -n 2 127.0.0.1 >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 Data: %ROOT%\data\
|
||||||
echo.
|
echo.
|
||||||
echo Login accounts: admin + comrade ^(passwords below after start^).
|
echo Login accounts: admin + comrade ^(passwords below after start^).
|
||||||
echo Cloudflare tunnel starts below ^(LAUNCH + server^).
|
|
||||||
echo Press Ctrl+C to stop.
|
echo Press Ctrl+C to stop.
|
||||||
echo ================================================================
|
echo ================================================================
|
||||||
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"
|
set "CF_SCRIPT=%ROOT%\scripts\usb-start-cloudflared.ps1"
|
||||||
if not exist "%CF_SCRIPT%" 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%" (
|
if exist "%CF_SCRIPT%" (
|
||||||
@@ -154,31 +168,123 @@ if exist "%CF_SCRIPT%" (
|
|||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: Ensure cwd matches deck root so webroot resolution finds usb\webroot
|
|
||||||
cd /d "%ROOT%"
|
cd /d "%ROOT%"
|
||||||
|
|
||||||
:: Open browser after short delay
|
|
||||||
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
|
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"
|
set "AF_TUNNEL_EXTERNAL=1"
|
||||||
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
|
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
|
||||||
set "EC=!ERRORLEVEL!"
|
set "EC=!ERRORLEVEL!"
|
||||||
|
|
||||||
if exist "%ROOT%\data\cloudflared.pid" (
|
call :cleanup_tunnel "%ROOT%"
|
||||||
for /f "usebackq" %%P in ("%ROOT%\data\cloudflared.pid") do (
|
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
|
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
|
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:server_stopped
|
||||||
echo.
|
echo.
|
||||||
if "!EC!"=="0" (
|
if "!EC!"=="0" (
|
||||||
echo [Server] Stopped normally.
|
echo [Server] Stopped normally.
|
||||||
) else (
|
) else (
|
||||||
echo [Server] Exited with code !EC!.
|
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.
|
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.
|
**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).
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
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)
|
### Operator path (dev control PC)
|
||||||
|
|
||||||
1. Double-click **`devrun.bat`** in the project root.
|
1. **`LAUNCH.bat`** — same one-click path as above (preferred).
|
||||||
Installs Go/Node if missing, builds the dashboard, compiles `bin\miner-server.exe`, copies web assets, and starts the server.
|
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. **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
|
|
||||||
|
|
||||||
|
9. Watch fleet stats on **Command Deck**; campaign hits in **Emberwake** War Room
|
||||||
### Portable USB command deck
|
### Portable USB command deck
|
||||||
|
|
||||||
1. Run **`pack-usb.bat`** from repo root (re-run after any code change)
|
1. Run **`pack-usb.bat`** from repo root (re-run after any code change)
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ func main() {
|
|||||||
sharesFound++
|
sharesFound++
|
||||||
fmt.Printf(" ★ SHARE job=%-14s nonce=%s\n", jobID, nonce)
|
fmt.Printf(" ★ SHARE job=%-14s nonce=%s\n", jobID, nonce)
|
||||||
})
|
})
|
||||||
pool.Start()
|
|
||||||
pool.SetJob(&job.Job{
|
pool.SetJob(&job.Job{
|
||||||
ID: "validate-001",
|
ID: "validate-001",
|
||||||
Blob: testBlobHex,
|
Blob: testBlobHex,
|
||||||
@@ -177,9 +176,11 @@ func main() {
|
|||||||
SeedHash: testSeedHex,
|
SeedHash: testSeedHex,
|
||||||
Height: 3000000,
|
Height: 3000000,
|
||||||
})
|
})
|
||||||
|
pool.Start()
|
||||||
|
|
||||||
fmt.Printf(" ✓ %d worker(s) started, job injected\n", *threads)
|
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)
|
done := time.After(time.Duration(*seconds) * time.Second)
|
||||||
elapsed := 0
|
elapsed := 0
|
||||||
var finalHS float64
|
var finalHS float64
|
||||||
@@ -187,20 +188,23 @@ loop:
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
elapsed += 5
|
elapsed += 1
|
||||||
hs := pool.HashesPerSecond()
|
hs := pool.HashesPerSecond()
|
||||||
pool.ResetHashCounter()
|
pool.ResetHashCounter()
|
||||||
finalHS = hs
|
finalHS = hs
|
||||||
fmt.Printf(" [%2ds] %8.1f H/s shares=%d\n", elapsed, hs, sharesFound)
|
fmt.Printf(" [%2ds] %8.1f H/s shares=%d\n", elapsed, hs, sharesFound)
|
||||||
case <-done:
|
case <-done:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
if hs := pool.HashesPerSecond(); hs > finalHS {
|
||||||
|
finalHS = hs
|
||||||
|
}
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pool.Stop()
|
pool.Stop()
|
||||||
|
|
||||||
if finalHS < 1 {
|
if finalHS < 1 && sharesFound == 0 {
|
||||||
fail(fmt.Sprintf("hashrate is 0 after %ds", *seconds), &ok)
|
fail(fmt.Sprintf("hashrate is 0 and no shares after %ds", *seconds), &ok)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n ✓ %.0f H/s total / %.0f H/s per thread\n", finalHS, finalHS/float64(*threads))
|
fmt.Printf("\n ✓ %.0f H/s total / %.0f H/s per thread\n", finalHS, finalHS/float64(*threads))
|
||||||
if sharesFound > 0 {
|
if sharesFound > 0 {
|
||||||
|
|||||||
66
devrun.bat
66
devrun.bat
@@ -73,7 +73,7 @@ echo Go installed.
|
|||||||
|
|
||||||
:go_ready
|
: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)...
|
echo [1.5/5] Checking Forge tools (Garble, go-winres)...
|
||||||
where garble >nul 2>nul
|
where garble >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -131,6 +131,26 @@ echo Node.js installed.
|
|||||||
|
|
||||||
:node_ready
|
: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
|
:: STEP 3: Data directories
|
||||||
:: ============================================================
|
:: ============================================================
|
||||||
@@ -142,44 +162,6 @@ if not exist "data\preps" mkdir "data\preps"
|
|||||||
if not exist "bin" mkdir "bin"
|
if not exist "bin" mkdir "bin"
|
||||||
echo OK: data\ and 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
|
:: STEP 5: Server binary
|
||||||
@@ -198,7 +180,7 @@ if errorlevel 1 (
|
|||||||
cd /d "%ROOT%"
|
cd /d "%ROOT%"
|
||||||
|
|
||||||
if defined AETHERFORGE_RELEASE (
|
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" (
|
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
|
echo Server binary: bin\miner-server.exe
|
||||||
|
|
||||||
:: ============================================================
|
:: ============================================================
|
||||||
:: LAUNCH (foreground — logs stay in this window)
|
:: LAUNCH (foreground ??? logs stay in this window)
|
||||||
:: ============================================================
|
:: ============================================================
|
||||||
echo.
|
echo.
|
||||||
echo Stopping any previous miner-server.exe...
|
echo Stopping any previous miner-server.exe...
|
||||||
@@ -261,7 +243,7 @@ goto end_pause
|
|||||||
:fatal_exit
|
:fatal_exit
|
||||||
echo.
|
echo.
|
||||||
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 ==============================================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ echo [5/8] Launcher synced.
|
|||||||
|
|
||||||
if not exist "%USB%\scripts" mkdir "%USB%\scripts"
|
if not exist "%USB%\scripts" mkdir "%USB%\scripts"
|
||||||
copy /y "%ROOT%\scripts\usb-start-cloudflared.ps1" "%USB%\scripts\" >nul
|
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" (
|
if not exist "%USB%\data\cloudflared-token.txt" (
|
||||||
echo eyJhIjoiODk1NDc5YWIzNTQwZGFhNmQ2MWFlNzAyYTUxNjQ0NzUiLCJ0IjoiYWYzNzY5NGYtNDA4Yy00ZWI3LWE2M2MtMzM5MzQ1MjI3NWIwIiwicyI6IlpEa3lPVE5pWWpjdE9USXlZeTAwTlRjNExUaGlZVGN0WWpGaFlUWmtOR05rTXpjMyJ9> "%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: ""}
|
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)
|
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, platform, false)
|
||||||
|
stopCompileProgress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanupBuild()
|
cleanupBuild()
|
||||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
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)
|
platforms := platformsForRequest(req)
|
||||||
workerPaths := map[string]string{}
|
workerPaths := map[string]string{}
|
||||||
total := len(platforms)
|
total := len(platforms)
|
||||||
|
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||||
for i, p := range platforms {
|
for i, p := range platforms {
|
||||||
pct := 14 + (i*56)/total
|
startPct := 14 + (i*56)/total
|
||||||
h.setProgress(req.CancelToken, fmt.Sprintf("Compiling %s", p.Label()), pct)
|
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)
|
wp, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||||
|
stopCompileProgress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanupBuild()
|
cleanupBuild()
|
||||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
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)
|
platforms := platformsForRequest(req)
|
||||||
p := platforms[0]
|
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)
|
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||||
|
stopCompileProgress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanupBuild()
|
cleanupBuild()
|
||||||
log.Printf("Build failed: %v", err)
|
log.Printf("Build failed: %v", err)
|
||||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||||
}
|
}
|
||||||
h.setProgress(req.CancelToken, "Compiled — linking output", 72)
|
h.setProgress(req.CancelToken, "Compiled — linking output", 72)
|
||||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
|
||||||
workerName := filepath.Base(outputPath)
|
workerName := filepath.Base(outputPath)
|
||||||
finalPath := outputPath
|
finalPath := outputPath
|
||||||
finalName := workerName
|
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-panel">
|
||||||
<div className="forge-dispense-sigil" aria-hidden />
|
<div className="forge-dispense-sigil" aria-hidden />
|
||||||
<h2 id="forge-dispense-title" className="forge-dispense-title">
|
<h2 id="forge-dispense-title" className="forge-dispense-title">
|
||||||
Dispensed
|
Forged
|
||||||
</h2>
|
</h2>
|
||||||
<p className="forge-dispense-sub">
|
<p className="forge-dispense-sub">
|
||||||
{result.file_name || 'Your worker'} is ready — each forge carries a unique binary signature.
|
{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 (
|
return (
|
||||||
<div
|
<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)}
|
data-operator-deck={operatorDeckId(location.pathname)}
|
||||||
>
|
>
|
||||||
{!isMobile && glowParticles && showDeckEffects && <CursorFire />}
|
{!isMobile && glowParticles && <CursorFire />}
|
||||||
<AmbientBackground weather={pageWeather} />
|
<AmbientBackground weather={pageWeather} />
|
||||||
{glowParticles && <SacredGeometryLayer />}
|
{glowParticles && <SacredGeometryLayer />}
|
||||||
<nav className="sidebar sidebar--desktop desktop-only">
|
<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 { 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 {
|
interface Particle {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
vx: number;
|
vx: number;
|
||||||
vy: number;
|
vy: number;
|
||||||
life: number; // 1 → 0
|
life: number;
|
||||||
size: number;
|
|
||||||
decay: 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() {
|
export default function CursorFire() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const particles = useRef<Particle[]>([]);
|
const particles = useRef<Particle[]>([]);
|
||||||
const mouse = useRef({ x: -9999, y: -9999, moved: false });
|
const mouse = useRef({ x: -9999, y: -9999 });
|
||||||
const rafRef = useRef<number>(0);
|
const lastMoveRef = useRef(0);
|
||||||
|
const rafRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
if (motionQuery.matches) return;
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let running = true;
|
||||||
|
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
canvas.width = window.innerWidth;
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
canvas.height = window.innerHeight;
|
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();
|
resize();
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
|
|
||||||
const onMove = (e: MouseEvent) => {
|
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 = () => {
|
const emit = () => {
|
||||||
|
if (performance.now() - lastMoveRef.current > EMIT_WINDOW_MS) return;
|
||||||
const { x, y } = mouse.current;
|
const { x, y } = mouse.current;
|
||||||
// Emit 6 particles per frame at cursor
|
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < EMIT_PER_FRAME; i++) {
|
||||||
const spread = 8;
|
const spread = 14;
|
||||||
particles.current.push({
|
particles.current.push({
|
||||||
x: x + (Math.random() - 0.5) * spread,
|
x: x + (Math.random() - 0.5) * spread,
|
||||||
y: y + (Math.random() - 0.5) * (spread * 0.5),
|
y: y + (Math.random() - 0.5) * (spread * 0.45),
|
||||||
vx: (Math.random() - 0.5) * 1.2,
|
vx: (Math.random() - 0.5) * 1.8,
|
||||||
vy: -(Math.random() * 2.8 + 1.8),
|
vy: -(Math.random() * 3.2 + 2.1),
|
||||||
life: 1,
|
life: 1,
|
||||||
size: Math.random() * 14 + 7,
|
decay: Math.random() * 0.016 + 0.012,
|
||||||
decay: Math.random() * 0.022 + 0.016,
|
char: pickChar(),
|
||||||
|
fontSize: Math.random() * 16 + 16,
|
||||||
|
tint: Math.random() < 0.5 ? 'cyan' : 'green',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Cap particle count for perf
|
|
||||||
if (particles.current.length > 400) {
|
if (particles.current.length > MAX_PARTICLES) {
|
||||||
particles.current = particles.current.slice(-400);
|
particles.current = particles.current.slice(-MAX_PARTICLES);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
|
if (!running) return;
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Additive blending makes overlapping particles look white-hot
|
|
||||||
ctx.globalCompositeOperation = 'screen';
|
|
||||||
|
|
||||||
emit();
|
emit();
|
||||||
|
|
||||||
const alive: Particle[] = [];
|
const alive: Particle[] = [];
|
||||||
for (const p of particles.current) {
|
for (const p of particles.current) {
|
||||||
// Turbulent horizontal drift
|
p.vx += (Math.random() - 0.5) * 0.28;
|
||||||
p.vx += (Math.random() - 0.5) * 0.35;
|
p.vx *= 0.96;
|
||||||
// Slight drag on vx
|
p.vy -= 0.035;
|
||||||
p.vx *= 0.97;
|
|
||||||
// Upward acceleration (heat rises)
|
|
||||||
p.vy -= 0.04;
|
|
||||||
|
|
||||||
p.x += p.vx;
|
p.x += p.vx;
|
||||||
p.y += p.vy;
|
p.y += p.vy;
|
||||||
p.life -= p.decay;
|
p.life -= p.decay;
|
||||||
// Particles shrink as they cool
|
p.fontSize *= 0.985;
|
||||||
p.size *= 0.982;
|
|
||||||
|
|
||||||
if (p.life <= 0 || p.size < 1) continue;
|
if (p.life <= 0 || p.fontSize < 8) continue;
|
||||||
alive.push(p);
|
alive.push(p);
|
||||||
|
|
||||||
const l = p.life;
|
const glow = 10 + p.life * 18;
|
||||||
// Color temperature: white-yellow core → orange → red → dark red
|
ctx.shadowBlur = glow;
|
||||||
let r: number, g: number, b: number;
|
ctx.shadowColor = glowForTint(p.tint);
|
||||||
if (l > 0.75) {
|
ctx.font = `600 ${p.fontSize}px ${FONT_STACK}`;
|
||||||
// White-hot
|
ctx.textAlign = 'center';
|
||||||
r = 255; g = 255; b = Math.round((l - 0.75) / 0.25 * 220);
|
ctx.textBaseline = 'middle';
|
||||||
} else if (l > 0.5) {
|
ctx.fillStyle = colorForLife(p.life, p.tint);
|
||||||
// Yellow-orange
|
ctx.fillText(p.char, p.x, p.y);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
particles.current = alive;
|
particles.current = alive;
|
||||||
rafRef.current = requestAnimationFrame(draw);
|
rafRef.current = requestAnimationFrame(draw);
|
||||||
};
|
};
|
||||||
@@ -116,22 +145,13 @@ export default function CursorFire() {
|
|||||||
rafRef.current = requestAnimationFrame(draw);
|
rafRef.current = requestAnimationFrame(draw);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
running = false;
|
||||||
cancelAnimationFrame(rafRef.current);
|
cancelAnimationFrame(rafRef.current);
|
||||||
window.removeEventListener('resize', resize);
|
window.removeEventListener('resize', resize);
|
||||||
window.removeEventListener('mousemove', onMove);
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
motionQuery.removeEventListener('change', stopOnReducedMotion);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return <canvas ref={canvasRef} className="cursor-hacker-fx" aria-hidden="true" />;
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
className="cursor-fire-fx"
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
inset: 0,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 9998,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -907,9 +907,29 @@ describe('AmbientBackground', () => {
|
|||||||
describe('CursorFire', () => {
|
describe('CursorFire', () => {
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
it('mounts fullscreen canvas', () => {
|
it('mounts fullscreen hacker-trail canvas', () => {
|
||||||
const { container } = render(<CursorFire />);
|
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();
|
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(
|
const { container: deck } = render(
|
||||||
<MemoryRouter initialEntries={['/dashboard']} future={routerFuture}>
|
<MemoryRouter initialEntries={['/dashboard']} future={routerFuture}>
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -980,7 +1000,7 @@ describe('Layout', () => {
|
|||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(deck.querySelector('.matrix-rain-canvas')).toBeTruthy();
|
expect(deck.querySelector('.matrix-rain-canvas')).toBeTruthy();
|
||||||
expect(deck.querySelector('.cursor-fire-fx')).toBeTruthy();
|
expect(deck.querySelector('.cursor-hacker-fx')).toBeTruthy();
|
||||||
});
|
});
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
@@ -995,6 +1015,7 @@ describe('Layout', () => {
|
|||||||
expect(screen.getByText('crucible')).toBeInTheDocument();
|
expect(screen.getByText('crucible')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(crucible.querySelector('.matrix-rain-canvas')).toBeNull();
|
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';
|
} from '../help/forgeFormNormalize';
|
||||||
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
||||||
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
||||||
|
import ForgeProgressBar from '../components/Forge/ForgeProgressBar';
|
||||||
|
import { useForgeProgressPoll } from '../hooks/useForgeProgressPoll';
|
||||||
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
||||||
import DownloadButton from '../components/DownloadButton';
|
import DownloadButton from '../components/DownloadButton';
|
||||||
import PoolPresetPicker from '../components/PoolPresetPicker';
|
import PoolPresetPicker from '../components/PoolPresetPicker';
|
||||||
@@ -101,29 +103,6 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds
|
|||||||
return forgeDefaultsFromServerSmart(config, 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';
|
const FORGE_MODE_KEY = 'aetherforge-forge-mode';
|
||||||
|
|
||||||
function loadSimpleMode(): boolean {
|
function loadSimpleMode(): boolean {
|
||||||
@@ -139,8 +118,7 @@ function loadSimpleMode(): boolean {
|
|||||||
export default function BuilderPage() {
|
export default function BuilderPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
const { setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
||||||
const forgeStageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
const [form, setForm] = useState<BuildRequest | null>(null);
|
const [form, setForm] = useState<BuildRequest | null>(null);
|
||||||
const [building, setBuilding] = useState(false);
|
const [building, setBuilding] = useState(false);
|
||||||
@@ -217,52 +195,7 @@ export default function BuilderPage() {
|
|||||||
|
|
||||||
const forgeSkinClass = forgePageClass(operationMode, forgeTheme);
|
const forgeSkinClass = forgePageClass(operationMode, forgeTheme);
|
||||||
|
|
||||||
// Poll real server-side build progress while a single build is running.
|
useForgeProgressPoll(Boolean(building && !batchJob), cancelTokenRef);
|
||||||
// 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]);
|
|
||||||
|
|
||||||
const setForgeMode = (simple: boolean) => {
|
const setForgeMode = (simple: boolean) => {
|
||||||
setSimpleMode(simple);
|
setSimpleMode(simple);
|
||||||
@@ -348,7 +281,7 @@ export default function BuilderPage() {
|
|||||||
}, [searchParams, recentBuilds, form]);
|
}, [searchParams, recentBuilds, form]);
|
||||||
|
|
||||||
const finishForgeSuccess = async (result: BuildResponse) => {
|
const finishForgeSuccess = async (result: BuildResponse) => {
|
||||||
setStage('Build complete!', 100);
|
setStage('Forged!', 100);
|
||||||
setLastBuild(result);
|
setLastBuild(result);
|
||||||
setDispenseReveal(result);
|
setDispenseReveal(result);
|
||||||
loadRecentBuilds();
|
loadRecentBuilds();
|
||||||
@@ -789,7 +722,6 @@ export default function BuilderPage() {
|
|||||||
cancelToken,
|
cancelToken,
|
||||||
onStep: (step) => {
|
onStep: (step) => {
|
||||||
setMissionStep(step);
|
setMissionStep(step);
|
||||||
if (step === 'forge') startForge();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setMissionExportSkipped(result.exportSkipped);
|
setMissionExportSkipped(result.exportSkipped);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import NeonCard from '../components/NeonCard/NeonCard';
|
|||||||
import { HelpTip } from '../components/HelpTip';
|
import { HelpTip } from '../components/HelpTip';
|
||||||
import AlsoHere from '../components/Presence/AlsoHere';
|
import AlsoHere from '../components/Presence/AlsoHere';
|
||||||
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal';
|
||||||
|
import ForgeProgressBar from '../components/Forge/ForgeProgressBar';
|
||||||
|
import { useForgeProgressPoll } from '../hooks/useForgeProgressPoll';
|
||||||
import { useModalAmbientDuck } from '../context/AmbientMusicContext';
|
import { useModalAmbientDuck } from '../context/AmbientMusicContext';
|
||||||
import { useForge } from '../context/ForgeContext';
|
import { useForge } from '../context/ForgeContext';
|
||||||
import { forgeDefaultsFromServerSmart, applySmartForgeDefaults } from '../help/forgeSmartDefaults';
|
import { forgeDefaultsFromServerSmart, applySmartForgeDefaults } from '../help/forgeSmartDefaults';
|
||||||
@@ -53,25 +55,6 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds
|
|||||||
return forgeDefaultsFromServerSmart(config, 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' {
|
function previewAccentForChip(chip: MissionOperationChip): 'cyan' | 'magenta' | 'amber' | 'gold' {
|
||||||
if (chip === 'ghost') return 'cyan';
|
if (chip === 'ghost') return 'cyan';
|
||||||
if (chip === 'loud') return 'magenta';
|
if (chip === 'loud') return 'magenta';
|
||||||
@@ -80,9 +63,7 @@ function previewAccentForChip(chip: MissionOperationChip): 'cyan' | 'magenta' |
|
|||||||
|
|
||||||
export default function MissionDeckPage() {
|
export default function MissionDeckPage() {
|
||||||
|
|
||||||
const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
const { setStage, stage: forgeStage, progress: forgeProgress } = useForge();
|
||||||
|
|
||||||
const forgeStageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
const cancelTokenRef = useRef('');
|
const cancelTokenRef = useRef('');
|
||||||
|
|
||||||
@@ -166,52 +147,10 @@ export default function MissionDeckPage() {
|
|||||||
.catch(() => setError('Failed to load server config — is the control server running?'))
|
.catch(() => setError('Failed to load server config — is the control server running?'))
|
||||||
.finally(() => setLoadingDefaults(false));
|
.finally(() => setLoadingDefaults(false));
|
||||||
}, []);
|
}, []);
|
||||||
// Poll real server-side build progress (same as BuilderPage).
|
useForgeProgressPoll(building, cancelTokenRef);
|
||||||
useEffect(() => {
|
|
||||||
if (!building) {
|
|
||||||
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]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
||||||
const tok = cancelTokenRef.current;
|
const tok = cancelTokenRef.current;
|
||||||
if (tok) api.cancelBuild(tok).catch(() => {});
|
if (tok) api.cancelBuild(tok).catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -247,7 +186,7 @@ export default function MissionDeckPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const finishForgeSuccess = async (result: BuildResponse) => {
|
const finishForgeSuccess = async (result: BuildResponse) => {
|
||||||
setStage('Build complete!', 100);
|
setStage('Forged!', 100);
|
||||||
setDispenseReveal(result);
|
setDispenseReveal(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +240,6 @@ export default function MissionDeckPage() {
|
|||||||
cancelToken,
|
cancelToken,
|
||||||
onStep: (step) => {
|
onStep: (step) => {
|
||||||
setMissionStep(step);
|
setMissionStep(step);
|
||||||
if (step === 'forge') startForge();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setMissionExportSkipped(result.exportSkipped);
|
setMissionExportSkipped(result.exportSkipped);
|
||||||
|
|||||||
@@ -1591,11 +1591,25 @@ button.deliverable-card .form-hint {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: linear-gradient(90deg, #ff6a00, #ffb300, #ffd700);
|
background: linear-gradient(90deg, #ff6a00, #ffb300, #ffd700);
|
||||||
box-shadow: 0 0 8px rgba(255, 160, 0, 0.6);
|
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;
|
position: relative;
|
||||||
z-index: 1;
|
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 {
|
.forge-progress-glow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -580,8 +580,8 @@ export default function SettingsPage() {
|
|||||||
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
|
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
|
||||||
<h2 className="font-display">Deck Atmosphere</h2>
|
<h2 className="font-display">Deck Atmosphere</h2>
|
||||||
<p className="section-desc">
|
<p className="section-desc">
|
||||||
Background glow particles and sparkles sit behind the UI (pointer-events off). Turn off on
|
Rising 0/1 and hex glyphs follow your pointer on desktop deck pages. Background glow
|
||||||
low-power devices if you want a calmer deck.
|
particles sit behind the UI. Turn off on low-power devices for a calmer deck.
|
||||||
</p>
|
</p>
|
||||||
<div className="form-group checkbox-group">
|
<div className="form-group checkbox-group">
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
@@ -591,7 +591,7 @@ export default function SettingsPage() {
|
|||||||
checked={glowParticles}
|
checked={glowParticles}
|
||||||
onChange={(e) => setGlowParticles(e.target.checked)}
|
onChange={(e) => setGlowParticles(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Glow particles & sparkles</span>
|
<span>Hacker cursor trail</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" style={{ marginTop: '1.25rem' }}>
|
<div className="form-group" style={{ marginTop: '1.25rem' }}>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ body {
|
|||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-fire-fx {
|
.cursor-hacker-fx {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('visualPrefs', () => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults glow particles to on', () => {
|
it('defaults hacker cursor trail to on', () => {
|
||||||
expect(loadGlowParticlesEnabled()).toBe(true);
|
expect(loadGlowParticlesEnabled()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
174
usb/LAUNCH.bat
174
usb/LAUNCH.bat
@@ -2,23 +2,52 @@
|
|||||||
setlocal EnableExtensions EnableDelayedExpansion
|
setlocal EnableExtensions EnableDelayedExpansion
|
||||||
title AetherForge Control Deck
|
title AetherForge Control Deck
|
||||||
cd /d "%~dp0"
|
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)
|
:: Portable deck root = folder that contains AetherForge.exe (repo usb\ or copied USB drive)
|
||||||
|
set "ROOT="
|
||||||
if exist "%CD%\AetherForge.exe" (
|
if exist "%CD%\AetherForge.exe" (
|
||||||
set "ROOT=!CD!"
|
set "ROOT=!CD!"
|
||||||
) else if exist "%CD%\usb\AetherForge.exe" (
|
) else if exist "%CD%\usb\AetherForge.exe" (
|
||||||
cd /d "%CD%\usb"
|
cd /d "%CD%\usb"
|
||||||
set "ROOT=!CD!"
|
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" (
|
if not exist "%ROOT%\AetherForge.exe" (
|
||||||
echo ERROR: AetherForge.exe missing in %ROOT%
|
echo ERROR: AetherForge.exe missing in %ROOT%
|
||||||
pause
|
pause
|
||||||
@@ -50,7 +79,6 @@ if exist "%BUNDLED_GO%" (
|
|||||||
goto go_ready
|
goto go_ready
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Check if Go is installed system-wide
|
|
||||||
where go >nul 2>nul
|
where go >nul 2>nul
|
||||||
if not errorlevel 1 (
|
if not errorlevel 1 (
|
||||||
echo [Go] Using system Go installation.
|
echo [Go] Using system Go installation.
|
||||||
@@ -58,7 +86,6 @@ if not errorlevel 1 (
|
|||||||
goto go_ready
|
goto go_ready
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Go not found anywhere - skip optional tools, proceed directly to server
|
|
||||||
echo.
|
echo.
|
||||||
echo [Go] Go not found - Forge obfuscation tools unavailable. Running server without them.
|
echo [Go] Go not found - Forge obfuscation tools unavailable. Running server without them.
|
||||||
echo.
|
echo.
|
||||||
@@ -66,17 +93,11 @@ goto server_launch
|
|||||||
|
|
||||||
:go_ready
|
:go_ready
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
:: 2. Pin all Go caches to the USB so module downloads travel with you
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
set "GOPATH=%ROOT%\toolchain\gopath"
|
set "GOPATH=%ROOT%\toolchain\gopath"
|
||||||
set "GOMODCACHE=%ROOT%\toolchain\gopath\pkg\mod"
|
set "GOMODCACHE=%ROOT%\toolchain\gopath\pkg\mod"
|
||||||
set "GOCACHE=%ROOT%\toolchain\gocache"
|
set "GOCACHE=%ROOT%\toolchain\gocache"
|
||||||
set "GOENV=off"
|
set "GOENV=off"
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
:: 3. Install optional Forge tools if missing (non-fatal)
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
||||||
if not exist "%ROOT%\toolchain\gopath\bin\garble.exe" (
|
if not exist "%ROOT%\toolchain\gopath\bin\garble.exe" (
|
||||||
echo [Tools] Installing garble...
|
echo [Tools] Installing garble...
|
||||||
@@ -95,9 +116,8 @@ if /i not "%AF_INSTALL_TOOLS%"=="0" (
|
|||||||
|
|
||||||
:server_launch
|
:server_launch
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
echo.
|
||||||
:: 4. Ensure data directories exist
|
echo Ensuring data directories...
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
if not exist "%ROOT%\data\builds" mkdir "%ROOT%\data\builds"
|
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\logs" mkdir "%ROOT%\data\logs"
|
||||||
if not exist "%ROOT%\data\spread-kits" mkdir "%ROOT%\data\spread-kits"
|
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\blueprints" mkdir "%ROOT%\data\blueprints"
|
||||||
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
|
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
:: 5. Detect LAN IP for display
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
set "SERVER_PORT=8989"
|
set "SERVER_PORT=8989"
|
||||||
set "CONFIG_FILE=%ROOT%\data\config.json"
|
set "CONFIG_FILE=%ROOT%\data\config.json"
|
||||||
if exist "%CONFIG_FILE%" (
|
if exist "%CONFIG_FILE%" (
|
||||||
@@ -123,9 +140,7 @@ set "LAN_IP=localhost"
|
|||||||
:lan_done
|
:lan_done
|
||||||
set "LAN_IP=%LAN_IP: =%"
|
set "LAN_IP=%LAN_IP: =%"
|
||||||
|
|
||||||
:: ----------------------------------------------------------------
|
echo Stopping stale processes...
|
||||||
:: 6. Kill any stale server and tunnel processes
|
|
||||||
:: ----------------------------------------------------------------
|
|
||||||
taskkill /F /IM AetherForge.exe >nul 2>nul
|
taskkill /F /IM AetherForge.exe >nul 2>nul
|
||||||
taskkill /F /IM cloudflared.exe >nul 2>nul
|
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||||
ping -n 2 127.0.0.1 >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 Data: %ROOT%\data\
|
||||||
echo.
|
echo.
|
||||||
echo Login accounts: admin + comrade ^(passwords below after start^).
|
echo Login accounts: admin + comrade ^(passwords below after start^).
|
||||||
echo Cloudflare tunnel starts below ^(LAUNCH + server^).
|
|
||||||
echo Press Ctrl+C to stop.
|
echo Press Ctrl+C to stop.
|
||||||
echo ================================================================
|
echo ================================================================
|
||||||
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"
|
set "CF_SCRIPT=%ROOT%\scripts\usb-start-cloudflared.ps1"
|
||||||
if not exist "%CF_SCRIPT%" 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%" (
|
if exist "%CF_SCRIPT%" (
|
||||||
@@ -154,31 +168,123 @@ if exist "%CF_SCRIPT%" (
|
|||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: Ensure cwd matches deck root so webroot resolution finds usb\webroot
|
|
||||||
cd /d "%ROOT%"
|
cd /d "%ROOT%"
|
||||||
|
|
||||||
:: Open browser after short delay
|
|
||||||
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
|
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"
|
set "AF_TUNNEL_EXTERNAL=1"
|
||||||
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
|
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
|
||||||
set "EC=!ERRORLEVEL!"
|
set "EC=!ERRORLEVEL!"
|
||||||
|
|
||||||
if exist "%ROOT%\data\cloudflared.pid" (
|
call :cleanup_tunnel "%ROOT%"
|
||||||
for /f "usebackq" %%P in ("%ROOT%\data\cloudflared.pid") do (
|
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
|
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
|
taskkill /F /IM cloudflared.exe >nul 2>nul
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:server_stopped
|
||||||
echo.
|
echo.
|
||||||
if "!EC!"=="0" (
|
if "!EC!"=="0" (
|
||||||
echo [Server] Stopped normally.
|
echo [Server] Stopped normally.
|
||||||
) else (
|
) else (
|
||||||
echo [Server] Exited with code !EC!.
|
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.
|
echo.
|
||||||
|
|||||||
Reference in New Issue
Block a user