Initial commit: VortexGPU rent-a-GPU platform + Linux node agent (nightmare 4080 SUPER)

This commit is contained in:
drjones
2026-08-24 21:07:59 -07:00
commit 28b2c7339f
24 changed files with 8198 additions and 0 deletions

27
.env.example Normal file
View File

@@ -0,0 +1,27 @@
# VortexGPU — rent-a-PC platform
# Gateway port
PORT=3000
# Hidden admin (openssl rand -hex 24)
ADMIN_TOKEN="CHANGE_ME"
# Windows GPU node agent secret
NODE_SECRET="CHANGE_ME"
# Proxmox hypervisor
PVE_HOST=10.30.20.85
PVE_USER=root
PVE_TEMPLATE_WIN=504 # comandoVM (Windows 10, template:1)
PVE_TEMPLATE_LINUX=990 # vortex-linux-tpl (debian cloud-init)
PVE_VMID_START=2000
# BTCPay
BTCPAY_URL=https://10.30.20.140
BTCPAY_API_KEY=4367faaccfa84c9a58d8fa65190290b94e385799
BTCPAY_STORE_ID=B3nkkrKECSugCBTWEXrm9PtTHNsVzP9WxESryzRdrF5T
BTCPAY_PUBLIC=https://btcpay.thetempleofdoom.com
BTCPAY_WEBHOOK_SECRET=vortexgpu-webhook-secret-2026
# Marketing tier label shown to tenants (decoupled from real hardware)
GPU_SKU=NVIDIA GeForce RTX 5090 32GB

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
dist/
.env
data/
*.log
*.pid
.DS_Store

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# VortexGPU — Anonymous GPU Rental Platform (Production)
No-KYC, BTC-paid, browser-access GPU machines. Each tenant provisions a
**completely isolated ComfyUI instance** on a shared physical GPU — they see a
clean private machine and never know the GPU is time-shared.
## Architecture
```
┌──────────────────────────────────────────┐
Tenant browser ────► │ VortexGPU Gateway (CT 731, .127:3000) │
│ Express + Vite SPA + SQLite │
│ • public SPA (login/provision/instances) │
│ • real BTCPay invoices + webhook settle │
│ • hidden admin /admin?token= │
└───────────────┬──────────────────────────┘
│ LAN (poll, X-NODE-SECRET)
┌─────────────────────────┴─────────────────────────┐
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ shadow-death (.128) │ │ GamingPC (.186) │
│ RTX 4080 SUPER 16GB │ │ RTX 3070 │
│ vortex-node-agent.ps1 │ │ vortex-node-agent.ps1 │
│ → telemetry │ │ → telemetry │
│ → provision_comfyui │ │ → provision_comfyui │
│ (isolated port+dir) │ │ (isolated port+dir) │
└────────────────────────┘ └────────────────────────┘
```
## Isolation model (how the GPU stays hidden)
One shared ComfyUI codebase per host (`C:\vortex\comfyui-base`), **N isolated
data dirs** (`C:\vortex\instances\<vm_id>\{user,output,input,models}`). Each
instance launches with its own `--port` and its own `--user-directory`,
`--output-directory`, `--input-directory`, so settings, checkpoints, and outputs
never cross tenant boundaries. The tenant's ComfyUI URL is
`http://<node-ip>:<port>` — they see a private, fresh machine. The physical RTX
is shared underneath and the VRAM load is load-balanced across tenants.
## BTCPay (real payments)
- Store: `B3nkkrKECSugCBTWEXrm9PtTHNsVzP9WxESryzRdrF5T`
- API key: `4367faaccfa84c9a58d8fa65190290b94e385799` (scoped: create/view invoices)
- Webhook: `4vyiy59LW3xbrrpDAKvzoV``POST /api/btcpay/webhook`
- Flow: create USD invoice → tenant pays at checkout link → BTCPay fires
`InvoiceSettled` webhook → gateway credits `users.balance_minutes` in SQLite.
- Dust threshold: $1 minimum (below ~$1 is BTC dust). Price = $1/hr.
## Endpoints
| Path | Auth | Purpose |
|------|------|---------|
| `POST /api/session` | none | No-KYC login → user row (120 min welcome bonus) |
| `GET /api/me?userId=` | none | balance + instances |
| `POST /api/btcpay/create-invoice` | none | real BTCPay invoice |
| `POST /api/btcpay/webhook` | BTCPay | settle invoice → credit balance |
| `POST /api/vms/provision` | none | allocate isolated ComfyUI on a GPU node |
| `POST /api/node/register` | X-NODE-SECRET | GPU box registers |
| `POST /api/node/report` | X-NODE-SECRET | nvidia-smi telemetry |
| `GET /api/node/jobs` | X-NODE-SECRET | poll for shell/provision/destroy jobs |
| `GET /admin?token=` | token | hidden admin SPA (404 without token) |
| `GET /api/admin/*` | Bearer | admin state, GPU jobs, credit, users |
## Windows GPU agent
On each GPU host: `C:\vortex\vortex-node-agent.ps1` (scheduled task
`VortexGPUNodeAgent`, ONSTART). It registers, streams telemetry every 5s, and
handles `provision_comfyui` / `destroy_instance` / `shell` jobs.
## Deploy notes
- CT 730 = template, CT 731 = live clone @ `10.30.20.127:3000`.
- Node.js 22 (native `node:sqlite`), systemd `vortexgpu`.
- Admin: `http://10.30.20.127:3000/admin?token=<ADMIN_TOKEN>` (in `/opt/vortexgpu/.env`).
- Secrets: `ADMIN_TOKEN`, `NODE_SECRET`, BTCPay keys → `/opt/vortexgpu/.env` (600).

13
admin.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow" />
<title>VortexGPU Admin</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/admin.tsx"></script>
</body>
</html>

18
index.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
<meta name="description" content="An application built with Google AI Studio." />
<meta property="og:title" content="My Google AI Studio App" />
<meta property="og:description" content="An application built with Google AI Studio." />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4353
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "tsx server.ts",
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
"start": "node dist/server.cjs",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14",
"@types/three": "^0.185.4",
"@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"three": "^0.185.1",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3",
"@types/express": "^4.17.21"
}
}

498
server.ts Normal file
View File

@@ -0,0 +1,498 @@
import express from "express";
import path from "path";
import fs from "fs";
import crypto from "crypto";
import https from "https";
import { execFile } from "child_process";
import { promisify } from "util";
import { createServer as createViteServer } from "vite";
import { DatabaseSync } from "node:sqlite";
const exec = promisify(execFile);
/**
* VortexGPU — rent-a-PC platform (production build)
*
* Real virtualization: drives Proxmox via `qm` over SSH to clone real KVM VMs.
* - Windows 10 (RDP) : clone template 504 (comandoVM, template:1)
* - Linux (SSH) : clone template 990 (vortex-linux-tpl, debian cloud-init)
* GPU offload: hashcat/comfyui jobs dispatch to the Windows GPU hosts (.128 4080S,
* .186 3070) over LAN — the shared GPU pool is hidden from tenants.
* Per-tenant access: each VM gets a dedicated RDP/SSH port + subdomain (real
* per-tenant entry; true rotating-residential IPs would need a proxy provider).
* Billing: $1/hr flat, up to 3 VMs per user, auto-stop at zero balance.
* Payments: BTCPay (real invoices + HMAC-signed webhook settlement).
* Admin: /admin?token= (404 without token) + /api/admin/* (Bearer).
*/
const PORT = Number(process.env.PORT) || 3000;
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(24).toString("hex");
const NODE_SECRET = process.env.NODE_SECRET || crypto.randomBytes(24).toString("hex");
// ---- Proxmox ----
const PVE_HOST = process.env.PVE_HOST || "10.30.20.85";
const PVE_USER = process.env.PVE_USER || "root";
const PVE_TEMPLATE_WIN = Number(process.env.PVE_TEMPLATE_WIN) || 504;
const PVE_TEMPLATE_LINUX = Number(process.env.PVE_TEMPLATE_LINUX) || 990;
const PVE_VMID_START = Number(process.env.PVE_VMID_START) || 2000;
// ---- BTCPay ----
const BTCPAY_URL = process.env.BTCPAY_URL || "https://10.30.20.140";
const BTCPAY_API_KEY = process.env.BTCPAY_API_KEY || "";
const BTCPAY_STORE_ID = process.env.BTCPAY_STORE_ID || "";
const BTCPAY_PUBLIC = process.env.BTCPAY_PUBLIC || "https://btcpay.thetempleofdoom.com";
const WEBHOOK_SECRET = process.env.BTCPAY_WEBHOOK_SECRET || "vortexgpu-webhook-secret-2026";
const PRICE_USD_PER_HOUR = 1.0;
const MAX_VMS_PER_USER = 3;
// Marketing tier label (what tenants see) — configurable, decoupled from truth.
const GPU_SKU = process.env.GPU_SKU || "NVIDIA GeForce RTX 5090 32GB";
// ---- GPU node registry (in-memory; agents phone home) ----
type GpuNode = {
hostname: string;
ip: string;
gpuModel: string;
driverVersion: string;
memTotalMb: number;
memUsedMb: number;
gpuUtilPct: number;
tempC: number;
cpuUtilPct: number;
ramTotalGb: number;
ramUsedGb: number;
uptimeSec: number;
lastSeen: number;
};
type GpuJob = {
id: string;
hostname: string;
kind: "shell" | "hashcat" | "comfyui";
command: string;
payload: Record<string, unknown>;
status: "pending" | "running" | "done" | "failed";
result: string;
createdAt: number;
completedAt: number | null;
};
const DATA_DIR = path.join(process.cwd(), "data");
fs.mkdirSync(DATA_DIR, { recursive: true });
const NODES_FILE = path.join(DATA_DIR, "nodes.json");
const JOBS_FILE = path.join(DATA_DIR, "jobs.json");
function loadJson<T>(f: string, fb: T): T { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return fb; } }
function saveJson(f: string, d: unknown) { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(f, JSON.stringify(d, null, 2)); }
const nodes: Record<string, GpuNode> = loadJson(NODES_FILE, {});
const jobs: GpuJob[] = loadJson(JOBS_FILE, []);
function persistNodes() { saveJson(NODES_FILE, nodes); }
function persistJobs() { saveJson(JOBS_FILE, jobs); }
function num(v: unknown, fb: number): number { const n = Number(v); return Number.isFinite(n) ? n : fb; }
function str(v: unknown, fb: string): string { return typeof v === "string" && v.length ? v : fb; }
function normHost(v: unknown): string { return str(v, "").toLowerCase().trim(); }
// ---- SQLite ----
const db = new DatabaseSync(path.join(DATA_DIR, "vortex.db"));
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL,
balance_minutes INTEGER NOT NULL DEFAULT 0, btc_address TEXT, created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS vms (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
vm_id INTEGER NOT NULL, -- Proxmox VMID
node_hostname TEXT NOT NULL, -- Proxmox host (pve)
name TEXT NOT NULL, os TEXT NOT NULL,
sku TEXT NOT NULL, -- marketing label shown to tenant
state TEXT NOT NULL DEFAULT 'provisioning',
ip TEXT, port INTEGER, -- assigned access port (rdp/ssh)
username TEXT, password TEXT, -- tenant credentials
app TEXT, -- optional one-click app
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS invoices (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
amount_usd REAL NOT NULL, minutes INTEGER NOT NULL,
btcpay_invoice_id TEXT, checkout_link TEXT,
status TEXT NOT NULL DEFAULT 'pending', created_at INTEGER NOT NULL, settled_at INTEGER
);
`);
// ---- Migrations (add columns that predate password/unlimited) ----
function ensureColumn(table: string, col: string, ddl: string) {
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map((c: any) => c.name);
if (!cols.includes(col)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
}
ensureColumn("users", "password_hash", "password_hash TEXT");
ensureColumn("users", "unlimited", "unlimited INTEGER NOT NULL DEFAULT 0");
function q(sql: string, ...p: (string | number)[]) { return db.prepare(sql).run(...p); }
function one<T>(sql: string, ...p: (string | number)[]): T | undefined { return db.prepare(sql).get(...p) as T | undefined; }
function all<T>(sql: string, ...p: (string | number)[]): T[] { return db.prepare(sql).all(...p) as T[]; }
// ---- Password hashing (scrypt) ----
function hashPassword(pw: string): string {
const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto.scryptSync(pw, salt, 32).toString("hex");
return `${salt}:${hash}`;
}
function verifyPassword(pw: string, stored: string): boolean {
if (!stored || !stored.includes(":")) return false;
const [salt, hash] = stored.split(":");
try {
const h = crypto.scryptSync(pw, salt, 32);
return crypto.timingSafeEqual(Buffer.from(hash, "hex"), h);
} catch { return false; }
}
// Seed the owner account: drjones / czapiewski, unlimited machines.
(function seedOwner() {
const existing = one<any>("SELECT * FROM users WHERE username=?", "drjones");
if (!existing) {
q("INSERT INTO users (id,username,balance_minutes,btc_address,created_at,password_hash,unlimited) VALUES (?,?,?,?,?,?,?)",
"usr_drjones", "drjones", 1_000_000_000, "bc1q" + crypto.randomBytes(16).toString("hex"), Date.now(), hashPassword("czapiewski"), 1);
console.log("[auth] seeded owner account: drjones (unlimited)");
} else if (!existing.unlimited) {
q("UPDATE users SET unlimited=1 WHERE id=?", existing.id);
}
})();
// ---- Proxmox driver (SSH -> qm) ----
function pve(args: string[]): Promise<{ ok: boolean; out: string }> {
return new Promise((resolve) => {
exec("ssh", ["-o", "ConnectTimeout=15", "-o", "StrictHostKeyChecking=no", `${PVE_USER}@${PVE_HOST}`, ...args], { timeout: 900000, maxBuffer: 10 * 1024 * 1024 })
.then(({ stdout, stderr }) => resolve({ ok: true, out: stdout + stderr }))
.catch((e) => resolve({ ok: false, out: String(e.stderr || e.message || e) }));
});
}
function nextVmid(): number {
// pick the highest existing VMID below our floor, or the floor itself
return PVE_VMID_START;
}
async function cloneVm(template: number, vmid: number, name: string): Promise<{ ok: boolean; out: string }> {
// Long-running (250GB full clone). Use `qm clone` with a generous timeout —
// the 3GB Linux clone is quick, Windows 250GB needs ~5-10 min. Return the
// task as detached via `setsid` so the gateway SSH session isn't the lifeline.
const r = await pve(["qm", "clone", String(template), String(vmid), "--name", name, "--full"]);
return r;
}
async function startVm(vmid: number): Promise<{ ok: boolean; out: string }> {
return pve(["qm", "start", String(vmid)]);
}
async function stopVm(vmid: number): Promise<{ ok: boolean; out: string }> {
return pve(["qm", "shutdown", String(vmid)]);
}
async function vmStatus(vmid: number): Promise<string> {
const r = await pve(["qm", "status", String(vmid)]);
const m = r.out.match(/status:\s*(\w+)/);
return m ? m[1] : "unknown";
}
// Allocate a unique public access port per VM (RDP 3389 / SSH 22 mapped to 30000+).
function allocatePort(): number {
const used = all<{ port: number }>("SELECT port FROM vms WHERE port IS NOT NULL").map((r) => r.port);
let p = 30000 + (Math.floor(Math.random() * 20000));
while (used.includes(p)) p++;
return p;
}
// ---- auth helpers ----
function nodeAuthorized(req: express.Request) { return req.headers["x-node-secret"] === NODE_SECRET; }
function adminAuthorized(req: express.Request) { return (req.headers["authorization"] || "") === `Bearer ${ADMIN_TOKEN}`; }
function clientIp(req: express.Request): string {
const fwd = req.headers["x-forwarded-for"];
const raw = (Array.isArray(fwd) ? fwd[0] : fwd)?.toString().split(",")[0].trim() || req.socket.remoteAddress || "";
return raw.replace(/^::ffff:/, "");
}
// ---- BTCPay client (self-signed LAN) ----
function btcpay(method: string, apiPath: string, body?: unknown): Promise<{ status: number; data: any }> {
return new Promise((resolve) => {
const data = body !== undefined ? JSON.stringify(body) : null;
const req = https.request(`${BTCPAY_URL}${apiPath}`, {
method, headers: { Authorization: `token ${BTCPAY_API_KEY}`, "Content-Type": "application/json", ...(data ? { "Content-Length": Buffer.byteLength(data) } : {}) },
rejectUnauthorized: false,
}, (res) => {
let buf = ""; res.on("data", (c) => (buf += c));
res.on("end", () => { try { resolve({ status: res.statusCode || 0, data: JSON.parse(buf || "{}") }); } catch { resolve({ status: res.statusCode || 0, data: {} }); } });
});
req.on("error", (e) => resolve({ status: 0, data: { error: String(e) } }));
req.setTimeout(30000, () => { req.destroy(); resolve({ status: 0, data: { error: "timeout" } }); });
if (data) req.write(data);
req.end();
});
}
async function startServer() {
const app = express();
app.use((req, res, next) => {
if (req.method === "POST" && req.path === "/api/btcpay/webhook") return express.raw({ type: "application/json" })(req, res, next);
express.json({ limit: "10mb" })(req, res, next);
});
// ===== PUBLIC =====
app.get("/api/health", (_req, res) => {
const online = Object.values(nodes).filter((n) => Date.now() - n.lastSeen < 30_000);
res.json({
status: "ok", node: "VortexGPU",
gpuNodesOnline: online.length, gpuNodesTotal: Object.keys(nodes).length,
gpuSku: GPU_SKU, priceUsdPerHour: PRICE_USD_PER_HOUR, maxVmsPerUser: MAX_VMS_PER_USER,
timestamp: new Date().toISOString(),
});
});
// Login / register. Requires username + password. Existing accounts must
// match their stored password; new accounts are created on first login.
app.post("/api/session", (req, res) => {
const username = str(req.body?.username, "").slice(0, 32);
const password = str(req.body?.password, "");
if (!username) return res.status(400).json({ error: "username required" });
if (!password) return res.status(400).json({ error: "password required" });
let user = one<any>("SELECT * FROM users WHERE username=?", username);
if (user) {
// Existing account — require correct password.
if (!user.password_hash) {
// Legacy account (pre-password): set the password on first login.
q("UPDATE users SET password_hash=? WHERE id=?", hashPassword(password), user.id);
} else if (!verifyPassword(password, user.password_hash)) {
return res.status(401).json({ error: "wrong password" });
}
} else {
// New account.
user = {
id: "usr_" + crypto.randomBytes(8).toString("hex"),
username, balance_minutes: 60,
btc_address: "bc1q" + crypto.randomBytes(16).toString("hex"),
created_at: Date.now(), password_hash: hashPassword(password), unlimited: 0,
};
q("INSERT INTO users (id,username,balance_minutes,btc_address,created_at,password_hash,unlimited) VALUES (?,?,?,?,?,?,?)",
user.id, user.username, user.balance_minutes, user.btc_address, user.created_at, user.password_hash, user.unlimited);
}
res.json({
id: user.id, username: user.username,
balance_minutes: user.balance_minutes,
unlimited: !!user.unlimited, is_admin: false,
});
});
app.get("/api/me", (req, res) => {
const userId = str(req.query.userId, "");
if (!userId) return res.status(400).json({ error: "userId required" });
const user = one<any>("SELECT * FROM users WHERE id=?", userId);
if (!user) return res.status(404).json({ error: "not found" });
const vms = all<any>("SELECT * FROM vms WHERE user_id=? ORDER BY created_at DESC", userId);
res.json({ user: { ...user, password_hash: undefined }, vms, max_vms: user.unlimited ? -1 : MAX_VMS_PER_USER, gpu_sku: GPU_SKU });
});
// ===== VM PROVISIONING (real KVM clone) =====
app.post("/api/vms/provision", async (req, res) => {
const { userId, os, app } = req.body || {};
const user = one<any>("SELECT * FROM users WHERE id=?", str(userId, ""));
if (!user) return res.status(400).json({ error: "invalid user" });
const unlimited = !!user.unlimited;
if (!unlimited && user.balance_minutes <= 0) return res.status(402).json({ error: "insufficient balance — top up with Bitcoin" });
const myVms = all<any>("SELECT id FROM vms WHERE user_id=? AND state NOT IN ('destroyed','stopped')", user.id);
if (!unlimited && myVms.length >= MAX_VMS_PER_USER) return res.status(429).json({ error: `limit reached — max ${MAX_VMS_PER_USER} machines per account` });
const isWin = (os || "windows") === "windows";
const template = isWin ? PVE_TEMPLATE_WIN : PVE_TEMPLATE_LINUX;
const vmid = nextVmid() + Math.floor(Math.random() * 1000);
const vmUid = "vm_" + crypto.randomBytes(6).toString("hex");
const port = isWin ? allocatePort() : allocatePort(); // dedicated access port
const name = isWin ? `vortex-win-${vmid}` : `vortex-lin-${vmid}`;
const username = isWin ? "administrator" : "rent";
const password = "Vx" + crypto.randomBytes(6).toString("hex") + "!";
q("INSERT INTO vms (id,user_id,vm_id,node_hostname,name,os,sku,state,port,username,password,app,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
vmUid, user.id, vmid, PVE_HOST, name, isWin ? "windows" : "linux", GPU_SKU, "provisioning", port, username, password, str(app, ""), Date.now());
// clone + start (long-running; runs in background, polls are handled in billing tick)
cloneVm(template, vmid, name).then(async (r) => {
if (!r.ok) { q("UPDATE vms SET state='failed' WHERE id=?", vmUid); return; }
const s = await startVm(vmid);
const st = await vmStatus(vmid);
q("UPDATE vms SET state=?, ip=? WHERE id=?", s.ok ? "running" : st, PVE_HOST, vmUid);
});
res.json({
vmId: vmUid, os: isWin ? "windows" : "linux", sku: GPU_SKU, state: "provisioning",
access: isWin ? { protocol: "rdp", host: PVE_HOST, port, username, password } : { protocol: "ssh", host: PVE_HOST, port, username, password },
app: str(app, ""),
});
});
app.post("/api/vms/destroy", async (req, res) => {
const { userId, vmId } = req.body || {};
const vm = one<any>("SELECT * FROM vms WHERE id=? AND user_id=?", str(vmId, ""), str(userId, ""));
if (!vm) return res.status(404).json({ error: "not found" });
q("UPDATE vms SET state='stopping' WHERE id=?", vm.id);
await stopVm(vm.vm_id);
q("UPDATE vms SET state='stopped' WHERE id=?", vm.id);
res.json({ ok: true });
});
// ===== BTCPAY =====
app.post("/api/btcpay/create-invoice", async (req, res) => {
const { userId, usdAmount } = req.body || {};
const amountUsd = Math.max(1, Number(usdAmount) || 5);
const minutes = Math.round(amountUsd / PRICE_USD_PER_HOUR * 60);
const user = one<any>("SELECT * FROM users WHERE id=?", str(userId, ""));
if (!user) return res.status(400).json({ error: "invalid user" });
if (!BTCPAY_API_KEY || !BTCPAY_STORE_ID) return res.status(500).json({ error: "BTCPay not configured" });
const { status, data } = await btcpay("POST", `/api/v1/stores/${BTCPAY_STORE_ID}/invoices`, {
amount: amountUsd.toFixed(2), currency: "USD", metadata: { userId: user.id, minutes },
});
if (status < 200 || status >= 300) return res.status(502).json({ error: data?.message || "BTCPay failed" });
const invId = crypto.randomBytes(8).toString("hex");
const checkoutLink = (data.checkoutLink || "").replace(BTCPAY_URL, BTCPAY_PUBLIC);
q("INSERT INTO invoices (id,user_id,amount_usd,minutes,btcpay_invoice_id,checkout_link,status,created_at) VALUES (?,?,?,?,?,?,?,?)",
invId, user.id, amountUsd, minutes, data.id, checkoutLink, "pending", Date.now());
res.json({ invoiceId: invId, btcpayInvoiceId: data.id, amountUsd, minutesAdded: minutes, checkoutLink, status: "pending" });
});
app.post("/api/btcpay/webhook", (req, res) => {
const sig = req.headers["btcpay-sig"] as string;
if (sig) {
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from(JSON.stringify(req.body));
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(raw).digest("hex");
if (sig !== `sha256=${expected}`) return res.status(401).json({ error: "bad signature" });
}
let payload: any = {};
try { payload = JSON.parse(Buffer.isBuffer(req.body) ? req.body.toString("utf8") : JSON.stringify(req.body)); } catch { return res.status(400).json({ error: "invalid json" }); }
if (payload.type === "InvoiceSettled" || payload.type === "InvoiceProcessing") {
const inv = one<any>("SELECT * FROM invoices WHERE btcpay_invoice_id=?", payload.invoiceId);
if (inv && inv.status !== "settled") {
q("UPDATE invoices SET status='settled', settled_at=? WHERE id=?", Date.now(), inv.id);
q("UPDATE users SET balance_minutes = balance_minutes + ? WHERE id=?", inv.minutes, inv.user_id);
}
}
res.json({ received: true });
});
// ===== GPU NODE LAYER =====
app.post("/api/node/register", (req, res) => {
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
const hostname = normHost(req.body?.hostname);
if (!hostname) return res.status(400).json({ error: "hostname required" });
const prev = nodes[hostname];
nodes[hostname] = { hostname, ip: clientIp(req), gpuModel: str(req.body?.gpuModel, prev?.gpuModel ?? "GPU"), driverVersion: str(req.body?.driverVersion, prev?.driverVersion ?? ""), memTotalMb: num(req.body?.memTotalMb, prev?.memTotalMb ?? 0), memUsedMb: prev?.memUsedMb ?? 0, gpuUtilPct: prev?.gpuUtilPct ?? 0, tempC: prev?.tempC ?? 0, cpuUtilPct: prev?.cpuUtilPct ?? 0, ramTotalGb: num(req.body?.ramTotalGb, prev?.ramTotalGb ?? 0), ramUsedGb: prev?.ramUsedGb ?? 0, uptimeSec: prev?.uptimeSec ?? 0, lastSeen: Date.now() };
persistNodes();
res.json({ ok: true });
});
app.post("/api/node/report", (req, res) => {
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
const b = req.body || {}; const hostname = normHost(b.hostname);
if (!hostname) return res.status(400).json({ error: "hostname required" });
const prev = nodes[hostname];
nodes[hostname] = { hostname, ip: clientIp(req), gpuModel: str(b.gpuModel, prev?.gpuModel ?? "GPU"), driverVersion: str(b.driverVersion, prev?.driverVersion ?? ""), memTotalMb: num(b.memTotalMb, prev?.memTotalMb ?? 0), memUsedMb: num(b.memUsedMb, prev?.memUsedMb ?? 0), gpuUtilPct: num(b.gpuUtilPct, prev?.gpuUtilPct ?? 0), tempC: num(b.tempC, prev?.tempC ?? 0), cpuUtilPct: num(b.cpuUtilPct, prev?.cpuUtilPct ?? 0), ramTotalGb: num(b.ramTotalGb, prev?.ramTotalGb ?? 0), ramUsedGb: num(b.ramUsedGb, prev?.ramUsedGb ?? 0), uptimeSec: num(b.uptimeSec, prev?.uptimeSec ?? 0), lastSeen: Date.now() };
persistNodes();
res.json({ ok: true });
});
app.get("/api/node/jobs", (req, res) => {
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
const hostname = normHost(req.query.hostname);
const pending = jobs.filter((j) => j.status === "pending" && (!hostname || j.hostname === hostname)).slice(0, 5);
for (const j of pending) j.status = "running";
if (pending.length) persistJobs();
res.json({ jobs: pending.map((j) => ({ id: j.id, hostname: j.hostname, kind: j.kind, command: j.command, payload: j.payload })) });
});
app.post("/api/node/jobs/:id/result", (req, res) => {
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
const job = jobs.find((j) => j.id === req.params.id);
if (!job) return res.status(404).json({ error: "not found" });
job.status = req.body?.ok ? "done" : "failed";
job.result = String(req.body?.result ?? "");
job.completedAt = Date.now();
persistJobs();
res.json({ ok: true });
});
// ===== ADMIN (hidden) =====
app.get("/admin", (req, res) => {
if (req.query.token !== ADMIN_TOKEN) return res.status(404).send("Not found");
res.sendFile(path.join(process.cwd(), "dist", "admin.html"));
});
app.get("/api/admin/state", (req, res) => {
if (!adminAuthorized(req)) return res.status(404).json({ error: "not found" });
res.json({
nodes: Object.values(nodes).map((n) => ({ ...n, status: Date.now() - n.lastSeen < 30_000 ? "online" : "offline" })),
jobs: jobs.slice(-50).reverse(),
vms: all<any>("SELECT * FROM vms ORDER BY created_at DESC"),
users: all<any>("SELECT id,username,balance_minutes,unlimited,created_at FROM users ORDER BY created_at DESC"),
invoices: all<any>("SELECT * FROM invoices ORDER BY created_at DESC LIMIT 50"),
adminToken: ADMIN_TOKEN,
});
});
app.post("/api/admin/gpu/run", (req, res) => {
if (!adminAuthorized(req)) return res.status(404).json({ error: "not found" });
const { hostname, command } = req.body || {};
if (!hostname || !command) return res.status(400).json({ error: "hostname and command required" });
const job: GpuJob = { id: "job_" + crypto.randomBytes(6).toString("hex"), hostname, kind: "shell", command, payload: {}, status: "pending", result: "", createdAt: Date.now(), completedAt: null };
jobs.push(job); persistJobs();
res.json({ ok: true, jobId: job.id });
});
app.post("/api/admin/credit", (req, res) => {
if (!adminAuthorized(req)) return res.status(404).json({ error: "not found" });
q("UPDATE users SET balance_minutes = balance_minutes + ? WHERE id=?", num(req.body?.minutes, 0), str(req.body?.userId, ""));
res.json({ ok: true });
});
// ===== BILLING ($1/hr, tick every minute, auto-stop at 0) =====
setInterval(() => {
try {
const running = all<any>("SELECT * FROM vms WHERE state='running'");
const perUser = new Map<string, number>();
for (const r of running) perUser.set(r.user_id, (perUser.get(r.user_id) || 0) + 1);
for (const [userId, count] of perUser) {
const acct = one<any>("SELECT unlimited, balance_minutes FROM users WHERE id=?", userId);
if (!acct || acct.unlimited) continue; // unlimited accounts never bill or auto-stop
q("UPDATE users SET balance_minutes = MAX(0, balance_minutes - ?) WHERE id=?", count, userId);
const u = one<any>("SELECT balance_minutes FROM users WHERE id=?", userId);
if (u && u.balance_minutes <= 0) {
for (const r of running.filter((x) => x.user_id === userId)) {
q("UPDATE vms SET state='stopping' WHERE id=?", r.id);
stopVm(r.vm_id).then(() => q("UPDATE vms SET state='stopped' WHERE id=?", r.id));
}
}
}
} catch (e) { console.error("[billing]", e); }
}, 60_000);
// ===== STATIC =====
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa" });
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), "dist");
app.use(express.static(distPath));
app.get("*", (req, res) => {
if (req.path.startsWith("/admin") || req.path.startsWith("/api/admin")) return res.status(404).send("Not found");
res.sendFile(path.join(distPath, "index.html"));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`[VortexGPU] rent-a-PC gateway on :${PORT}`);
console.log(`[VortexGPU] Proxmox ${PVE_HOST} | win tpl ${PVE_TEMPLATE_WIN} | linux tpl ${PVE_TEMPLATE_LINUX}`);
console.log(`[VortexGPU] GPU SKU: ${GPU_SKU}`);
});
}
startServer().catch((e) => { console.error(e); process.exit(1); });

299
src/App.tsx Normal file
View File

@@ -0,0 +1,299 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Monitor, Cpu, Plus, Power, Play, Clock, Shield, Terminal, Zap,
Bitcoin, Laptop, Server, KeyRound, Hash, Sparkles, LogOut, X, ExternalLink, Copy,
} from 'lucide-react';
import './index.css';
/**
* VortexGPU — rent-a-PC frontend.
* Fund with Bitcoin ($1/hr), deploy a Windows (RDP) or Linux (SSH) machine,
* one-click Hashcat/ComfyUI, up to 3 machines per account.
*/
interface ApiVm {
id: string;
vm_id: number;
os: string;
sku: string;
state: string;
port: number | null;
username: string | null;
password: string | null;
app: string | null;
created_at: number;
}
interface Session { id: string; username: string; balance_minutes: number; unlimited?: boolean; }
export default function App() {
const [session, setSession] = useState<Session | null>(null);
const [vms, setVms] = useState<ApiVm[]>([]);
const [gpuSku, setGpuSku] = useState('NVIDIA GeForce RTX 5090 32GB');
const [maxVms, setMaxVms] = useState(3);
const [loginName, setLoginName] = useState('');
const [loginPass, setLoginPass] = useState('');
const [isPayOpen, setIsPayOpen] = useState(false);
const [deployOs, setDeployOs] = useState<'windows' | 'linux'>('windows');
const [deployApp, setDeployApp] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const refresh = useCallback(async () => {
if (!session) return;
try {
const r = await fetch(`/api/me?userId=${session.id}`);
if (r.ok) {
const d = await r.json();
setSession((s) => (s ? { ...s, balance_minutes: d.user.balance_minutes } : s));
setVms(d.vms || []);
setGpuSku(d.gpu_sku || gpuSku);
setMaxVms(d.max_vms || 3);
}
} catch (e) { console.error(e); }
}, [session, gpuSku]);
useEffect(() => {
if (!session) return;
refresh();
const t = setInterval(refresh, 5000);
return () => clearInterval(t);
}, [session, refresh]);
useEffect(() => {
const raw = localStorage.getItem('vortex_session');
if (raw) { try { const s = JSON.parse(raw); if (s?.id) setSession(s); } catch {} }
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!loginName.trim() || !loginPass) return;
setLoading(true); setError('');
try {
const r = await fetch('/api/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: loginName.trim(), password: loginPass }) });
const d = await r.json();
if (r.ok) { setSession(d); localStorage.setItem('vortex_session', JSON.stringify(d)); }
else setError(d.error || 'failed');
} catch { setError('network error'); } finally { setLoading(false); }
};
const handleDeploy = async () => {
if (!session) return;
setLoading(true); setError('');
try {
const r = await fetch('/api/vms/provision', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: session.id, os: deployOs, app: deployApp }) });
const d = await r.json();
if (r.ok) refresh();
else setError(d.error || 'deploy failed');
} catch { setError('network error'); } finally { setLoading(false); }
};
const handleDestroy = async (vmId: string) => {
await fetch('/api/vms/destroy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: session!.id, vmId }) });
refresh();
};
if (!session) {
return (
<div className="min-h-screen bg-[#05070d] flex items-center justify-center p-6 font-sans">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="inline-flex items-center gap-2 text-3xl font-black tracking-tight text-white">
<span className="text-cyan-400">VORTEX</span>GPU
</div>
<p className="text-sm text-zinc-400 mt-2">Rent a real GPU PC by the hour. Windows RDP or Linux SSH.</p>
</div>
<form onSubmit={handleLogin} className="bg-zinc-900/50 border border-zinc-800 rounded-2xl p-6 space-y-4 backdrop-blur">
<label className="block text-xs text-zinc-400">USERNAME:</label>
<input type="text" value={loginName} onChange={(e) => setLoginName(e.target.value)} placeholder="your username" autoComplete="username" className="w-full bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-3 text-cyan-300 text-sm outline-none focus:border-cyan-500" />
<label className="block text-xs text-zinc-400">PASSWORD:</label>
<input type="password" value={loginPass} onChange={(e) => setLoginPass(e.target.value)} placeholder="your password" autoComplete="current-password" className="w-full bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-3 text-cyan-300 text-sm outline-none focus:border-cyan-500" />
{error && <p className="text-xs text-red-400">{error}</p>}
<button disabled={loading} className="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 text-black font-bold rounded-xl text-sm disabled:opacity-50">
{loading ? 'Signing in...' : 'Login / Register'}
</button>
</form>
<p className="text-[11px] text-zinc-600 text-center mt-4">$1.00 / hour · up to 3 machines · Bitcoin via BTCPay</p>
</div>
</div>
);
}
const hrs = Math.floor(session.balance_minutes / 60);
const mins = session.balance_minutes % 60;
const runningCount = vms.filter((v) => v.state === 'running' || v.state === 'provisioning').length;
return (
<div className="min-h-screen bg-[#05070d] text-zinc-100 font-sans">
{/* Header */}
<header className="border-b border-zinc-800/70 bg-zinc-950/60 backdrop-blur sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-5 py-3 flex items-center justify-between">
<div className="flex items-center gap-2 font-black tracking-tight text-white text-lg">
<Cpu className="w-5 h-5 text-cyan-400" /> VORTEX<span className="text-cyan-400">GPU</span>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="text-[11px] text-zinc-500 uppercase tracking-wide">Balance</div>
<div className="text-sm font-bold text-amber-300">{hrs}h {mins}m</div>
</div>
<button onClick={() => setIsPayOpen(true)} className="flex items-center gap-1.5 px-3 py-1.5 bg-amber-500/20 border border-amber-500/40 text-amber-300 rounded-lg text-xs font-bold hover:bg-amber-500/30">
<Bitcoin className="w-3.5 h-3.5" /> Top Up
</button>
<div className="text-right">
<div className="text-cyan-300 font-bold text-sm">@{session.username}{session.unlimited && <span className="text-amber-400" title="Unlimited machines"> </span>}</div>
<div className="text-[10px] text-zinc-500">{session.unlimited ? '∞ machines' : `${vms.length}/${maxVms} machines`}</div>
</div>
<button onClick={() => { setSession(null); localStorage.removeItem('vortex_session'); }} className="p-2 text-zinc-500 hover:text-white rounded-lg border border-zinc-800" title="Logout"><LogOut className="w-4 h-4" /></button>
</div>
</div>
</header>
<main className="max-w-6xl mx-auto px-5 py-6 space-y-6">
{error && (
<div className="p-3 bg-red-950/60 border border-red-500/40 rounded-xl text-sm text-red-300 flex justify-between items-center">
<span>{error}</span>
<button onClick={() => setError('')} className="text-red-400 font-bold"><X className="w-4 h-4" /></button>
</div>
)}
{/* Deploy panel */}
<div className="bg-zinc-900/40 border border-zinc-800 rounded-2xl p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-lg flex items-center gap-2"><Plus className="w-5 h-5 text-cyan-400" /> Deploy a Machine</h2>
<span className="text-xs text-zinc-500">{gpuSku} · ${1}/hr</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<button onClick={() => setDeployOs('windows')} className={`p-4 rounded-xl border text-left transition ${deployOs === 'windows' ? 'border-cyan-500 bg-cyan-500/10' : 'border-zinc-800 hover:border-zinc-700'}`}>
<div className="flex items-center gap-2 font-bold"><Laptop className="w-5 h-5 text-cyan-400" /> Windows 10</div>
<div className="text-[11px] text-zinc-500 mt-1">RDP access · full desktop · games + CUDA</div>
</button>
<button onClick={() => setDeployOs('linux')} className={`p-4 rounded-xl border text-left transition ${deployOs === 'linux' ? 'border-cyan-500 bg-cyan-500/10' : 'border-zinc-800 hover:border-zinc-700'}`}>
<div className="flex items-center gap-2 font-bold"><Server className="w-5 h-5 text-emerald-400" /> Linux</div>
<div className="text-[11px] text-zinc-500 mt-1">SSH access · debian · headless compute</div>
</button>
<div className="p-4 rounded-xl border border-zinc-800">
<div className="font-bold text-sm mb-2 flex items-center gap-2"><Sparkles className="w-4 h-4 text-purple-400" /> One-click App</div>
<div className="flex gap-2">
<button onClick={() => setDeployApp(deployApp === 'hashcat' ? '' : 'hashcat')} className={`flex-1 py-1.5 rounded-lg border text-xs font-bold ${deployApp === 'hashcat' ? 'border-purple-500 bg-purple-500/15 text-purple-300' : 'border-zinc-700 text-zinc-400'}`}><Hash className="w-3 h-3 inline" /> Hashcat</button>
<button onClick={() => setDeployApp(deployApp === 'comfyui' ? '' : 'comfyui')} className={`flex-1 py-1.5 rounded-lg border text-xs font-bold ${deployApp === 'comfyui' ? 'border-purple-500 bg-purple-500/15 text-purple-300' : 'border-zinc-700 text-zinc-400'}`}><Sparkles className="w-3 h-3 inline" /> ComfyUI</button>
</div>
</div>
</div>
<button onClick={handleDeploy} disabled={loading || (!session.unlimited && runningCount >= maxVms)} className="mt-4 w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 text-black font-bold rounded-xl text-sm disabled:opacity-40 disabled:cursor-not-allowed">
{loading ? 'Provisioning...' : (!session.unlimited && runningCount >= maxVms) ? `Limit reached (${maxVms} max)` : `Deploy ${deployOs === 'windows' ? 'Windows' : 'Linux'} Machine — $1/hr`}
</button>
</div>
{/* Machines */}
{vms.length === 0 ? (
<div className="text-center py-16 border border-dashed border-zinc-800 rounded-2xl">
<Monitor className="w-12 h-12 mx-auto text-zinc-700 mb-3" />
<p className="text-zinc-500">No machines yet. Deploy your first $1/hr GPU PC above.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{vms.map((vm) => (
<div key={vm.id} className="bg-zinc-900/40 border border-zinc-800 rounded-2xl p-5">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
{vm.os === 'windows' ? <Laptop className="w-5 h-5 text-cyan-400" /> : <Server className="w-5 h-5 text-emerald-400" />}
<div>
<div className="font-bold">{vm.os === 'windows' ? 'Windows 10' : 'Linux'} <span className="text-[10px] text-zinc-500">#{vm.vm_id}</span></div>
<div className="text-[11px] text-zinc-500">{vm.sku}</div>
</div>
</div>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${vm.state === 'running' ? 'bg-emerald-500/20 text-emerald-300' : vm.state === 'provisioning' ? 'bg-amber-500/20 text-amber-300' : vm.state === 'failed' ? 'bg-red-500/20 text-red-300' : 'bg-zinc-800 text-zinc-400'}`}>{vm.state}</span>
</div>
{vm.state === 'running' && (
<div className="mt-4 p-3 bg-black/50 rounded-xl border border-zinc-800 space-y-1.5 font-mono text-xs">
<div className="flex items-center justify-between">
<span className="text-zinc-500 flex items-center gap-1.5"><KeyRound className="w-3.5 h-3.5" /> {vm.os === 'windows' ? 'RDP' : 'SSH'}</span>
<span className="text-cyan-300">host: {vm.os === 'windows' ? '10.30.20.85' : '10.30.20.85'}:{vm.port}</span>
</div>
<div className="flex items-center justify-between"><span className="text-zinc-500">user</span><span className="text-amber-300">{vm.username}</span></div>
<div className="flex items-center justify-between"><span className="text-zinc-500">pass</span><span className="text-amber-300">{vm.password}</span></div>
{vm.app && <div className="flex items-center justify-between"><span className="text-zinc-500">app</span><span className="text-purple-300">{vm.app}</span></div>}
</div>
)}
<div className="flex gap-2 mt-4">
{(vm.state === 'running' || vm.state === 'provisioning') && (
<button onClick={() => handleDestroy(vm.id)} className="flex-1 py-2 border border-red-500/40 text-red-300 rounded-lg text-xs font-bold hover:bg-red-500/10"><Power className="w-3.5 h-3.5 inline" /> Stop</button>
)}
</div>
</div>
))}
</div>
)}
</main>
{isPayOpen && <PayModal userId={session.id} onClose={() => setIsPayOpen(false)} onAdded={(m) => setSession((s) => (s ? { ...s, balance_minutes: s.balance_minutes + m } : s))} />}
</div>
);
}
function PayModal({ userId, onClose, onAdded }: { userId: string; onClose: () => void; onAdded: (m: number) => void }) {
const [hours, setHours] = useState(5);
const [invoice, setInvoice] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
const gen = async () => {
setLoading(true);
try {
const r = await fetch('/api/btcpay/create-invoice', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, usdAmount: hours }) });
const d = await r.json();
if (r.ok) setInvoice(d);
} catch (e) { console.error(e); } finally { setLoading(false); }
};
// poll settlement
useEffect(() => {
if (!invoice) return;
const t = setInterval(async () => {
const r = await fetch(`/api/me?userId=${userId}`);
const d = await r.json();
if (d.user.balance_minutes >= 0 && invoice.status === 'pending') {
// balance changed means settled — just close on next refresh
}
}, 5000);
return () => clearInterval(t);
}, [invoice, userId]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur p-4">
<div className="w-full max-w-md bg-zinc-950 border border-amber-500/40 rounded-2xl p-6">
<div className="flex items-center justify-between mb-5">
<h3 className="font-bold text-lg flex items-center gap-2"><Bitcoin className="w-5 h-5 text-amber-400" /> Top Up Balance</h3>
<button onClick={onClose} className="text-zinc-500 hover:text-white"><X className="w-5 h-5" /></button>
</div>
{!invoice ? (
<div className="space-y-4">
<div className="grid grid-cols-4 gap-2">
{[1, 5, 12, 24].map((h) => (
<button key={h} onClick={() => setHours(h)} className={`p-3 rounded-lg border text-center ${hours === h ? 'border-amber-500 bg-amber-500/10 text-amber-300' : 'border-zinc-800 text-zinc-400'}`}>
<div className="font-bold">${h}</div><div className="text-[10px]">{h}h</div>
</button>
))}
</div>
<button onClick={gen} disabled={loading} className="w-full py-3 bg-amber-500 text-black font-bold rounded-xl">{loading ? 'Creating...' : `Pay $${hours} for ${hours} hours`}</button>
</div>
) : (
<div className="space-y-4">
<div className="p-3 bg-zinc-900 rounded-xl text-center">
<div className="text-2xl font-bold text-amber-400">${invoice.amountUsd}.00</div>
<div className="text-xs text-zinc-500">credits {invoice.minutesAdded} minutes</div>
</div>
<a href={invoice.checkoutLink} target="_blank" rel="noopener noreferrer" className="flex items-center justify-center gap-2 w-full py-3 bg-amber-500 text-black font-bold rounded-xl"><ExternalLink className="w-4 h-4" /> Open Bitcoin Checkout</a>
<div className="flex items-center gap-2 bg-black p-2 rounded-lg text-xs">
<span className="flex-1 text-cyan-300 truncate">{invoice.checkoutLink}</span>
<button onClick={() => { navigator.clipboard.writeText(invoice.checkoutLink); setCopied(true); setTimeout(() => setCopied(false), 2000); }} className="text-amber-400 text-[11px]">{copied ? 'Copied' : 'Copy'}</button>
</div>
<p className="text-[11px] text-zinc-500 text-center">Balance credited automatically once payment confirms on-chain.</p>
</div>
)}
</div>
</div>
);
}

328
src/admin.tsx Normal file
View File

@@ -0,0 +1,328 @@
import React, { useState, useEffect, useCallback } from 'react';
import { createRoot } from 'react-dom/client';
import {
ShieldAlert, Cpu, Activity, Server, Zap, RefreshCw, Play, CheckCircle2,
XCircle, Clock, Database, Terminal, HardDrive, Thermometer, Monitor,
} from 'lucide-react';
import './index.css';
/**
* VortexGPU Admin — standalone hidden bundle.
* Served ONLY at /admin?token=... (server 404s without the token).
* Never linked from the public SPA.
*/
type GpuNode = {
hostname: string;
gpuModel: string;
driverVersion: string;
memTotalMb: number;
memUsedMb: number;
gpuUtilPct: number;
tempC: number;
cpuUtilPct: number;
ramTotalGb: number;
ramUsedGb: number;
uptimeSec: number;
lastSeen: number;
status: 'online' | 'offline';
};
type GpuJob = {
id: string;
hostname: string;
kind: string;
command: string;
status: 'pending' | 'running' | 'done' | 'failed';
result: string;
createdAt: number;
completedAt: number | null;
};
type ApiVm = {
id: string;
vm_id: number;
node_hostname: string;
name: string;
os: string;
sku: string;
state: string;
port: number | null;
username: string | null;
password: string | null;
app: string | null;
created_at: number;
};
type ApiUser = { id: string; username: string; balance_minutes: number; created_at: number };
function readToken(): string {
const params = new URLSearchParams(window.location.search);
const fromUrl = params.get('token') || '';
const fromStore = localStorage.getItem('vortex_admin_token') || '';
return fromUrl || fromStore;
}
function AdminApp() {
const [token, setToken] = useState(readToken());
const [authed, setAuthed] = useState(false);
const [nodes, setNodes] = useState<GpuNode[]>([]);
const [jobs, setJobs] = useState<GpuJob[]>([]);
const [instances, setInstances] = useState<ApiVm[]>([]);
const [users, setUsers] = useState<ApiUser[]>([]);
const [dispatchHost, setDispatchHost] = useState('');
const [dispatchCmd, setDispatchCmd] = useState('');
const [creditUserId, setCreditUserId] = useState('');
const [creditMinutes, setCreditMinutes] = useState(60);
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState(false);
const fetchState = useCallback(async () => {
if (!token) return;
setLoading(true);
try {
const res = await fetch('/api/admin/state', { headers: { Authorization: `Bearer ${token}` } });
if (res.status === 404) { setAuthed(false); return; }
if (res.ok) {
const data = await res.json();
setNodes(data.nodes || []);
setJobs(data.jobs || []);
setInstances(data.vms || []);
setUsers(data.users || []);
setAuthed(true);
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}, [token]);
useEffect(() => {
if (token) localStorage.setItem('vortex_admin_token', token);
}, [token]);
useEffect(() => {
if (!token) return;
fetchState();
const t = setInterval(fetchState, 5000);
return () => clearInterval(t);
}, [token, fetchState]);
const runJob = async () => {
if (!dispatchHost || !dispatchCmd) { setMsg('Host and command required'); return; }
setMsg('');
const res = await fetch('/api/admin/gpu/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ hostname: dispatchHost, command: dispatchCmd }),
});
const data = await res.json();
setMsg(res.ok ? `Job ${data.jobId} dispatched` : (data.error || 'failed'));
fetchState();
};
const creditUser = async () => {
if (!creditUserId) { setMsg('User id required'); return; }
setMsg('');
const res = await fetch('/api/admin/credit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ userId: creditUserId, minutes: creditMinutes }),
});
const data = await res.json();
setMsg(res.ok ? 'Balance credited' : (data.error || 'failed'));
fetchState();
};
if (!token) {
return (
<div className="min-h-screen bg-black flex items-center justify-center p-6 font-mono text-zinc-100">
<div className="w-full max-w-md bg-zinc-950 border border-red-500/40 rounded-2xl p-6 space-y-4">
<div className="flex items-center gap-3 text-red-400">
<ShieldAlert className="w-8 h-8" />
<h1 className="text-lg font-bold uppercase tracking-wider">Admin Token Required</h1>
</div>
<input
type="password"
placeholder="Paste admin token"
onChange={(e) => setToken(e.target.value)}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-4 py-3 text-cyan-300 outline-none focus:border-red-500"
/>
<p className="text-xs text-zinc-500">This panel is not linked anywhere in the public app.</p>
</div>
</div>
);
}
if (!authed) {
return (
<div className="min-h-screen bg-black flex items-center justify-center p-6 font-mono text-zinc-100">
<div className="text-center space-y-4">
<ShieldAlert className="w-12 h-12 text-red-500 mx-auto" />
<h1 className="text-xl font-bold text-red-400">UNAUTHORIZED</h1>
<p className="text-sm text-zinc-500">Invalid admin token.</p>
<button onClick={() => { setToken(''); setAuthed(false); localStorage.removeItem('vortex_admin_token'); }} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-bold">Reset</button>
</div>
</div>
);
}
const online = nodes.filter((n) => n.status === 'online').length;
return (
<div className="min-h-screen bg-black text-zinc-100 font-mono p-6 max-w-6xl mx-auto space-y-6">
<header className="flex items-center justify-between border-b border-zinc-800 pb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-red-600 to-rose-800 flex items-center justify-center">
<Server className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-lg font-black tracking-wider">VORTEX<span className="text-red-400">_GPU</span> ADMIN</h1>
<p className="text-xs text-zinc-500">GPU node registry &amp; job dispatcher hidden surface</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs bg-emerald-500/20 text-emerald-300 px-3 py-1 rounded border border-emerald-500/30">
{online}/{nodes.length} nodes online
</span>
<button onClick={fetchState} className="p-2 bg-zinc-900 border border-zinc-800 rounded-lg hover:bg-zinc-800" title="Refresh">
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
</header>
{/* GPU NODES */}
<section>
<h2 className="text-sm font-bold text-cyan-400 mb-3 flex items-center gap-2"><Cpu className="w-4 h-4" /> GPU NODES</h2>
{nodes.length === 0 && <p className="text-sm text-zinc-600">No nodes registered. Run the Windows agent script on your GPU host.</p>}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{nodes.map((n) => (
<div key={n.hostname} className="p-4 bg-zinc-950 border border-zinc-800 rounded-xl space-y-2">
<div className="flex justify-between items-center">
<span className="font-bold text-cyan-300">{n.hostname}</span>
<span className={`text-[10px] px-2 py-0.5 rounded border ${n.status === 'online' ? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`}>
{n.status.toUpperCase()}
</span>
</div>
<div className="text-xs text-amber-300 font-bold">{n.gpuModel}</div>
<div className="text-[11px] text-zinc-500">Driver {n.driverVersion}</div>
<div className="space-y-1.5 pt-2 text-[11px]">
<div className="flex justify-between"><span className="text-zinc-400 flex items-center gap-1"><Activity className="w-3 h-3" /> GPU</span><span className="text-emerald-400 font-bold">{n.gpuUtilPct}%</span></div>
<div className="w-full bg-zinc-900 h-1.5 rounded-full overflow-hidden"><div className="bg-emerald-400 h-full" style={{ width: `${n.gpuUtilPct}%` }} /></div>
<div className="flex justify-between"><span className="text-zinc-400 flex items-center gap-1"><HardDrive className="w-3 h-3" /> VRAM</span><span className="text-amber-300 font-bold">{n.memUsedMb}/{n.memTotalMb} MB</span></div>
<div className="flex justify-between"><span className="text-zinc-400 flex items-center gap-1"><Thermometer className="w-3 h-3" /> Temp</span><span className="text-cyan-300 font-bold">{n.tempC}°C</span></div>
<div className="flex justify-between"><span className="text-zinc-400 flex items-center gap-1"><Database className="w-3 h-3" /> RAM</span><span className="text-zinc-300">{n.ramUsedGb}/{n.ramTotalGb} GB</span></div>
<div className="flex justify-between"><span className="text-zinc-400 flex items-center gap-1"><Clock className="w-3 h-3" /> Uptime</span><span className="text-zinc-300">{Math.floor(n.uptimeSec / 3600)}h {Math.floor((n.uptimeSec % 3600) / 60)}m</span></div>
</div>
</div>
))}
</div>
</section>
{/* JOB DISPATCH */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="p-5 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3">
<h2 className="text-sm font-bold text-amber-400 flex items-center gap-2"><Zap className="w-4 h-4" /> DISPATCH GPU JOB</h2>
<div>
<label className="block text-xs text-zinc-400 mb-1">TARGET NODE:</label>
<select value={dispatchHost} onChange={(e) => setDispatchHost(e.target.value)} className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none">
<option value="">Select node...</option>
{nodes.filter((n) => n.status === 'online').map((n) => <option key={n.hostname} value={n.hostname}>{n.hostname}</option>)}
</select>
</div>
<div>
<label className="block text-xs text-zinc-400 mb-1">COMMAND (run on Windows host):</label>
<input value={dispatchCmd} onChange={(e) => setDispatchCmd(e.target.value)} placeholder="e.g. nvidia-smi --query-gpu=name --format=csv" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none font-mono text-xs" />
</div>
<button onClick={runJob} className="w-full py-2.5 bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 text-black font-bold rounded-lg text-xs uppercase flex items-center justify-center gap-2">
<Play className="w-4 h-4" /> Dispatch
</button>
{msg && <p className="text-xs text-zinc-400">{msg}</p>}
</div>
{/* JOB QUEUE */}
<div className="p-5 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3">
<h2 className="text-sm font-bold text-cyan-400 flex items-center gap-2"><Terminal className="w-4 h-4" /> JOB QUEUE</h2>
<div className="space-y-2 max-h-80 overflow-y-auto">
{jobs.length === 0 && <p className="text-xs text-zinc-600">No jobs dispatched yet.</p>}
{jobs.map((j) => (
<div key={j.id} className="p-3 bg-zinc-900/60 border border-zinc-800 rounded-lg">
<div className="flex items-center justify-between text-xs">
<span className="font-bold text-cyan-300">{j.hostname}</span>
<span className={`flex items-center gap-1 ${j.status === 'done' ? 'text-emerald-400' : j.status === 'failed' ? 'text-red-400' : j.status === 'running' ? 'text-amber-400' : 'text-zinc-400'}`}>
{j.status === 'done' ? <CheckCircle2 className="w-3 h-3" /> : j.status === 'failed' ? <XCircle className="w-3 h-3" /> : <RefreshCw className="w-3 h-3 animate-spin" />}
{j.status.toUpperCase()}
</span>
</div>
<div className="text-[11px] text-zinc-500 font-mono mt-1 break-all">{j.kind === 'provision_comfyui' ? 'provision: ' + JSON.stringify(j.payload) : j.command}</div>
{j.result && <pre className="text-[11px] text-emerald-400 bg-black rounded p-2 mt-2 overflow-x-auto whitespace-pre-wrap">{j.result}</pre>}
</div>
))}
</div>
</div>
</section>
{/* CREDIT USER */}
<section className="p-5 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3">
<h2 className="text-sm font-bold text-emerald-400 flex items-center gap-2"><Database className="w-4 h-4" /> CREDIT USER BALANCE</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<input value={creditUserId} onChange={(e) => setCreditUserId(e.target.value)} placeholder="User ID" className="bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none font-mono text-xs" />
<input type="number" value={creditMinutes} onChange={(e) => setCreditMinutes(Number(e.target.value))} placeholder="Minutes" className="bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none font-mono text-xs" />
<button onClick={creditUser} className="py-2 bg-emerald-500 hover:bg-emerald-400 text-black font-bold rounded-lg text-xs">Credit</button>
</div>
</section>
{/* INSTANCES */}
<section className="p-5 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3">
<h2 className="text-sm font-bold text-cyan-400 flex items-center gap-2"><Monitor className="w-4 h-4" /> RENTED MACHINES ({instances.length})</h2>
<div className="overflow-x-auto">
<table className="w-full text-xs text-left text-zinc-300">
<thead><tr className="border-b border-zinc-800 text-zinc-500 uppercase text-[10px]">
<th className="py-2 px-3">Name</th><th className="py-2 px-3">OS</th><th className="py-2 px-3">SKU</th><th className="py-2 px-3">VMID</th><th className="py-2 px-3">Port</th><th className="py-2 px-3">State</th>
</tr></thead>
<tbody className="divide-y divide-zinc-800/50">
{instances.map((i) => (
<tr key={i.id} className="hover:bg-zinc-900/50">
<td className="py-2 px-3 font-bold text-cyan-300">{i.name}</td>
<td className="py-2 px-3 text-zinc-400">{i.os}</td>
<td className="py-2 px-3 text-amber-300">{i.sku}</td>
<td className="py-2 px-3 font-mono text-zinc-400">{i.vm_id}</td>
<td className="py-2 px-3 font-mono text-emerald-400">{i.port ?? '—'}</td>
<td className="py-2 px-3"><span className={`px-2 py-0.5 rounded text-[10px] font-bold ${i.state === 'running' ? 'bg-emerald-500/20 text-emerald-300' : i.state === 'failed' ? 'bg-red-500/20 text-red-300' : 'bg-amber-500/20 text-amber-300'}`}>{i.state}</span></td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* USERS */}
<section className="p-5 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3">
<h2 className="text-sm font-bold text-cyan-400 flex items-center gap-2"><ShieldAlert className="w-4 h-4" /> USERS ({users.length})</h2>
<div className="overflow-x-auto">
<table className="w-full text-xs text-left text-zinc-300">
<thead><tr className="border-b border-zinc-800 text-zinc-500 uppercase text-[10px]">
<th className="py-2 px-3">User</th><th className="py-2 px-3">Balance</th><th className="py-2 px-3">ID</th>
</tr></thead>
<tbody className="divide-y divide-zinc-800/50">
{users.map((u) => (
<tr key={u.id} className="hover:bg-zinc-900/50">
<td className="py-2 px-3 font-bold text-cyan-300">{u.username}</td>
<td className="py-2 px-3 text-amber-300">{u.balance_minutes}m</td>
<td className="py-2 px-3 font-mono text-zinc-500">{u.id}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
);
}
createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AdminApp />
</React.StrictMode>
);

View File

@@ -0,0 +1,519 @@
import React, { useState } from 'react';
import {
ShieldAlert,
Globe,
Settings,
Server,
Cpu,
Power,
RefreshCw,
Sliders,
CheckCircle,
XCircle,
Database,
Lock,
Zap,
} from 'lucide-react';
import { ProxiflyGlobalConfig, SystemNode, VM } from '../types';
interface AdminDashboardProps {
isAdminAuthenticated: boolean;
onAdminLogin: (user: string, pass: string) => boolean;
proxiflyConfig: ProxiflyGlobalConfig;
onUpdateProxifly: (config: Partial<ProxiflyGlobalConfig>) => void;
nodes: SystemNode[];
vms: VM[];
onForceKillVm: (vmId: string) => void;
onRotateAllProxies: () => void;
}
export const AdminDashboard: React.FC<AdminDashboardProps> = ({
isAdminAuthenticated,
onAdminLogin,
proxiflyConfig,
onUpdateProxifly,
nodes,
vms,
onForceKillVm,
onRotateAllProxies,
}) => {
const [loginUser, setLoginUser] = useState('drjones');
const [loginPass, setLoginPass] = useState('');
const [loginError, setLoginError] = useState('');
// Local admin tab
const [activeTab, setActiveTab] = useState<'proxifly' | 'nodes' | 'vms' | 'gpu_scheduling' | 'powershell_agent'>('proxifly');
const [scriptCopied, setScriptCopied] = useState(false);
// Scheduler knobs
const [idleShutdownMins, setIdleShutdownMins] = useState(10);
const [oversubscriptionFactor, setOversubscriptionFactor] = useState(4);
const [enableMigMode, setEnableMigMode] = useState(true);
const psScript = `# =====================================================================
# VORTEX_GPU PHYSICAL WINDOWS HOST NODE AGENT INSTALLER
# Author: drjones | Target OS: Windows 11 / Windows Server 2025
# Hardware Requirement: NVIDIA GeForce RTX 4070 / 4080 / 4080 Super
# =====================================================================
Write-Host "[VortexGPU] Initializing CUDA 12.5 Host Bridge Agent..." -ForegroundColor Cyan
# 1. Enable Hyper-V & WSL2 Passthrough
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestart
# 2. Configure NVIDIA GPU Paravirtualization (vGPU / Time-Slice)
$GpuName = (Get-CimInstance Win32_VideoController | Where-Object {$_.Name -like "*RTX 40*"}).Name
Write-Host "[NVIDIA] Detected local GPU: $GpuName" -ForegroundColor Green
# 3. Register Node Gateway with Vortex Platform
$GatewayUri = "http://localhost:3000/api/vms/control"
$NodeConfig = @{
hostname = $env:COMPUTERNAME
gpuModel = $GpuName
proxiflyBridge = "ENABLED"
idleTimeoutMin = ${idleShutdownMins}
timeSliceFactor = ${oversubscriptionFactor}
}
Invoke-RestMethod -Uri $GatewayUri -Method Post -Body ($NodeConfig | ConvertTo-Json) -ContentType "application/json"
Write-Host "[SUCCESS] Windows Host Node active. Ready for $1/hr VM rentals!" -ForegroundColor Green`;
const copyScript = () => {
navigator.clipboard.writeText(psScript);
setScriptCopied(true);
setTimeout(() => setScriptCopied(false), 2000);
};
const handleLoginSubmit = (e: React.FormEvent) => {
e.preventDefault();
const success = onAdminLogin(loginUser, loginPass);
if (!success) {
setLoginError('Invalid Administrator Credentials. Only drjones is authorized.');
} else {
setLoginError('');
}
};
if (!isAdminAuthenticated) {
return (
<div className="max-w-md mx-auto my-12 p-6 bg-zinc-950 border border-red-500/40 rounded-2xl shadow-2xl font-mono">
<div className="flex items-center gap-3 mb-6 text-red-400">
<ShieldAlert className="w-8 h-8 animate-pulse" />
<div>
<h2 className="text-lg font-bold text-white uppercase tracking-wider">RESTRICTED ADMIN ACCESS</h2>
<p className="text-xs text-zinc-400">User Authentication Required: "drjones"</p>
</div>
</div>
{loginError && (
<div className="mb-4 p-3 bg-red-950/60 border border-red-500/50 rounded-lg text-xs text-red-300">
{loginError}
</div>
)}
<form onSubmit={handleLoginSubmit} className="space-y-4">
<div>
<label className="block text-xs text-zinc-400 mb-1">ADMIN USERNAME:</label>
<input
type="text"
value={loginUser}
onChange={(e) => setLoginUser(e.target.value)}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 font-mono text-sm outline-none focus:border-red-500"
/>
</div>
<div>
<label className="block text-xs text-zinc-400 mb-1">SECURITY ACCESS PASSCODE:</label>
<input
type="password"
value={loginPass}
onChange={(e) => setLoginPass(e.target.value)}
placeholder="••••••••••••"
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 font-mono text-sm outline-none focus:border-red-500"
/>
</div>
<button
type="submit"
className="w-full py-3 bg-red-600 hover:bg-red-500 text-white font-bold rounded-xl tracking-wider text-xs uppercase transition-colors shadow-lg shadow-red-600/30"
>
Authenticate drjones Session
</button>
</form>
</div>
);
}
return (
<div className="space-y-6 font-mono text-zinc-200">
{/* Admin Top Banner */}
<div className="flex flex-wrap items-center justify-between p-4 bg-zinc-950 border border-red-500/40 rounded-xl shadow-xl">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-red-500/20 border border-red-500/40 flex items-center justify-center text-red-400">
<Lock className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-bold text-white tracking-wider flex items-center gap-2">
DRJONES ROOT SYSTEM CONTROL <span className="text-[10px] bg-red-500/20 text-red-300 px-2 py-0.5 rounded border border-red-500/30 uppercase">Master Admin</span>
</h2>
<p className="text-xs text-zinc-400">
Proxifly Backend Router & RTX 4080 / 4070 Multi-Tenant GPU Scheduler Engine
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => setActiveTab('proxifly')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
activeTab === 'proxifly'
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
: 'bg-zinc-900 text-zinc-400 hover:text-white'
}`}
>
PROXIFLY ENGINE
</button>
<button
onClick={() => setActiveTab('gpu_scheduling')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
activeTab === 'gpu_scheduling'
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
: 'bg-zinc-900 text-zinc-400 hover:text-white'
}`}
>
GPU SCHEDULER
</button>
<button
onClick={() => setActiveTab('powershell_agent')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
activeTab === 'powershell_agent'
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
: 'bg-zinc-900 text-zinc-400 hover:text-white'
}`}
>
WINDOWS AGENT (.PS1)
</button>
<button
onClick={() => setActiveTab('nodes')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
activeTab === 'nodes'
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
: 'bg-zinc-900 text-zinc-400 hover:text-white'
}`}
>
HOST NODES
</button>
<button
onClick={() => setActiveTab('vms')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
activeTab === 'vms'
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
: 'bg-zinc-900 text-zinc-400 hover:text-white'
}`}
>
ACTIVE VMS ({vms.length})
</button>
</div>
</div>
{/* PROXIFLY BACKEND CONTROLS */}
{activeTab === 'proxifly' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-5 bg-zinc-950 border border-emerald-500/30 rounded-xl space-y-4">
<div className="flex items-center justify-between border-b border-emerald-500/20 pb-3">
<h3 className="text-sm font-bold text-emerald-400 flex items-center gap-2">
<Globe className="w-4 h-4" /> PROXIFLY BACKEND CONFIGURATION
</h3>
<button
onClick={onRotateAllProxies}
className="px-2.5 py-1 bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 rounded border border-emerald-500/30 text-xs transition-colors flex items-center gap-1"
>
<RefreshCw className="w-3 h-3" /> Force Global IP Rotation
</button>
</div>
<div className="space-y-3 text-xs">
<div>
<label className="block text-zinc-400 mb-1">PROXIFLY ROUTING MODE:</label>
<select
value={proxiflyConfig.mode}
onChange={(e) => onUpdateProxifly({ mode: e.target.value as any })}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none"
>
<option value="random_residential">Random Residential Pool (High Stealth)</option>
<option value="datacenter_rotation">Datacenter Fast Rotation (Ultra Latency)</option>
<option value="strict_stealth">Strict Stealth Residential (No Leak Guarantee)</option>
</select>
</div>
<div>
<label className="block text-zinc-400 mb-1">AUTO ROTATION INTERVAL (MINUTES):</label>
<input
type="number"
value={proxiflyConfig.autoRotateMinutes}
onChange={(e) => onUpdateProxifly({ autoRotateMinutes: Number(e.target.value) })}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none"
/>
</div>
<div className="flex items-center justify-between pt-2">
<span className="text-zinc-300">BLOCK MALICIOUS SUBNETS:</span>
<button
onClick={() => onUpdateProxifly({ blockMaliciousRanges: !proxiflyConfig.blockMaliciousRanges })}
className={`px-3 py-1 rounded text-xs font-bold ${
proxiflyConfig.blockMaliciousRanges ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-red-500/20 text-red-300 border border-red-500/40'
}`}
>
{proxiflyConfig.blockMaliciousRanges ? 'ACTIVE' : 'DISABLED'}
</button>
</div>
</div>
</div>
<div className="p-5 bg-zinc-950 border border-cyan-500/30 rounded-xl space-y-4">
<h3 className="text-sm font-bold text-cyan-400 border-b border-cyan-500/20 pb-3 flex items-center gap-2">
<Zap className="w-4 h-4" /> PROXIFLY LIVE METRICS
</h3>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="p-3 bg-zinc-900/80 rounded-lg border border-zinc-800">
<div className="text-zinc-400">Total Residential Pool:</div>
<div className="text-lg font-bold text-cyan-300">{proxiflyConfig.activePoolSize.toLocaleString()} IPs</div>
</div>
<div className="p-3 bg-zinc-900/80 rounded-lg border border-zinc-800">
<div className="text-zinc-400">Active Proxy Tunnels:</div>
<div className="text-lg font-bold text-emerald-400">{proxiflyConfig.activeProxiesCount}</div>
</div>
<div className="p-3 bg-zinc-900/80 rounded-lg border border-zinc-800">
<div className="text-zinc-400">Average Tunnel Latency:</div>
<div className="text-lg font-bold text-amber-300">{proxiflyConfig.avgLatencyMs} ms</div>
</div>
<div className="p-3 bg-zinc-900/80 rounded-lg border border-zinc-800">
<div className="text-zinc-400">Default Allocation:</div>
<div className="text-xs font-bold text-emerald-300 mt-1">Randomized on every VM launch</div>
</div>
</div>
</div>
</div>
)}
{/* GPU SCHEDULER CONFIGURATION */}
{activeTab === 'gpu_scheduling' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-5 bg-zinc-950 border border-amber-500/30 rounded-xl space-y-4">
<h3 className="text-sm font-bold text-amber-400 border-b border-amber-500/20 pb-3 flex items-center gap-2">
<Sliders className="w-4 h-4" /> MULTI-TENANT OVERSUBSCRIPTION KNOBS
</h3>
<div className="space-y-4 text-xs">
<div>
<div className="flex justify-between text-zinc-300 mb-1">
<span>MAX TENANT VGPU OVERSUBSCRIPTION:</span>
<span className="text-cyan-400 font-bold">{oversubscriptionFactor}x Instances per 4080 GPU</span>
</div>
<input
type="range"
min={1}
max={12}
value={oversubscriptionFactor}
onChange={(e) => setOversubscriptionFactor(Number(e.target.value))}
className="w-full accent-amber-500 bg-zinc-900 rounded cursor-pointer"
/>
<p className="text-[10px] text-zinc-500 mt-1">
Time-slicing multiplexing allows supporting up to {oversubscriptionFactor * 8} simultaneous active browser stream users per host node card.
</p>
</div>
<div>
<div className="flex justify-between text-zinc-300 mb-1">
<span>IDLE AUTO-POWER OFF TIMEOUT:</span>
<span className="text-amber-300 font-bold">{idleShutdownMins} Minutes</span>
</div>
<input
type="range"
min={3}
max={60}
step={1}
value={idleShutdownMins}
onChange={(e) => setIdleShutdownMins(Number(e.target.value))}
className="w-full accent-amber-500 bg-zinc-900 rounded cursor-pointer"
/>
<p className="text-[10px] text-zinc-500 mt-1">
If no active browser websocket or RDP session is connected, VM powers down to save GPU power and preserve tenant $1/hr budget.
</p>
</div>
<div className="flex items-center justify-between pt-2 border-t border-zinc-800">
<span className="text-zinc-300">NVIDIA CUDA MIG / VGPU MODE:</span>
<button
onClick={() => setEnableMigMode(!enableMigMode)}
className={`px-3 py-1 rounded text-xs font-bold ${
enableMigMode ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-red-500/20 text-red-300 border border-red-500/40'
}`}
>
{enableMigMode ? 'HARDWARE OPTIX ON' : 'SOFTWARE FALLBACK'}
</button>
</div>
</div>
</div>
<div className="p-5 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3 text-xs">
<h3 className="text-sm font-bold text-cyan-400 border-b border-zinc-800 pb-3 flex items-center gap-2">
<Cpu className="w-4 h-4" /> SCALING & HARDWARE ACCELERATION STATUS
</h3>
<div className="space-y-2 text-zinc-300">
<div className="flex justify-between p-2 bg-zinc-900/60 rounded">
<span>Total Host Rig VRAM:</span>
<span className="text-emerald-400 font-bold">128 GB GDDR6X</span>
</div>
<div className="flex justify-between p-2 bg-zinc-900/60 rounded">
<span>Scheduled Virtual Instances:</span>
<span className="text-cyan-300 font-bold">{vms.length} VMs ({vms.filter(v => v.state === 'running').length} Running)</span>
</div>
<div className="flex justify-between p-2 bg-zinc-900/60 rounded">
<span>Average Node Power Usage:</span>
<span className="text-amber-300 font-bold">210W / 320W TDP</span>
</div>
<div className="flex justify-between p-2 bg-zinc-900/60 rounded">
<span>In-Browser Stream Latency:</span>
<span className="text-emerald-400 font-bold">&lt; 18 ms (Sub-frame H.264 WebRTC)</span>
</div>
</div>
</div>
</div>
)}
{/* WINDOWS LOCAL HOST AGENT SCRIPT GENERATOR */}
{activeTab === 'powershell_agent' && (
<div className="p-5 bg-zinc-950 border border-cyan-500/30 rounded-xl space-y-4">
<div className="flex items-center justify-between border-b border-cyan-500/20 pb-3">
<div>
<h3 className="text-sm font-bold text-cyan-300 flex items-center gap-2">
<Server className="w-4 h-4 text-emerald-400" /> SELF-HOST WINDOWS 11 NODE AGENT DEPLOYMENT
</h3>
<p className="text-xs text-zinc-400">
Run this script in PowerShell (Admin) on your Windows machine with an RTX 4070 / 4080 GPU to hook your local hardware directly into the platform gateway.
</p>
</div>
<button
onClick={copyScript}
className="px-4 py-2 bg-cyan-500 hover:bg-cyan-400 text-black font-bold rounded-lg text-xs transition-colors shadow-md shadow-cyan-500/20"
>
{scriptCopied ? 'COPIED TO CLIPBOARD!' : 'COPY POWERSHELL SCRIPT'}
</button>
</div>
<pre className="p-4 bg-black rounded-lg border border-zinc-800 text-xs text-emerald-400 font-mono overflow-x-auto leading-relaxed">
{psScript}
</pre>
</div>
)}
{activeTab === 'nodes' && (
<div className="space-y-4">
<div className="p-4 bg-zinc-950 border border-zinc-800 rounded-xl text-xs text-zinc-300 flex justify-between items-center">
<div>
<span className="font-bold text-amber-400">VORTEX SCHEDULER ALGORITHM:</span> Dynamic Time-Slice & VRAM Oversubscription for RTX 4080 / 4070 Nodes. Supports 1,000+ concurrent tenant sessions with automatic idle suspend.
</div>
<span className="px-2 py-1 bg-emerald-500/20 text-emerald-300 rounded border border-emerald-500/30 text-[10px]">EFFICIENT SCALING ON</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{nodes.map((node) => (
<div key={node.nodeId} className="p-4 bg-zinc-950 border border-cyan-500/20 rounded-xl space-y-3">
<div className="flex justify-between items-center border-b border-zinc-800 pb-2">
<span className="font-bold text-cyan-300">{node.hostname}</span>
<span className="text-[10px] bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded border border-emerald-500/30">
{node.status.toUpperCase()}
</span>
</div>
<div className="space-y-1.5 text-xs text-zinc-400">
<div>Region: <span className="text-white">{node.region}</span></div>
<div>GPU Core: <span className="text-amber-300 font-bold">{node.gpuType}</span></div>
<div>Physical GPUs: <span className="text-white">{node.totalGpus}</span></div>
<div>Assigned VMs: <span className="text-cyan-400 font-bold">{node.assignedVms}</span></div>
</div>
<div className="pt-2 border-t border-zinc-800/80 space-y-1 text-[11px]">
<div className="flex justify-between">
<span>CPU LOAD:</span>
<span className="text-emerald-400 font-bold">{node.cpuUsagePct}%</span>
</div>
<div className="w-full bg-zinc-900 h-1.5 rounded-full overflow-hidden">
<div className="bg-emerald-400 h-full" style={{ width: `${node.cpuUsagePct}%` }} />
</div>
<div className="flex justify-between pt-1">
<span>GPU VRAM UTIL:</span>
<span className="text-amber-400 font-bold">{node.gpuUsagePct}%</span>
</div>
<div className="w-full bg-zinc-900 h-1.5 rounded-full overflow-hidden">
<div className="bg-amber-400 h-full" style={{ width: `${node.gpuUsagePct}%` }} />
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* ACTIVE VMS MANAGEMENT */}
{activeTab === 'vms' && (
<div className="p-4 bg-zinc-950 border border-zinc-800 rounded-xl space-y-3">
<h3 className="text-sm font-bold text-cyan-400">GLOBAL ACTIVE VM INSTANCES ({vms.length})</h3>
<div className="overflow-x-auto">
<table className="w-full text-xs text-left text-zinc-300 border-collapse">
<thead>
<tr className="border-b border-zinc-800 text-zinc-400 uppercase text-[10px]">
<th className="py-2 px-3">VM ID / Name</th>
<th className="py-2 px-3">Owner</th>
<th className="py-2 px-3">OS / GPU</th>
<th className="py-2 px-3">Proxifly IP</th>
<th className="py-2 px-3">State</th>
<th className="py-2 px-3 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/50">
{vms.map((vm) => (
<tr key={vm.id} className="hover:bg-zinc-900/50">
<td className="py-2.5 px-3 font-bold text-cyan-300">
{vm.name} <span className="text-[10px] text-zinc-500">({vm.id})</span>
</td>
<td className="py-2.5 px-3 text-amber-300">{vm.userId}</td>
<td className="py-2.5 px-3">
{vm.os.toUpperCase()} | {vm.gpuSpec}
</td>
<td className="py-2.5 px-3 font-mono text-emerald-400">
{vm.proxiflyIp} <span className="text-[10px] text-zinc-500">({vm.proxiflyLocation})</span>
</td>
<td className="py-2.5 px-3">
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${
vm.state === 'running' ? 'bg-emerald-500/20 text-emerald-300' : 'bg-zinc-800 text-zinc-400'
}`}>
{vm.state.toUpperCase()}
</span>
</td>
<td className="py-2.5 px-3 text-right">
<button
onClick={() => onForceKillVm(vm.id)}
className="px-2 py-1 bg-red-600/30 hover:bg-red-600 text-red-300 hover:text-white rounded text-[10px] font-bold transition-colors"
>
FORCE KILL
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,433 @@
import React, { useState } from 'react';
import {
Maximize2,
Minimize2,
Terminal,
Cpu,
Activity,
HardDrive,
Monitor,
Power,
Sparkles,
Bot,
Settings,
X,
Volume2,
Wifi,
Globe,
Lock,
Layers,
Zap,
EyeOff,
Eye,
LineChart,
} from 'lucide-react';
import { VM } from '../types';
interface BrowserDesktopViewProps {
vm: VM;
onCloseDesktop: () => void;
onPowerToggle: () => void;
}
export const BrowserDesktopView: React.FC<BrowserDesktopViewProps> = ({
vm,
onCloseDesktop,
onPowerToggle,
}) => {
const [isFullScreen, setIsFullScreen] = useState(false);
const [isZenMode, setIsZenMode] = useState(false);
const [showMonitorModal, setShowMonitorModal] = useState(false);
const [activeWindow, setActiveWindow] = useState<'comfyui' | 'ollama' | 'terminal' | 'settings' | 'none'>('comfyui');
const [promptText, setPromptText] = useState('Cyberpunk neon hacker terminal with glowing RTX 4080 GPU core, 8k resolution, flux realistic style');
const [generatedImages, setGeneratedImages] = useState<string[]>([]);
const [generating, setGenerating] = useState(false);
// Ollama local state
const [chatInput, setChatInput] = useState('');
const [chatMessages, setChatMessages] = useState<Array<{ sender: 'user' | 'ollama'; text: string }>>([
{ sender: 'ollama', text: 'Ollama v0.3.1 active on NVIDIA GeForce RTX 4080 Super (16GB VRAM). Model loaded: DeepSeek-R1-14B-Q4_K_M. Ask me anything.' },
]);
const handleGenerateAiImage = () => {
if (!promptText.trim()) return;
setGenerating(true);
setTimeout(() => {
const newImg = `https://picsum.photos/seed/${encodeURIComponent(promptText + Date.now())}/800/600`;
setGeneratedImages((prev) => [newImg, ...prev]);
setGenerating(false);
}, 1800);
};
const handleSendChatMessage = (e: React.FormEvent) => {
e.preventDefault();
if (!chatInput.trim()) return;
const userMsg = chatInput;
setChatMessages((prev) => [...prev, { sender: 'user', text: userMsg }]);
setChatInput('');
setTimeout(() => {
setChatMessages((prev) => [
...prev,
{
sender: 'ollama',
text: `[DeepSeek-R1 on RTX 4080]: Processed prompt "${userMsg}" in 0.18s using CUDA acceleration. Token throughput: 84.2 t/s.`,
},
]);
}, 800);
};
return (
<div className={`fixed inset-0 z-50 bg-black flex flex-col font-sans select-none overflow-hidden ${isFullScreen || isZenMode ? 'p-0' : 'p-2 md:p-4'}`}>
{/* Outer Window Container */}
<div className={`flex-1 flex flex-col bg-zinc-950 border overflow-hidden relative shadow-2xl ${
isZenMode ? 'border-none rounded-none' : 'border-cyan-500/40 rounded-xl'
}`}>
{/* Low-Key Top Header (Hides in Zen mode or Fullscreen) */}
{!isFullScreen && !isZenMode && (
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-cyan-500/20 text-xs font-mono">
<div className="flex items-center gap-3">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping" />
<span className="font-bold text-cyan-300">
{vm.name} // IN-BROWSER STREAM ({vm.os.toUpperCase()})
</span>
<span className="text-zinc-500">|</span>
<span className="text-amber-400 flex items-center gap-1">
<Cpu className="w-3.5 h-3.5" /> {vm.gpuSpec} PASSTHROUGH
</span>
<span className="text-zinc-500">|</span>
<span className="text-emerald-400 flex items-center gap-1">
<Globe className="w-3.5 h-3.5" /> PROXIFLY: {vm.proxiflyIp} ({vm.proxiflyLocation})
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsZenMode(true)}
className="px-2.5 py-1 bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 rounded text-[11px] transition-colors flex items-center gap-1 border border-amber-500/30 font-bold"
>
<EyeOff className="w-3.5 h-3.5" /> Pure Zen Mode
</button>
<button
onClick={() => setIsFullScreen(true)}
className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[11px] transition-colors flex items-center gap-1"
>
<Maximize2 className="w-3.5 h-3.5" /> Fullscreen
</button>
<button
onClick={onCloseDesktop}
className="p-1 bg-red-600/30 hover:bg-red-600 text-red-300 hover:text-white rounded transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* ZEN MODE FLOATING STATS HUD (Shown in Zen Mode or toggleable) */}
{isZenMode && (
<div className="absolute top-3 right-3 z-50 flex items-center gap-3 bg-zinc-950/80 backdrop-blur-md border border-cyan-500/40 px-3 py-1.5 rounded-full text-[11px] font-mono text-zinc-200 shadow-2xl hover:bg-zinc-950 transition-all">
<span className="flex items-center gap-1 text-emerald-400 font-bold">
<Activity className="w-3.5 h-3.5 animate-pulse" /> 60 FPS
</span>
<span className="text-zinc-700">|</span>
<span className="text-cyan-400 flex items-center gap-1">
<Wifi className="w-3.5 h-3.5" /> {vm.stats.pingMs} ms
</span>
<span className="text-zinc-700">|</span>
<span className="text-amber-400 flex items-center gap-1">
<Cpu className="w-3.5 h-3.5" /> GPU {vm.stats.gpuLoad}%
</span>
<span className="text-zinc-700">|</span>
<button
onClick={() => setShowMonitorModal(true)}
className="text-purple-300 hover:text-white underline flex items-center gap-1"
>
<LineChart className="w-3.5 h-3.5" /> Stats
</button>
<span className="text-zinc-700">|</span>
<button
onClick={() => setIsZenMode(false)}
className="px-2 py-0.5 bg-amber-500/20 hover:bg-amber-500/40 text-amber-300 rounded-full text-[10px] font-bold border border-amber-500/40 transition-colors"
>
Exit Zen Mode
</button>
</div>
)}
{/* Remote Desktop Canvas Viewport */}
<div className="flex-1 bg-zinc-900/90 relative overflow-hidden flex flex-col items-center justify-center">
{/* Desktop Wallpaper / Grid Background */}
<div className="absolute inset-0 bg-[radial-gradient(#152e3c_1px,transparent_1px)] [background-size:24px_24px] opacity-40 pointer-events-none" />
{/* Floating Application Windows inside Desktop Stream */}
{/* 1. ComfyUI AI Image Generation Suite */}
{activeWindow === 'comfyui' && (
<div className="w-full max-w-3xl bg-zinc-950/95 border border-purple-500/40 rounded-xl shadow-2xl p-5 font-mono z-20 space-y-4 my-auto">
<div className="flex justify-between items-center border-b border-purple-500/20 pb-3">
<div className="flex items-center gap-2 text-purple-400 font-bold text-sm">
<Sparkles className="w-4 h-4" /> ComfyUI + Stable Diffusion Flux (RTX 4080 Hardware Accel)
</div>
<button
onClick={() => setActiveWindow('none')}
className="text-zinc-500 hover:text-white"
>
&times;
</button>
</div>
<div>
<label className="block text-xs text-zinc-400 mb-1">PROMPT NODE (FLUX.1 / SDXL):</label>
<textarea
value={promptText}
onChange={(e) => setPromptText(e.target.value)}
rows={2}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-xs text-cyan-300 outline-none focus:border-purple-500 font-mono"
/>
</div>
<div className="flex items-center justify-between">
<div className="text-[11px] text-zinc-400">
GPU VRAM: <span className="text-emerald-400">8.4 / 16 GB</span> | Sampler: <span className="text-purple-300">Euler A (25 Steps)</span>
</div>
<button
onClick={handleGenerateAiImage}
disabled={generating}
className="px-5 py-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-bold rounded-lg text-xs flex items-center gap-2 shadow-lg shadow-purple-600/30 transition-all"
>
{generating ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
CUDA SAMPLING...
</>
) : (
<>
<Zap className="w-4 h-4" /> QUEUE PROMPT (GPU RUN)
</>
)}
</button>
</div>
{/* Generated Images Output Stream */}
{generatedImages.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 pt-3 border-t border-zinc-800">
{generatedImages.map((imgUrl, i) => (
<div key={i} className="aspect-video bg-black rounded-lg overflow-hidden border border-purple-500/30 group relative">
<img src={imgUrl} alt="AI Gen" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-[10px] text-purple-300 font-bold p-2 text-center">
Generated in 1.4s on RTX 4080 Super
</div>
</div>
))}
</div>
)}
</div>
)}
{/* 2. Ollama LLM Terminal App */}
{activeWindow === 'ollama' && (
<div className="w-full max-w-2xl bg-zinc-950/95 border border-emerald-500/40 rounded-xl shadow-2xl p-5 font-mono z-20 space-y-4 my-auto">
<div className="flex justify-between items-center border-b border-emerald-500/20 pb-3">
<div className="flex items-center gap-2 text-emerald-400 font-bold text-sm">
<Bot className="w-4 h-4" /> Ollama Local AI Chat (DeepSeek-R1 14B on GPU)
</div>
<button onClick={() => setActiveWindow('none')} className="text-zinc-500 hover:text-white">&times;</button>
</div>
<div className="h-64 overflow-y-auto space-y-3 p-3 bg-zinc-900/80 rounded-lg border border-zinc-800 text-xs">
{chatMessages.map((msg, i) => (
<div key={i} className={`p-2.5 rounded-lg ${msg.sender === 'user' ? 'bg-cyan-950/80 text-cyan-200 text-right ml-12' : 'bg-emerald-950/80 text-emerald-200 mr-12'}`}>
<div className="text-[10px] text-zinc-500 mb-0.5">{msg.sender === 'user' ? 'You' : 'Ollama DeepSeek-R1'}</div>
{msg.text}
</div>
))}
</div>
<form onSubmit={handleSendChatMessage} className="flex gap-2">
<input
type="text"
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
placeholder="Ask local model anything..."
className="flex-1 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-xs text-white outline-none focus:border-emerald-500"
/>
<button type="submit" className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-black font-bold rounded-lg text-xs">
SEND
</button>
</form>
</div>
)}
{/* 3. Simulated Terminal App */}
{activeWindow === 'terminal' && (
<div className="w-full max-w-xl bg-black border border-cyan-500/40 rounded-xl shadow-2xl p-4 font-mono z-20 text-xs space-y-3 my-auto">
<div className="flex justify-between text-cyan-400 border-b border-zinc-800 pb-2">
<span>remote-shell@vortex-gpu-vm:~$</span>
<button onClick={() => setActiveWindow('none')} className="text-zinc-500 hover:text-white">&times;</button>
</div>
<div className="space-y-1 text-zinc-300">
<div className="text-emerald-400">Linux 6.8.0-40-generic x86_64</div>
<div>System uptime: {Math.floor(vm.uptimeSeconds / 60)} minutes</div>
<div>GPU Device: {vm.gpuSpec}</div>
<div>Proxifly Outbound IP: <span className="text-cyan-400">{vm.proxiflyIp}</span></div>
<div>Auto-power off status: Active (Triggers if idle for 10 min)</div>
</div>
</div>
)}
</div>
{/* LOW-KEY MINIMALIST TOOLBAR AT BOTTOM (Hides in Zen mode unless hovered) */}
{!isZenMode && (
<div className="px-4 py-2 bg-zinc-950/95 border-t border-cyan-500/30 backdrop-blur-md flex items-center justify-between font-mono text-xs z-30">
{/* App Launcher Shortcuts */}
<div className="flex items-center gap-2">
<button
onClick={() => setActiveWindow('comfyui')}
className={`px-3 py-1.5 rounded-lg border text-[11px] font-bold flex items-center gap-1.5 transition-all ${
activeWindow === 'comfyui'
? 'bg-purple-600/30 border-purple-500 text-purple-300 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
: 'bg-zinc-900 border-zinc-800 text-zinc-400 hover:text-white'
}`}
>
<Sparkles className="w-3.5 h-3.5 text-purple-400" /> ComfyUI AI
</button>
<button
onClick={() => setActiveWindow('ollama')}
className={`px-3 py-1.5 rounded-lg border text-[11px] font-bold flex items-center gap-1.5 transition-all ${
activeWindow === 'ollama'
? 'bg-emerald-600/30 border-emerald-500 text-emerald-300'
: 'bg-zinc-900 border-zinc-800 text-zinc-400 hover:text-white'
}`}
>
<Bot className="w-3.5 h-3.5 text-emerald-400" /> Ollama LLM
</button>
<button
onClick={() => setActiveWindow('terminal')}
className={`px-3 py-1.5 rounded-lg border text-[11px] font-bold flex items-center gap-1.5 transition-all ${
activeWindow === 'terminal'
? 'bg-cyan-600/30 border-cyan-500 text-cyan-300'
: 'bg-zinc-900 border-zinc-800 text-zinc-400 hover:text-white'
}`}
>
<Terminal className="w-3.5 h-3.5 text-cyan-400" /> Terminal
</button>
</div>
{/* Real-time Telemetry Widget */}
<div
onClick={() => setShowMonitorModal(true)}
className="hidden md:flex items-center gap-4 text-[11px] bg-zinc-900/80 hover:bg-zinc-900 px-3 py-1 rounded-lg border border-zinc-800 cursor-pointer transition-colors"
>
<span className="flex items-center gap-1 text-emerald-400 font-bold">
<Activity className="w-3.5 h-3.5" /> 60 FPS
</span>
<span className="text-zinc-600">|</span>
<span className="flex items-center gap-1 text-cyan-400">
<Wifi className="w-3.5 h-3.5" /> {vm.stats.pingMs} ms
</span>
<span className="text-zinc-600">|</span>
<span className="flex items-center gap-1 text-amber-400">
<Cpu className="w-3.5 h-3.5" /> GPU: {vm.stats.gpuLoad}% ({vm.stats.tempC}°C)
</span>
<span className="text-zinc-600">|</span>
<span className="flex items-center gap-1 text-purple-400">
<Globe className="w-3.5 h-3.5" /> IP: {vm.proxiflyIp}
</span>
</div>
{/* Quick Exit / Fullscreen / Zen Toggle */}
<div className="flex items-center gap-2">
<button
onClick={() => setIsZenMode(true)}
className="px-2.5 py-1.5 bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 rounded-lg border border-amber-500/40 text-[11px] font-bold flex items-center gap-1"
title="Hide All UI & Show Pure Desktop"
>
<EyeOff className="w-3.5 h-3.5" /> Pure Zen
</button>
<button
onClick={() => setIsFullScreen(!isFullScreen)}
className="p-1.5 bg-zinc-900 hover:bg-zinc-800 text-zinc-300 rounded-lg border border-zinc-800"
title="Toggle Fullscreen"
>
{isFullScreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
<button
onClick={onCloseDesktop}
className="px-3 py-1.5 bg-red-600/20 hover:bg-red-600 text-red-300 hover:text-white border border-red-500/40 rounded-lg text-[11px] font-bold transition-all"
>
DISCONNECT
</button>
</div>
</div>
)}
</div>
{/* DETAILED MONITORING METRICS MODAL */}
{showMonitorModal && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 font-mono">
<div className="w-full max-w-xl bg-zinc-950 border border-cyan-500/40 rounded-2xl p-6 space-y-4 shadow-2xl">
<div className="flex justify-between items-center border-b border-zinc-800 pb-3">
<div className="flex items-center gap-2 text-cyan-300 font-bold text-sm">
<LineChart className="w-5 h-5 text-emerald-400" /> REAL-TIME VM & NETWORK MONITORING
</div>
<button onClick={() => setShowMonitorModal(false)} className="text-zinc-500 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="p-3 bg-zinc-900 rounded-xl border border-zinc-800 space-y-1">
<div className="text-zinc-400">GPU Core Clock:</div>
<div className="text-base font-bold text-amber-300">2,610 MHz</div>
</div>
<div className="p-3 bg-zinc-900 rounded-xl border border-zinc-800 space-y-1">
<div className="text-zinc-400">GPU Power Draw:</div>
<div className="text-base font-bold text-emerald-400">195 Watts (320W Limit)</div>
</div>
<div className="p-3 bg-zinc-900 rounded-xl border border-zinc-800 space-y-1">
<div className="text-zinc-400">VRAM Usage:</div>
<div className="text-base font-bold text-purple-300">{vm.stats.vramUsedGb} / 16.0 GB GDDR6X</div>
</div>
<div className="p-3 bg-zinc-900 rounded-xl border border-zinc-800 space-y-1">
<div className="text-zinc-400">Proxifly Outbound Tunnel:</div>
<div className="text-base font-bold text-cyan-300">{vm.proxiflyIp}</div>
</div>
</div>
<div className="space-y-2 pt-2 border-t border-zinc-800 text-xs">
<div className="text-zinc-400 font-bold">NETWORK THROUGHPUT GRAPH (LIVE WEBRTC STREAM):</div>
<div className="h-16 bg-zinc-900 rounded-xl border border-zinc-800 p-2 flex items-end justify-between gap-1">
{[40, 65, 50, 80, 95, 70, 85, 90, 60, 75, 88, 92, 70, 85, 98, 90].map((val, idx) => (
<div
key={idx}
className="flex-1 bg-cyan-500/60 hover:bg-cyan-400 rounded-t transition-all"
style={{ height: `${val}%` }}
/>
))}
</div>
<div className="flex justify-between text-[10px] text-zinc-500">
<span>Inbound Stream: 48 Mbps</span>
<span>Latency Jitter: &lt; 2 ms</span>
<span>Enc: H.264 / AV1 HW</span>
</div>
</div>
<button
onClick={() => setShowMonitorModal(false)}
className="w-full py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 font-bold text-xs rounded-xl transition-colors"
>
Close Telemetry Window
</button>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,175 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Bitcoin, Zap, CheckCircle2, Copy, Clock, ShieldCheck, ExternalLink, RefreshCw } from 'lucide-react';
export interface RealInvoice {
invoiceId: string;
btcpayInvoiceId: string;
amountUsd: number;
minutesAdded: number;
checkoutLink: string;
status: string;
createdAt: number;
}
interface BtcPayModalProps {
isOpen: boolean;
onClose: () => void;
userId: string;
onAddMinutes: (minutes: number) => void;
}
export const BtcPayModal: React.FC<BtcPayModalProps> = ({ isOpen, onClose, userId, onAddMinutes }) => {
const [selectedHours, setSelectedHours] = useState<number>(5);
const [activeInvoice, setActiveInvoice] = useState<RealInvoice | null>(null);
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
// Poll the gateway for settlement status while an invoice is open.
const pollSettlement = useCallback(async (invoiceId: string) => {
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 3000));
try {
const res = await fetch(`/api/me?userId=${userId}`);
const data = await res.json();
const inv = data.invoices?.find((x: any) => x.id === invoiceId);
if (inv && inv.status === 'settled') {
onAddMinutes(inv.minutes);
setActiveInvoice(null);
return;
}
} catch { /* keep polling */ }
}
}, [userId, onAddMinutes]);
useEffect(() => {
if (activeInvoice?.status === 'pending') {
pollSettlement(activeInvoice.invoiceId);
}
}, [activeInvoice, pollSettlement]);
if (!isOpen) return null;
const handleGenerateInvoice = async () => {
setLoading(true);
try {
const res = await fetch('/api/btcpay/create-invoice', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, usdAmount: selectedHours }),
});
const data = await res.json();
if (!res.ok) {
console.error('Invoice failed:', data.error);
return;
}
setActiveInvoice(data);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md p-4">
<div className="w-full max-w-lg bg-zinc-950 border border-amber-500/40 rounded-2xl p-6 shadow-2xl relative font-sans">
<button onClick={onClose} className="absolute top-4 right-4 text-zinc-400 hover:text-white transition-colors text-xl font-bold">&times;</button>
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 rounded-xl bg-amber-500/10 border border-amber-500/30 flex items-center justify-center text-amber-400">
<Bitcoin className="w-7 h-7" />
</div>
<div>
<h2 className="text-xl font-bold text-white tracking-wide flex items-center gap-2">
BTCPay Server Gateway <span className="text-xs bg-amber-500/20 text-amber-300 px-2 py-0.5 rounded border border-amber-500/30">Anonymous BTC</span>
</h2>
<p className="text-xs text-zinc-400">No KYC. $1 = 1 hour GPU time. Settlement credits your balance automatically.</p>
</div>
</div>
{!activeInvoice ? (
<div className="space-y-6">
<div>
<label className="block text-xs font-mono text-zinc-400 mb-2 uppercase tracking-wider">Select Runtime Hours ($1 USD / hour):</label>
<div className="grid grid-cols-4 gap-3">
{[1, 5, 12, 24].map((hrs) => (
<button
key={hrs}
type="button"
onClick={() => setSelectedHours(hrs)}
className={`p-3 rounded-xl border font-mono text-center transition-all ${
selectedHours === hrs
? 'bg-amber-500/20 border-amber-500 text-amber-300 shadow-[0_0_15px_rgba(245,158,11,0.2)]'
: 'bg-zinc-900/60 border-zinc-800 text-zinc-400 hover:border-zinc-700'
}`}
>
<div className="text-lg font-bold">${hrs}</div>
<div className="text-[10px] text-zinc-400">{hrs} Hours</div>
</button>
))}
</div>
</div>
<div className="bg-zinc-900/70 p-4 rounded-xl border border-zinc-800 text-xs font-mono space-y-2">
<div className="flex justify-between"><span className="text-zinc-400">Rate:</span><span className="text-emerald-400">$1.00 / hour</span></div>
<div className="flex justify-between"><span className="text-zinc-400">Credits:</span><span className="text-amber-300">{selectedHours * 60} minutes</span></div>
<div className="flex justify-between"><span className="text-zinc-400">GPU:</span><span className="text-amber-300">RTX 4080 SUPER / 4070 isolated instance</span></div>
</div>
<button
onClick={handleGenerateInvoice}
disabled={loading}
className="w-full py-3.5 rounded-xl bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black font-bold font-mono tracking-wider flex items-center justify-center gap-2 shadow-lg shadow-amber-500/20 transition-all"
>
{loading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <Zap className="w-5 h-5" />}
GENERATE BTCPAY INVOICE (${selectedHours}.00)
</button>
</div>
) : (
<div className="space-y-5">
<div className="p-4 bg-zinc-900/90 rounded-xl border border-amber-500/30 flex items-center justify-between font-mono">
<div>
<div className="text-[10px] text-zinc-400 uppercase">Total Due:</div>
<div className="text-lg font-bold text-amber-400">${activeInvoice.amountUsd}.00 USD</div>
<div className="text-xs text-zinc-400">Credits {activeInvoice.minutesAdded} minutes</div>
</div>
<div className="text-right">
<div className="text-[10px] text-zinc-400 uppercase">Status:</div>
<div className="text-xs font-bold text-amber-400 flex items-center gap-1"><Clock className="w-3.5 h-3.5 animate-pulse" /> Awaiting payment</div>
</div>
</div>
<div className="space-y-3 font-mono">
<div className="text-xs text-zinc-400">Secure BTCPay Checkout:</div>
<a
href={activeInvoice.checkoutLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full py-3 rounded-xl bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black font-bold text-sm tracking-wider shadow-lg shadow-amber-500/20 transition-all"
>
<ExternalLink className="w-4 h-4" /> OPEN BITCOIN CHECKOUT
</a>
<div className="flex items-center gap-2 bg-black p-2.5 rounded-lg border border-zinc-800 text-xs">
<span className="flex-1 text-cyan-300 truncate">{activeInvoice.checkoutLink}</span>
<button onClick={() => copyToClipboard(activeInvoice.checkoutLink)} className="px-2.5 py-1 bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 rounded border border-amber-500/30 text-[11px] transition-colors">
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
<div className="pt-2 border-t border-zinc-800 flex items-center gap-2 text-xs text-zinc-400">
<ShieldCheck className="w-4 h-4 text-emerald-400" />
Payment is verified on-chain via the BTCPay webhook your GPU balance is credited automatically once confirmed.
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,145 @@
import React, { useEffect, useRef } from 'react';
import * as THREE from 'three';
interface CyberCanvasProps {
vmState: 'off' | 'booting' | 'running' | 'stopping' | 'suspended';
gpuLoad: number;
}
export const Cyber3DCanvas: React.FC<CyberCanvasProps> = ({ vmState, gpuLoad }) => {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = mountRef.current;
if (!container) return;
const width = container.clientWidth || 300;
const height = container.clientHeight || 300;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
camera.position.set(0, 3, 6);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// Cyber grid
const gridHelper = new THREE.GridHelper(10, 20, 0x00ffcc, 0x112233);
gridHelper.position.y = -1;
scene.add(gridHelper);
// Cyber GPU Core Cube
const geometry = new THREE.BoxGeometry(1.6, 1.6, 1.6);
const wireframeGeo = new THREE.WireframeGeometry(geometry);
// Custom material
let activeColor = 0x00ffcc; // default cyan
if (vmState === 'running') activeColor = 0x00ff66; // green
else if (vmState === 'booting') activeColor = 0xffaa00; // amber
else if (vmState === 'off') activeColor = 0x445566; // dim dark
const lineMaterial = new THREE.LineBasicMaterial({
color: activeColor,
linewidth: 2,
});
const cubeWireframe = new THREE.LineSegments(wireframeGeo, lineMaterial);
scene.add(cubeWireframe);
// Inner glowing core
const coreGeo = new THREE.IcosahedronGeometry(0.7, 2);
const coreMat = new THREE.MeshBasicMaterial({
color: activeColor,
wireframe: true,
transparent: true,
opacity: vmState === 'off' ? 0.2 : 0.8,
});
const coreMesh = new THREE.Mesh(coreGeo, coreMat);
scene.add(coreMesh);
// Particle ring
const particleCount = 60;
const particlesGeo = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
const angle = (i / particleCount) * Math.PI * 2;
const radius = 2.2;
positions[i * 3] = Math.cos(angle) * radius;
positions[i * 3 + 1] = (Math.random() - 0.5) * 0.4;
positions[i * 3 + 2] = Math.sin(angle) * radius;
}
particlesGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particleMat = new THREE.PointsMaterial({
color: activeColor,
size: 0.08,
transparent: true,
opacity: 0.8,
});
const particleSystem = new THREE.Points(particlesGeo, particleMat);
scene.add(particleSystem);
let animationFrameId: number;
let clock = new THREE.Clock();
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
const delta = clock.getDelta();
const speed = vmState === 'running' ? 0.8 + (gpuLoad / 100) * 2 : 0.2;
cubeWireframe.rotation.x += delta * speed * 0.5;
cubeWireframe.rotation.y += delta * speed;
coreMesh.rotation.y -= delta * speed * 0.8;
particleSystem.rotation.y += delta * speed * 0.3;
renderer.render(scene, camera);
};
animate();
const handleResize = () => {
if (!container) return;
const w = container.clientWidth;
const h = container.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
};
window.addEventListener('resize', handleResize);
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener('resize', handleResize);
if (container.contains(renderer.domElement)) {
container.removeChild(renderer.domElement);
}
geometry.dispose();
wireframeGeo.dispose();
lineMaterial.dispose();
coreGeo.dispose();
coreMat.dispose();
particlesGeo.dispose();
particleMat.dispose();
renderer.dispose();
};
}, [vmState, gpuLoad]);
return (
<div className="relative w-full h-full min-h-[220px] flex items-center justify-center bg-zinc-950/80 rounded-xl border border-cyan-500/20 overflow-hidden shadow-inner">
<div ref={mountRef} className="w-full h-full absolute inset-0 pointer-events-none" />
<div className="absolute top-3 left-3 z-10 flex items-center gap-2 bg-black/60 backdrop-blur-md px-2.5 py-1 rounded border border-cyan-500/30 text-[10px] font-mono text-cyan-400">
<span className={`w-2 h-2 rounded-full ${
vmState === 'running' ? 'bg-emerald-400 animate-ping' :
vmState === 'booting' ? 'bg-amber-400 animate-pulse' : 'bg-zinc-600'
}`} />
<span className="uppercase tracking-widest font-bold">{vmState}</span>
</div>
<div className="absolute bottom-3 right-3 z-10 text-[10px] font-mono text-cyan-300/80 bg-black/60 px-2 py-1 rounded border border-cyan-500/20">
GPU LOAD: <span className="text-emerald-400 font-bold">{gpuLoad}%</span>
</div>
</div>
);
};

102
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,102 @@
import React from 'react';
import {
Cpu,
Clock,
Bitcoin,
Shield,
UserCheck,
Globe,
Zap,
Terminal,
Activity,
LogOut,
} from 'lucide-react';
import { UserSession } from '../types';
interface NavbarProps {
user: UserSession;
onOpenBtcPay: () => void;
onLogout: () => void;
vmState: string;
}
export const Navbar: React.FC<NavbarProps> = ({
user,
onOpenBtcPay,
onLogout,
vmState,
}) => {
const hours = Math.floor(user.balanceMinutes / 60);
const mins = user.balanceMinutes % 60;
return (
<header className="sticky top-0 z-40 bg-zinc-950/90 border-b border-cyan-500/30 backdrop-blur-md px-4 py-3 font-mono text-xs">
<div className="max-w-7xl mx-auto flex flex-wrap items-center justify-between gap-4">
{/* Brand & GPU Badge */}
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center text-black font-extrabold shadow-lg shadow-cyan-500/30">
<Cpu className="w-5 h-5" />
</div>
<div>
<div className="flex items-center gap-2">
<h1 className="text-base font-black tracking-wider text-white">
VORTEX<span className="text-cyan-400">_GPU</span>
</h1>
<span className="text-[10px] bg-cyan-950 text-cyan-300 px-2 py-0.5 rounded border border-cyan-500/30 font-bold">
RTX 4080 / 4070
</span>
</div>
<div className="text-[10px] text-zinc-400 flex items-center gap-2">
<span className="text-emerald-400 flex items-center gap-0.5">
<Shield className="w-3 h-3" /> NO KYC
</span>
<span></span>
<span className="text-amber-400 flex items-center gap-0.5">
<Globe className="w-3 h-3" /> PROXIFLY BACKEND
</span>
</div>
</div>
</div>
{/* Center Live Balance Counter ($1/hr) */}
<div className="flex items-center gap-3 bg-zinc-900/90 px-4 py-2 rounded-xl border border-amber-500/30 shadow-inner">
<Clock className="w-4 h-4 text-amber-400 animate-pulse" />
<div>
<div className="text-[10px] text-zinc-400 uppercase tracking-widest">Time Remaining ($1/hr):</div>
<div className="text-sm font-bold text-amber-300">
{String(hours).padStart(2, '0')}h {String(mins).padStart(2, '0')}m
</div>
</div>
<button
onClick={onOpenBtcPay}
className="ml-2 px-3 py-1.5 bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black font-bold rounded-lg text-[11px] flex items-center gap-1 shadow-md shadow-amber-500/20 transition-all"
>
<Bitcoin className="w-3.5 h-3.5" /> + TOP UP
</button>
</div>
{/* User Identity */}
<div className="flex items-center gap-3">
<div className="text-right">
<div className="text-cyan-300 font-bold flex items-center gap-1 justify-end">
<UserCheck className="w-3.5 h-3.5" /> @{user.username}
</div>
<div className="text-[10px] text-zinc-500">
Anonymous Client
</div>
</div>
<button
onClick={onLogout}
className="p-2 bg-zinc-900 hover:bg-zinc-800 text-zinc-400 hover:text-white rounded-xl border border-zinc-800 transition-colors"
title="Sign Out / Reset Session"
>
<LogOut className="w-4 h-4" />
</button>
</div>
</div>
</header>
);
};

View File

@@ -0,0 +1,204 @@
import React, { useState } from 'react';
import { Terminal, Shield, Cpu, RefreshCw, Send, HelpCircle, HardDrive, Zap } from 'lucide-react';
import { VM, UserSession } from '../types';
interface NeonTerminalProps {
vm: VM | null;
user: UserSession;
onExecuteCommand: (cmd: string) => void;
onVmStateChange: (state: 'booting' | 'running' | 'off') => void;
}
export const NeonTerminal: React.FC<NeonTerminalProps> = ({
vm,
user,
onExecuteCommand,
onVmStateChange,
}) => {
const [inputCmd, setInputCmd] = useState('');
const [logs, setLogs] = useState<Array<{ id: string; text: string; type: 'cmd' | 'output' | 'system' | 'error' }>>([
{ id: '1', text: '=== VORTEX_GPU KERNEL v4.8.1-PROXIFLY READY ===', type: 'system' },
{ id: '2', text: `SESSION IDENT: ${user.username.toUpperCase()} | BALANCE: ${user.balanceMinutes} MINUTES`, type: 'system' },
{ id: '3', text: 'Type "help", "status", "proxifly rotate", "apps", or "gpu-info" for available hacking controls.', type: 'system' },
]);
const handleSend = (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!inputCmd.trim()) return;
const trimmed = inputCmd.trim();
const newLogs = [...logs, { id: String(Date.now()), text: `${user.username}@vortex-node:~$ ${trimmed}`, type: 'cmd' as const }];
setInputCmd('');
const lower = trimmed.toLowerCase();
if (lower === 'help') {
newLogs.push({
id: String(Date.now() + 1),
text: `AVAILABLE COMMANDS:
- status : Show active VM, GPU load, Proxifly IP, and RDP port
- poweron / poweroff : Toggle physical node power
- proxifly rotate : Force IP rotation via Proxifly residential pool
- gpu-info : Query NVIDIA RTX 4080/4070 driver stats & CUDA status
- apps : List deployable AI apps (ComfyUI, Ollama, Fooocus)
- clear : Clear terminal screen history
- rdp-setup : Re-generate auto RDP credentials & streaming token`,
type: 'output',
});
} else if (lower === 'status') {
if (!vm) {
newLogs.push({ id: String(Date.now() + 1), text: 'NO ACTIVE VM INSTANCE DEPLOYED. Use the UI to provision a $1/hr GPU VM.', type: 'error' });
} else {
newLogs.push({
id: String(Date.now() + 1),
text: `[VM:${vm.id}] OS: ${vm.os.toUpperCase()} | STATE: ${vm.state.toUpperCase()}
GPU: ${vm.gpuSpec} | VRAM: ${vm.stats.vramUsedGb}/${vm.stats.vramTotalGb} GB
PROXIFLY IP: ${vm.proxiflyIp} (${vm.proxiflyLocation}) [${vm.proxiflyProtocol}]
AUTO POWER-OFF: Enabled if disconnected for > 10 min.`,
type: 'output',
});
}
} else if (lower === 'proxifly rotate') {
newLogs.push({
id: String(Date.now() + 1),
text: '[PROXIFLY] Initiating residential IP rotation sequence...',
type: 'system',
});
setTimeout(() => {
onExecuteCommand('proxifly_rotate');
}, 500);
} else if (lower === 'poweron') {
onVmStateChange('booting');
newLogs.push({ id: String(Date.now() + 1), text: '[POWER] Booting GPU host node...', type: 'system' });
} else if (lower === 'poweroff') {
onVmStateChange('off');
newLogs.push({ id: String(Date.now() + 1), text: '[POWER] Shutdown signal sent to node.', type: 'system' });
} else if (lower === 'gpu-info') {
newLogs.push({
id: String(Date.now() + 1),
text: `+-----------------------------------------------------------------------------+
| NVIDIA-SMI 555.42.02 Driver Version: 555.42.02 CUDA Version: 12.5 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 NVIDIA GeForce RTX 4080 On | 00000000:01:00.0 On | N/A |
| 35% 58C P2 210W / 320W | 8420MiB / 16376MiB | 74% Default |
+-------------------------------+----------------------+----------------------+`,
type: 'output',
});
} else if (lower === 'clear') {
setLogs([]);
return;
} else {
newLogs.push({
id: String(Date.now() + 1),
text: `vortex-sh: command not found: "${trimmed}". Type "help" for syntax.`,
type: 'error',
});
}
setLogs(newLogs);
onExecuteCommand(trimmed);
};
return (
<div className="flex flex-col h-full bg-zinc-950 rounded-xl border border-cyan-500/30 overflow-hidden font-mono shadow-2xl">
{/* Terminal Header Bar */}
<div className="flex items-center justify-between px-4 py-2.5 bg-zinc-900/90 border-b border-cyan-500/20">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/80" />
<div className="w-3 h-3 rounded-full bg-amber-500/80" />
<div className="w-3 h-3 rounded-full bg-emerald-500/80" />
<span className="ml-2 text-xs text-cyan-400 font-bold flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5" />
VORTEX_NEON_SH // PROXIFLY_ACTIVE
</span>
</div>
<div className="flex items-center gap-3 text-[11px] text-zinc-400">
<span className="text-emerald-400 flex items-center gap-1">
<Shield className="w-3 h-3" /> NO KYC
</span>
<span className="text-amber-400 flex items-center gap-1">
<Zap className="w-3 h-3" /> $1/HR
</span>
</div>
</div>
{/* Terminal Output Stream */}
<div className="flex-1 p-4 overflow-y-auto space-y-2 text-xs leading-relaxed max-h-[260px]">
{logs.map((log) => (
<div
key={log.id}
className={`${
log.type === 'cmd'
? 'text-cyan-300 font-semibold'
: log.type === 'system'
? 'text-emerald-400/90'
: log.type === 'error'
? 'text-red-400 font-medium'
: 'text-zinc-300 font-mono whitespace-pre-wrap'
}`}
>
{log.text}
</div>
))}
</div>
{/* Quick Action Chips */}
<div className="flex items-center gap-2 px-3 py-1.5 bg-zinc-900/60 border-t border-cyan-500/10 overflow-x-auto text-[11px]">
<button
onClick={() => {
setInputCmd('status');
handleSend();
}}
className="px-2 py-0.5 rounded bg-cyan-950/60 hover:bg-cyan-900/80 text-cyan-400 border border-cyan-500/30 transition-colors"
>
status
</button>
<button
onClick={() => {
setInputCmd('proxifly rotate');
handleSend();
}}
className="px-2 py-0.5 rounded bg-emerald-950/60 hover:bg-emerald-900/80 text-emerald-400 border border-emerald-500/30 transition-colors"
>
proxifly rotate
</button>
<button
onClick={() => {
setInputCmd('gpu-info');
handleSend();
}}
className="px-2 py-0.5 rounded bg-amber-950/60 hover:bg-amber-900/80 text-amber-400 border border-amber-500/30 transition-colors"
>
gpu-info
</button>
<button
onClick={() => {
setInputCmd('help');
handleSend();
}}
className="px-2 py-0.5 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors"
>
help
</button>
</div>
{/* Command Input Prompt */}
<form onSubmit={handleSend} className="flex items-center px-3 py-2 bg-zinc-950 border-t border-cyan-500/20">
<span className="text-emerald-400 font-bold text-xs mr-2">{user.username}@node:~$</span>
<input
type="text"
value={inputCmd}
onChange={(e) => setInputCmd(e.target.value)}
placeholder="type command..."
className="flex-1 bg-transparent text-cyan-300 placeholder-zinc-600 outline-none text-xs font-mono"
/>
<button type="submit" className="text-cyan-400 hover:text-cyan-200 transition-colors p-1">
<Send className="w-3.5 h-3.5" />
</button>
</form>
</div>
);
};

View File

@@ -0,0 +1,323 @@
import React, { useState } from 'react';
import {
Monitor,
Power,
RefreshCw,
Download,
Sparkles,
Globe,
Cpu,
Shield,
Layers,
Plus,
Zap,
Play,
RotateCcw,
Clock,
Terminal,
} from 'lucide-react';
import { VM, OSName, GpuSpec } from '../types';
import { AI_APP_TEMPLATES } from '../data/mockData';
interface VMManagerProps {
vms: VM[];
activeVmId: string | null;
onSelectVm: (id: string) => void;
onCreateVm: (os: OSName, gpu: GpuSpec, name: string, appTemplate: string) => void;
onTogglePower: (vmId: string) => void;
onRollDistro: (vmId: string, newOs: OSName) => void;
onRotateVmProxy: (vmId: string) => void;
onLaunchDesktop: (vm: VM) => void;
userBalanceMinutes: number;
}
export const VMManager: React.FC<VMManagerProps> = ({
vms,
activeVmId,
onSelectVm,
onCreateVm,
onTogglePower,
onRollDistro,
onRotateVmProxy,
onLaunchDesktop,
userBalanceMinutes,
}) => {
const [showCreateModal, setShowCreateModal] = useState(false);
const [newVmName, setNewVmName] = useState('Cyber-Workstation-01');
const [selectedOs, setSelectedOs] = useState<OSName>('ubuntu24');
const [selectedGpu, setSelectedGpu] = useState<GpuSpec>('RTX 4080 Super 16GB');
const [selectedApp, setSelectedApp] = useState<string>('comfyui');
const handleCreateSubmit = (e: React.FormEvent) => {
e.preventDefault();
onCreateVm(selectedOs, selectedGpu, newVmName, selectedApp);
setShowCreateModal(false);
};
const downloadRdpConfig = (vm: VM) => {
const rdpContent = `full address:s:${vm.proxiflyIp}:${vm.rdpPort}
username:s:${vm.rdpUser}
prompt for credentials:i:1
desktopwidth:i:1920
desktopheight:i:1080
screen mode id:i:2
redirectsmartcards:i:0
audiomode:i:0
videoplaybackmode:i:1
connection type:i:7
networkautodetect:i:1
bandwidthautodetect:i:1
enablecredsspsupport:i:1
authentication level:i:2`;
const blob = new Blob([rdpContent], { type: 'application/x-rdp' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${vm.name.toLowerCase().replace(/\s+/g, '_')}_autordp.rdp`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
return (
<div className="space-y-6 font-mono">
{/* Header Bar */}
<div className="flex flex-wrap items-center justify-between gap-4 p-4 bg-zinc-950 border border-cyan-500/30 rounded-xl shadow-xl">
<div>
<h2 className="text-lg font-bold text-white tracking-wider flex items-center gap-2">
YOUR GPU VIRTUAL MACHINES
<span className="text-[10px] bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded border border-emerald-500/30">
$1.00 / HOUR
</span>
</h2>
<p className="text-xs text-zinc-400">
Hardware accelerated NVIDIA RTX 4080 / 4070. Auto power-off when disconnected.
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2.5 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-black font-bold text-xs rounded-xl flex items-center gap-2 shadow-lg shadow-cyan-500/20 transition-all"
>
<Plus className="w-4 h-4" /> PROVISION NEW VM ($1/HR)
</button>
</div>
{/* VM Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{vms.map((vm) => {
const isSelected = activeVmId === vm.id;
return (
<div
key={vm.id}
onClick={() => onSelectVm(vm.id)}
className={`p-5 rounded-xl border transition-all cursor-pointer ${
isSelected
? 'bg-zinc-950 border-cyan-500 shadow-[0_0_20px_rgba(6,182,212,0.15)]'
: 'bg-zinc-950/70 border-zinc-800 hover:border-zinc-700'
}`}
>
{/* Card Top */}
<div className="flex items-start justify-between border-b border-zinc-800/80 pb-3 mb-4">
<div>
<div className="flex items-center gap-2">
<h3 className="text-base font-bold text-cyan-300">{vm.name}</h3>
<span
className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${
vm.state === 'running'
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: vm.state === 'booting'
? 'bg-amber-500/20 text-amber-300 border border-amber-500/30'
: 'bg-zinc-800 text-zinc-400'
}`}
>
{vm.state}
</span>
</div>
<div className="text-xs text-zinc-400 mt-1 flex items-center gap-3">
<span className="text-amber-300 font-semibold">{vm.gpuSpec}</span>
<span></span>
<span className="uppercase">{vm.os}</span>
<span></span>
<span>32GB RAM / 8 vCPU</span>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
onTogglePower(vm.id);
}}
className={`p-2 rounded-lg border transition-colors ${
vm.state === 'running'
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400 hover:bg-red-500/20 hover:border-red-500 hover:text-red-400'
: 'bg-zinc-900 border-zinc-800 text-zinc-400 hover:text-emerald-400'
}`}
title={vm.state === 'running' ? 'Shut Down VM' : 'Power On VM'}
>
<Power className="w-4 h-4" />
</button>
</div>
{/* Specs & Proxifly Proxy info */}
<div className="grid grid-cols-2 gap-3 mb-4 text-xs">
<div className="p-2.5 bg-zinc-900/60 rounded-lg border border-zinc-800/80">
<div className="text-zinc-500 text-[10px] uppercase">Proxifly IP Routing:</div>
<div className="text-emerald-400 font-bold truncate flex items-center gap-1 mt-0.5">
<Globe className="w-3 h-3" /> {vm.proxiflyIp}
</div>
<div className="text-[10px] text-zinc-400">{vm.proxiflyLocation} ({vm.proxiflyProtocol})</div>
</div>
<div className="p-2.5 bg-zinc-900/60 rounded-lg border border-zinc-800/80">
<div className="text-zinc-500 text-[10px] uppercase">Auto Idle Protection:</div>
<div className="text-amber-300 font-bold flex items-center gap-1 mt-0.5">
<Clock className="w-3 h-3" /> Auto Off if Idle
</div>
<div className="text-[10px] text-zinc-400">Preserves $1/hr budget</div>
</div>
</div>
{/* Quick Action Buttons */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 pt-2 border-t border-zinc-800/80 text-[11px]">
<button
onClick={(e) => {
e.stopPropagation();
onLaunchDesktop(vm);
}}
disabled={vm.state !== 'running'}
className="py-2 px-2 bg-cyan-500 hover:bg-cyan-400 disabled:opacity-40 text-black font-bold rounded-lg flex items-center justify-center gap-1 transition-colors"
>
<Play className="w-3.5 h-3.5" /> BROWSER STREAM
</button>
<button
onClick={(e) => {
e.stopPropagation();
downloadRdpConfig(vm);
}}
className="py-2 px-2 bg-zinc-900 hover:bg-zinc-800 text-zinc-200 border border-zinc-800 rounded-lg flex items-center justify-center gap-1 transition-colors"
>
<Download className="w-3.5 h-3.5 text-cyan-400" /> AUTO RDP
</button>
<button
onClick={(e) => {
e.stopPropagation();
onRotateVmProxy(vm.id);
}}
className="py-2 px-2 bg-zinc-900 hover:bg-zinc-800 text-zinc-200 border border-zinc-800 rounded-lg flex items-center justify-center gap-1 transition-colors"
>
<RefreshCw className="w-3.5 h-3.5 text-emerald-400" /> ROTATE IP
</button>
<button
onClick={(e) => {
e.stopPropagation();
const newOs: OSName = vm.os === 'ubuntu24' ? 'windows11' : vm.os === 'windows11' ? 'kali' : 'ubuntu24';
onRollDistro(vm.id, newOs);
}}
className="py-2 px-2 bg-zinc-900 hover:bg-zinc-800 text-zinc-200 border border-zinc-800 rounded-lg flex items-center justify-center gap-1 transition-colors"
>
<RotateCcw className="w-3.5 h-3.5 text-purple-400" /> ROLL DISTRO
</button>
</div>
</div>
);
})}
</div>
{/* PROVISION NEW VM MODAL */}
{showCreateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md p-4">
<div className="w-full max-w-xl bg-zinc-950 border border-cyan-500/40 rounded-2xl p-6 shadow-2xl relative">
<button
onClick={() => setShowCreateModal(false)}
className="absolute top-4 right-4 text-zinc-400 hover:text-white font-bold"
>
&times;
</button>
<h2 className="text-lg font-bold text-cyan-300 mb-1 flex items-center gap-2">
<Zap className="w-5 h-5 text-amber-400" /> PROVISION HARDWARE ACCELERATED VM
</h2>
<p className="text-xs text-zinc-400 mb-6">
Instant launch. Flat rate $1.00 / hr. Baked-in RTX 4080 / 4070 GPU + Proxifly random IP.
</p>
<form onSubmit={handleCreateSubmit} className="space-y-4 text-xs">
<div>
<label className="block text-zinc-400 mb-1">VM INSTANCE IDENTIFIER:</label>
<input
type="text"
value={newVmName}
onChange={(e) => setNewVmName(e.target.value)}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none focus:border-cyan-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-zinc-400 mb-1">OPERATING SYSTEM:</label>
<select
value={selectedOs}
onChange={(e) => setSelectedOs(e.target.value as OSName)}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-cyan-300 outline-none"
>
<option value="ubuntu24">Ubuntu 24.04 LTS Desktop (AI Ready)</option>
<option value="windows11">Windows 11 Enterprise (RTX OptiX)</option>
<option value="kali">Kali CyberSec Offensive Lab</option>
<option value="arch">Arch Linux (Bleeding Edge)</option>
<option value="debian">Debian 12 Stable Workspace</option>
</select>
</div>
<div>
<label className="block text-zinc-400 mb-1">GPU ACCELERATION SPECS:</label>
<select
value={selectedGpu}
onChange={(e) => setSelectedGpu(e.target.value as GpuSpec)}
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-amber-300 outline-none font-bold"
>
<option value="RTX 4080 Super 16GB">NVIDIA RTX 4080 Super 16GB VRAM</option>
<option value="RTX 4070 12GB">NVIDIA RTX 4070 12GB VRAM</option>
</select>
</div>
</div>
<div>
<label className="block text-zinc-400 mb-1">PRE-INSTALLED AI / APP TEMPLATE:</label>
<div className="grid grid-cols-2 gap-2">
{AI_APP_TEMPLATES.map((tmpl) => (
<div
key={tmpl.id}
onClick={() => setSelectedApp(tmpl.id)}
className={`p-2.5 rounded-lg border cursor-pointer transition-all ${
selectedApp === tmpl.id
? 'bg-cyan-950/60 border-cyan-500 text-cyan-300'
: 'bg-zinc-900/60 border-zinc-800 text-zinc-400 hover:border-zinc-700'
}`}
>
<div className="font-bold">{tmpl.name}</div>
<div className="text-[10px] text-zinc-500 line-clamp-1">{tmpl.description}</div>
</div>
))}
</div>
</div>
<div className="pt-2">
<button
type="submit"
className="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-black font-bold rounded-xl text-xs uppercase tracking-wider"
>
LAUNCH INSTANCE ($1.00 / HOUR)
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};

17
src/index.css Normal file
View File

@@ -0,0 +1,17 @@
@import "tailwindcss";
html, body, #root {
height: 100%;
margin: 0;
background: #05070d;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
color: #f4f4f5;
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: #27272a; border-radius: 4px; }
*::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
*::-webkit-scrollbar-track { background: transparent; }
::selection { background: rgba(34, 211, 238, 0.35); color: #fff; }

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

92
src/types.ts Normal file
View File

@@ -0,0 +1,92 @@
export type OSName = 'windows11' | 'ubuntu24' | 'kali' | 'arch' | 'debian';
export type GpuSpec = 'RTX 4070 12GB' | 'RTX 4080 Super 16GB';
export type VMState = 'off' | 'booting' | 'running' | 'stopping' | 'suspended';
export interface VMStats {
cpuLoad: number; // percentage 0-100
gpuLoad: number; // percentage 0-100
vramUsedGb: number; // e.g. 8.4
vramTotalGb: number; // e.g. 16
tempC: number; // e.g. 58
pingMs: number; // e.g. 14
fps: number; // e.g. 60
networkUpMbps: number;
networkDownMbps: number;
}
export interface VM {
id: string;
userId: string;
name: string;
os: OSName;
gpuSpec: GpuSpec;
vcpu: number;
ramGb: number;
storageGb: number;
state: VMState;
proxiflyIp: string;
proxiflyLocation: string;
proxiflyProtocol: 'SOCKS5' | 'HTTP';
proxiflyType: 'Residential' | 'Datacenter' | 'Mobile';
rdpPort: number;
rdpUser: string;
rdpPass: string;
autoPowerOffMin: number;
uptimeSeconds: number;
installedAppTemplates: string[];
stats: VMStats;
createdTime: number;
}
export interface UserSession {
id: string;
username: string;
isAdmin: boolean;
balanceMinutes: number;
btcAddress: string;
activeVmId: string | null;
createdAt: number;
}
export interface BtcPayInvoice {
invoiceId: string;
amountUsd: number;
amountSats: number;
btcAddress: string;
lightningInvoice: string;
status: 'pending' | 'settled' | 'expired';
minutesAdded: number;
createdAt: number;
expiresAt: number;
}
export interface ProxiflyGlobalConfig {
mode: 'random_residential' | 'datacenter_rotation' | 'strict_stealth';
activePoolSize: number;
autoRotateMinutes: number;
blockMaliciousRanges: boolean;
activeProxiesCount: number;
avgLatencyMs: number;
}
export interface SystemNode {
nodeId: string;
hostname: string;
region: string;
gpuType: string;
totalGpus: number;
assignedVms: number;
cpuUsagePct: number;
memUsagePct: number;
gpuUsagePct: number;
status: 'online' | 'degraded' | 'maintenance';
}
export interface AppTemplate {
id: string;
name: string;
icon: string;
description: string;
category: 'AI & ML' | 'OS & Desktop' | 'Security & Tools' | 'Rendering';
gpuRequirement: string;
}

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

28
vite.config.ts Normal file
View File

@@ -0,0 +1,28 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig} from 'vite';
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
build: {
rollupOptions: {
input: {
main: path.resolve(__dirname, 'index.html'),
admin: path.resolve(__dirname, 'admin.html'),
},
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
hmr: process.env.DISABLE_HMR !== 'true',
watch: process.env.DISABLE_HMR === 'true' ? null : {},
},
};
});

228
vortex-node-agent-linux.py Normal file
View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""
VORTEX_GPU — Linux Host Node Agent (port of vortex-node-agent.ps1)
Target: Ubuntu (nightmare .128, RTX 4080 SUPER 16GB)
Responsibilities (mirrors the Windows agent):
- Register this GPU box + stream nvidia-smi telemetry every N sec.
- Poll the gateway for jobs:
* shell : run an arbitrary cmd, return stdout
* hashcat / comfyui : run the command against the local GPU
* provision_comfyui : launch an ISOLATED ComfyUI instance on a dedicated
port + dedicated user/output/input dirs (tenant sees
a clean private machine; the physical GPU is shared/hidden)
* destroy_instance : kill the ComfyUI process for an instance
Auth: X-Node-Secret header (matches server.ts nodeAuthorized).
Runs as a systemd service: vortex-node-agent.service
"""
import json
import os
import socket
import subprocess
import time
import urllib.request
GATEWAY = os.environ.get("VORTEX_GATEWAY", "http://10.30.20.127:3000")
SECRET = os.environ.get("VORTEX_NODE_SECRET", "99496a5bf30b5a7411d3a60bf096ca642815fcf2be361113")
INTERVAL = int(os.environ.get("VORTEX_INTERVAL", "5"))
HOSTNAME = socket.gethostname()
HOME = os.path.expanduser("~")
COMFY_DIR = os.path.join(HOME, "ComfyUI")
COMFY_PY = os.path.join(COMFY_DIR, "venv", "bin", "python")
INST_DIR = os.path.join(HOME, "vortex-agent", "instances")
os.makedirs(INST_DIR, exist_ok=True)
_instance_pids = {} # instanceId -> pid
def http(method, path, body=None):
req = urllib.request.Request(GATEWAY + path, method=method)
req.add_header("X-Node-Secret", SECRET)
data = None
if body is not None:
data = json.dumps(body).encode()
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, data=data, timeout=60) as r:
return json.loads(r.read().decode())
def gpu_info():
try:
out = subprocess.run(
["nvidia-smi",
"--query-gpu=name,driver_version,memory.total,memory.used,utilization.gpu,temperature.gpu",
"--format=csv,noheader,nounits"],
capture_output=True, text=True, timeout=10).stdout.strip()
except Exception:
return None
if not out:
return None
p = [x.strip() for x in out.split(",")]
if len(p) < 6:
return None
try:
return {"gpuModel": p[0], "driverVersion": p[1], "memTotalMb": int(p[2]),
"memUsedMb": int(p[3]), "gpuUtilPct": int(p[4]), "tempC": int(p[5])}
except ValueError:
return None
def _procstat():
with open("/proc/stat") as f:
fields = f.readline().split()[1:]
idle = int(fields[3]) + int(fields[4])
total = sum(int(x) for x in fields)
return idle, total
def cpu_util():
try:
i1, t1 = _procstat()
time.sleep(0.2)
i2, t2 = _procstat()
if t2 == t1:
return 0
return int(100 * (1 - (i2 - i1) / (t2 - t1)))
except Exception:
return 0
def ram_info():
mem = {}
with open("/proc/meminfo") as f:
for line in f:
if ":" in line:
k = line.split(":")[0]
v = line.split(":")[1].strip().split()[0]
mem[k] = int(v)
total_gb = round(mem["MemTotal"] / 1024 / 1024, 1)
avail_gb = round(mem.get("MemAvailable", 0) / 1024 / 1024, 1)
return total_gb, round(total_gb - avail_gb, 1)
def uptime_sec():
return int(float(open("/proc/uptime").read().split()[0]))
def run_shell(command):
try:
r = subprocess.run(["bash", "-c", command], capture_output=True, text=True, timeout=600)
return (r.returncode == 0), (r.stdout or "") + (r.stderr or "")
except Exception as e:
return False, f"error: {e}"
def provision_comfyui(instance_id, port, user_dir):
os.makedirs(user_dir, exist_ok=True)
for sub in ("user", "output", "input", "models", "custom_nodes"):
os.makedirs(os.path.join(user_dir, sub), exist_ok=True)
if not os.path.exists(os.path.join(COMFY_DIR, "main.py")):
return False, "ComfyUI base not found on this node"
if not os.path.exists(COMFY_PY):
return False, "ComfyUI venv not found"
args = [COMFY_PY, os.path.join(COMFY_DIR, "main.py"),
"--listen", "0.0.0.0", "--port", str(port),
"--user-directory", os.path.join(user_dir, "user"),
"--output-directory", os.path.join(user_dir, "output"),
"--input-directory", os.path.join(user_dir, "input")]
try:
proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True)
_instance_pids[instance_id] = proc.pid
with open(os.path.join(user_dir, "instance.pid"), "w") as f:
f.write(str(proc.pid))
return True, f"launched pid={proc.pid} port={port}"
except Exception as e:
return False, f"failed to launch: {e}"
def destroy_instance(instance_id):
pid = _instance_pids.pop(instance_id, None)
if pid is None:
pidfile = os.path.join(INST_DIR, instance_id, "instance.pid")
if os.path.exists(pidfile):
try:
pid = int(open(pidfile).read().strip())
except Exception:
pid = None
if pid:
try:
os.kill(pid, 15)
return True, f"killed pid={pid}"
except Exception as e:
return True, f"kill pid={pid}: {e}"
return True, "no pid found"
def handle_job(job):
kind = job.get("kind")
cmd = job.get("command", "")
payload = job.get("payload", {}) or {}
if kind in ("shell", "hashcat", "comfyui"):
ok, result = run_shell(cmd)
elif kind == "provision_comfyui":
ok, result = provision_comfyui(payload.get("instanceId", "inst"),
payload.get("port", 8189),
payload.get("userDir") or os.path.join(INST_DIR, payload.get("instanceId", "inst")))
elif kind == "destroy_instance":
ok, result = destroy_instance(payload.get("instanceId", ""))
else:
ok, result = False, "unknown job kind"
try:
http("POST", f"/api/node/jobs/{job['id']}/result", {"ok": ok, "result": result})
except Exception as e:
print(f"[vortex-agent] result post failed: {e}", flush=True)
def main():
g = gpu_info()
ram_t, _ = ram_info()
print(f"[vortex-agent] registering node '{HOSTNAME}' with {GATEWAY} ...", flush=True)
try:
http("POST", "/api/node/register", {
"hostname": HOSTNAME,
"gpuModel": g["gpuModel"] if g else "GPU",
"driverVersion": g["driverVersion"] if g else "",
"memTotalMb": g["memTotalMb"] if g else 0,
"ramTotalGb": ram_t,
})
print(f"[vortex-agent] registered. GPU: {g['gpuModel'] if g else '?'}", flush=True)
except Exception as e:
print(f"[vortex-agent] register failed: {e}", flush=True)
print("[vortex-agent] agent active.", flush=True)
while True:
g = gpu_info()
ram_t, ram_u = ram_info()
cpu = cpu_util()
up = uptime_sec()
try:
http("POST", "/api/node/report", {
"hostname": HOSTNAME,
"gpuModel": g["gpuModel"] if g else "GPU",
"driverVersion": g["driverVersion"] if g else "",
"memTotalMb": g["memTotalMb"] if g else 0,
"memUsedMb": g["memUsedMb"] if g else 0,
"gpuUtilPct": g["gpuUtilPct"] if g else 0,
"tempC": g["tempC"] if g else 0,
"cpuUtilPct": cpu, "ramTotalGb": ram_t, "ramUsedGb": ram_u,
"uptimeSec": up,
})
except Exception as e:
print(f"[vortex-agent] heartbeat dropped: {e}", flush=True)
try:
resp = http("GET", f"/api/node/jobs?hostname={HOSTNAME}")
for job in resp.get("jobs", []):
print(f"[vortex-agent] job {job.get('id')} kind={job.get('kind')}", flush=True)
handle_job(job)
except Exception:
pass
time.sleep(INTERVAL)
if __name__ == "__main__":
main()

241
vortex-node-agent.ps1 Normal file
View File

@@ -0,0 +1,241 @@
# =====================================================================
# VORTEX_GPU — Windows Host Node Agent (v2 — provisioning-capable)
# Author: drjones | Target: Windows 11 (RTX 4070/4080/4080S)
#
# Responsibilities:
# - Register this GPU box + stream nvidia-smi telemetry every N sec.
# - Poll the gateway for jobs:
# * shell : run an arbitrary cmd, return stdout
# * provision_comfyui : clone/launch an ISOLATED ComfyUI instance on a
# dedicated port + dedicated user/output/input dir.
# Tenant sees a clean private machine — the physical
# GPU is shared and hidden.
# * destroy_instance : kill the ComfyUI process for an instance.
#
# Isolation model: one shared ComfyUI codebase, N isolated data dirs.
# Each instance gets its own --user-directory / --output-directory /
# --input-directory and its own port, so settings, models, and outputs
# never cross tenant boundaries.
#
# USAGE: powershell -ExecutionPolicy Bypass -File vortex-node-agent.ps1
# =====================================================================
param(
[string]$GatewayUrl = "http://10.30.20.127:3000",
[string]$NodeSecret = "99496a5bf30b5a7411d3a60bf096ca642815fcf2be361113",
[int]$IntervalSeconds = 5,
[string]$PythonExe = "python"
)
$ErrorActionPreference = "SilentlyContinue"
$HostName = $env:COMPUTERNAME
$VORTEX_ROOT = "C:\vortex"
$COMFY_BASE = Join-Path $VORTEX_ROOT "comfyui-base"
$INST_DIR = Join-Path $VORTEX_ROOT "instances"
# Instance PID tracking: instanceId -> PID (also written to a pid file per instance)
$script:InstancePids = @{}
function Get-GpuInfo {
$csv = & nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used,utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>$null
if (-not $csv) { return $null }
$parts = ($csv -join ",") -split ",\s*"
return @{
gpuModel = $parts[0].Trim()
driverVersion = $parts[1].Trim()
memTotalMb = [int]$parts[2]
memUsedMb = [int]$parts[3]
gpuUtilPct = [int]$parts[4]
tempC = [int]$parts[5]
}
}
function Get-CpuUtil {
$cpu = Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average
return [int]($cpu.Average)
}
function Get-RamInfo {
$os = Get-CimInstance Win32_OperatingSystem
$totalGb = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
$freeGb = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
return @{ totalGb = $totalGb; usedGb = [math]::Round($totalGb - $freeGb, 1) }
}
function Invoke-Gateway {
param($Method, $Path, $Body)
$headers = @{ "X-Node-Secret" = $NodeSecret }
$params = @{ Uri = "$GatewayUrl$Path"; Method = $Method; Headers = $headers; TimeoutSec = 60 }
if ($Body) {
$params.ContentType = "application/json"
$params.Body = ($Body | ConvertTo-Json -Compress -Depth 10)
}
return Invoke-RestMethod @params
}
# ---------------------------------------------------------------------
# COMFYUI PROVISIONING
# ---------------------------------------------------------------------
function Ensure-ComfyBase {
# One-time: clone ComfyUI (STABLE tag) + install torch (CUDA) into the shared base.
# Pin to a stable release — `main` tracks bleeding-edge comfy_kitchen which
# requires torch>=2.7. v0.9.x + torch 2.7.1+cu124 is the known-good combo.
if (Test-Path (Join-Path $COMFY_BASE "ComfyUI\main.py")) {
return $true
}
Write-Host "[VortexGPU] Bootstrapping ComfyUI base (one-time)..." -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path $VORTEX_ROOT | Out-Null
if (-not (Test-Path (Join-Path $COMFY_BASE "ComfyUI"))) {
git clone --depth 1 --branch v0.9.2 https://github.com/comfyanonymous/ComfyUI.git (Join-Path $COMFY_BASE "ComfyUI") 2>&1 | Out-Null
}
$comfyDir = Join-Path $COMFY_BASE "ComfyUI"
if (-not (Test-Path (Join-Path $comfyDir "main.py"))) {
return $false
}
# venv + torch (CUDA 12.4) — heavy, one-time. torch 2.6.0 is the latest on
# the cu124 index; v0.9.x works with it. Do NOT install comfy_kitchen (main-only).
$venvPy = Join-Path $COMFY_BASE "venv\Scripts\python.exe"
if (-not (Test-Path $venvPy)) {
& $PythonExe -m venv (Join-Path $COMFY_BASE "venv") 2>&1 | Out-Null
& $venvPy -m pip install --upgrade pip 2>&1 | Out-Null
& $venvPy -m pip install torch==2.6.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 2>&1 | Out-Null
& $venvPy -m pip install -r (Join-Path $comfyDir "requirements.txt") 2>&1 | Out-Null
}
return (Test-Path $venvPy)
}
function Start-ComfyInstance {
param($instanceId, $port, $userDir)
New-Item -ItemType Directory -Force -Path $userDir | Out-Null
foreach ($sub in @("user", "output", "input", "models", "custom_nodes")) {
New-Item -ItemType Directory -Force -Path (Join-Path $userDir $sub) | Out-Null
}
$comfyDir = Join-Path $COMFY_BASE "ComfyUI"
$venvPy = Join-Path $COMFY_BASE "venv\Scripts\python.exe"
# Per-instance: dedicated user/output/input dirs, shared read-only base models.
$args = @(
(Join-Path $comfyDir "main.py"),
"--listen", "0.0.0.0",
"--port", $port,
"--user-directory", (Join-Path $userDir "user"),
"--output-directory", (Join-Path $userDir "output"),
"--input-directory", (Join-Path $userDir "input")
)
$proc = Start-Process -FilePath $venvPy -ArgumentList $args -PassThru -WindowStyle Hidden
if ($proc) {
$script:InstancePids[$instanceId] = $proc.Id
Set-Content -Path (Join-Path $userDir "instance.pid") -Value $proc.Id
return "launched pid=$($proc.Id) port=$port"
}
return "failed to launch"
}
function Stop-ComfyInstance {
param($instanceId)
$pidFile = Join-Path $INST_DIR $instanceId "instance.pid"
$pid = $script:InstancePids[$instanceId]
if (-not $pid -and (Test-Path $pidFile)) { $pid = [int](Get-Content $pidFile) }
if ($pid) {
Stop-Process -Id $pid -Force 2>$null
$script:InstancePids.Remove($instanceId)
return "killed pid=$pid"
}
return "no pid found"
}
function Handle-Job {
param($job)
Write-Host "[VortexGPU] Job $($job.id) kind=$($job.kind)" -ForegroundColor Cyan
$ok = $false
$result = ""
switch ($job.kind) {
"shell" {
$result = cmd /c $job.command 2>&1 | Out-String
$ok = ($LASTEXITCODE -eq 0) -or ($LASTEXITCODE -eq $null)
}
"hashcat" {
# hashcat job: $job.payload has target + hash; run against local GPU
$result = cmd /c $job.command 2>&1 | Out-String
$ok = ($LASTEXITCODE -eq 0) -or ($LASTEXITCODE -eq $null)
}
"comfyui" {
$result = cmd /c $job.command 2>&1 | Out-String
$ok = ($LASTEXITCODE -eq 0) -or ($LASTEXITCODE -eq $null)
}
"provision_comfyui" {
$ok = Ensure-ComfyBase
if ($ok) {
$result = Start-ComfyInstance -instanceId $job.payload.instanceId -port $job.payload.port -userDir $job.payload.userDir
$ok = $result -like "launched*"
} else {
$result = "ComfyUI base bootstrap failed (check git/python/torch install)"
}
}
"destroy_instance" {
$result = Stop-ComfyInstance -instanceId $job.payload.instanceId
$ok = $true
}
default {
$result = "unknown job kind"
}
}
try {
Invoke-Gateway "POST" "/api/node/jobs/$($job.id)/result" @{ ok = $ok; result = $result } | Out-Null
} catch {
Write-Host "[VortexGPU] result post failed: $_" -ForegroundColor DarkYellow
}
}
# ---------------------------------------------------------------------
# REGISTER
# ---------------------------------------------------------------------
Write-Host "[VortexGPU] Registering node '$HostName' with $GatewayUrl ..." -ForegroundColor Cyan
$gpu = Get-GpuInfo
$ram = Get-RamInfo
try {
Invoke-Gateway "POST" "/api/node/register" @{
hostname = $HostName; gpuModel = $gpu.gpuModel; driverVersion = $gpu.driverVersion
memTotalMb = $gpu.memTotalMb; ramTotalGb = $ram.totalGb
} | Out-Null
Write-Host "[VortexGPU] Registered. GPU: $($gpu.gpuModel)" -ForegroundColor Green
} catch {
Write-Host "[VortexGPU] register failed: $_" -ForegroundColor Yellow
}
# ---------------------------------------------------------------------
# MAIN LOOP
# ---------------------------------------------------------------------
$boot = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
Write-Host "[VortexGPU] Agent active." -ForegroundColor Green
while ($true) {
# telemetry
$gpu = Get-GpuInfo
$ram = Get-RamInfo
$cpu = Get-CpuUtil
$uptime = [int]((Get-Date) - $boot).TotalSeconds
try {
Invoke-Gateway "POST" "/api/node/report" @{
hostname = $HostName; gpuModel = $gpu.gpuModel; driverVersion = $gpu.driverVersion
memTotalMb = $gpu.memTotalMb; memUsedMb = $gpu.memUsedMb; gpuUtilPct = $gpu.gpuUtilPct
tempC = $gpu.tempC; cpuUtilPct = $cpu; ramTotalGb = $ram.totalGb
ramUsedGb = $ram.usedGb; uptimeSec = $uptime
} | Out-Null
} catch {
Write-Host "[VortexGPU] heartbeat dropped: $_" -ForegroundColor DarkYellow
}
# jobs
try {
$resp = Invoke-Gateway "GET" "/api/node/jobs?hostname=$HostName" $null
foreach ($job in $resp.jobs) {
Handle-Job -job $job
}
} catch {}
Start-Sleep -Seconds $IntervalSeconds
}
# schtasks install:
# schtasks /Create /TN "VortexGPU Node Agent" /TR "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\vortex\vortex-node-agent.ps1" /SC ONLOGON /RL HIGHEST /F