Ubuntu GPU sessions: spawn/destroy full desktop with 4080 attached via noVNC proxy. Fixed branding (was 'My Google AI Studio App').
This commit is contained in:
@@ -3,10 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>My Google AI Studio App</title>
|
<title>VortexGPU — Rent a GPU PC by the Hour</title>
|
||||||
<meta name="description" content="An application built with Google AI Studio." />
|
<meta name="description" content="Rent a real GPU PC by the hour. Ubuntu sessions, Windows RDP, Linux SSH — Bitcoin via BTCPay." />
|
||||||
<meta property="og:title" content="My Google AI Studio App" />
|
<meta property="og:title" content="VortexGPU — Rent a GPU PC by the Hour" />
|
||||||
<meta property="og:description" content="An application built with Google AI Studio." />
|
<meta property="og:description" content="Rent a real GPU PC by the hour. Ubuntu sessions, Windows RDP, Linux SSH." />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
173
package-lock.json
generated
173
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
@@ -1554,6 +1555,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/http-proxy": {
|
||||||
|
"version": "1.17.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
|
||||||
|
"integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -1814,6 +1824,18 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/braces": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fill-range": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.8",
|
"version": "4.28.8",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||||
@@ -2171,6 +2193,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.22.2",
|
"version": "4.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||||
@@ -2284,6 +2312,18 @@
|
|||||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fill-range": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
@@ -2317,6 +2357,26 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/formdata-polyfill": {
|
"node_modules/formdata-polyfill": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
@@ -2573,6 +2633,37 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-proxy": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"follow-redirects": "^1.0.0",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-middleware": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-iwbQltVlx8bCrqePUM8C+hllHvdawVhQJaLrj1X7qllkvFQdXFsr16pW/mo9+JDVjN+QO2XUx9jd8SmoFkE5qw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/http-proxy": "^1.17.15",
|
||||||
|
"debug": "^4.3.6",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"is-glob": "^4.0.3",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"micromatch": "^4.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || ^16.10.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
@@ -2613,6 +2704,45 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-glob": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-number": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||||
@@ -3006,6 +3136,31 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/micromatch": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"braces": "^3.0.3",
|
||||||
|
"picomatch": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/micromatch/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
@@ -3371,6 +3526,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
@@ -3657,6 +3818,18 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/to-regex-range": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
|
|||||||
91
server.ts
91
server.ts
@@ -7,6 +7,7 @@ import { execFile } from "child_process";
|
|||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { createServer as createViteServer } from "vite";
|
import { createServer as createViteServer } from "vite";
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||||
|
|
||||||
const exec = promisify(execFile);
|
const exec = promisify(execFile);
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ type GpuNode = {
|
|||||||
type GpuJob = {
|
type GpuJob = {
|
||||||
id: string;
|
id: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
kind: "shell" | "hashcat" | "comfyui";
|
kind: "shell" | "hashcat" | "comfyui" | "provision_ubuntu" | "destroy_ubuntu";
|
||||||
command: string;
|
command: string;
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
status: "pending" | "running" | "done" | "failed";
|
status: "pending" | "running" | "done" | "failed";
|
||||||
@@ -119,6 +120,13 @@ db.exec(`
|
|||||||
btcpay_invoice_id TEXT, checkout_link TEXT,
|
btcpay_invoice_id TEXT, checkout_link TEXT,
|
||||||
status TEXT NOT NULL DEFAULT 'pending', created_at INTEGER NOT NULL, settled_at INTEGER
|
status TEXT NOT NULL DEFAULT 'pending', created_at INTEGER NOT NULL, settled_at INTEGER
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
||||||
|
instance_id TEXT NOT NULL UNIQUE, node_hostname TEXT NOT NULL,
|
||||||
|
node_ip TEXT NOT NULL, port INTEGER NOT NULL, password TEXT NOT NULL,
|
||||||
|
resolution TEXT, state TEXT NOT NULL DEFAULT 'provisioning',
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// ---- Migrations (add columns that predate password/unlimited) ----
|
// ---- Migrations (add columns that predate password/unlimited) ----
|
||||||
@@ -202,6 +210,21 @@ function allocatePort(): number {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allocate a session (noVNC) port from a dedicated range, distinct from VM ports.
|
||||||
|
function allocateSessionPort(): number {
|
||||||
|
const used = all<{ port: number }>("SELECT port FROM sessions WHERE port IS NOT NULL").map((r) => r.port);
|
||||||
|
let p = 6090;
|
||||||
|
while (used.includes(p) && p < 6190) p++;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue a GPU job for a node to pick up on its next poll.
|
||||||
|
function dispatchJob(hostname: string, kind: GpuJob["kind"], command: string, payload: Record<string, unknown>): GpuJob {
|
||||||
|
const job: GpuJob = { id: "job_" + crypto.randomBytes(6).toString("hex"), hostname, kind, command, payload, status: "pending", result: "", createdAt: Date.now(), completedAt: null };
|
||||||
|
jobs.push(job); persistJobs();
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- auth helpers ----
|
// ---- auth helpers ----
|
||||||
function nodeAuthorized(req: express.Request) { return req.headers["x-node-secret"] === NODE_SECRET; }
|
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 adminAuthorized(req: express.Request) { return (req.headers["authorization"] || "") === `Bearer ${ADMIN_TOKEN}`; }
|
||||||
@@ -380,6 +403,46 @@ async function startServer() {
|
|||||||
res.json({ received: true });
|
res.json({ received: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== UBUNTU GPU SESSIONS (spawn in-browser desktop with the 4080 attached) =====
|
||||||
|
app.post("/api/session/spawn", (req, res) => {
|
||||||
|
const { userId, resolution } = req.body || {};
|
||||||
|
const user = one<any>("SELECT * FROM users WHERE id=?", str(userId, ""));
|
||||||
|
if (!user) return res.status(400).json({ error: "invalid user" });
|
||||||
|
|
||||||
|
// Target the Linux GPU node (nightmare) that runs the Ubuntu-session agent.
|
||||||
|
const hostname = "nightmare";
|
||||||
|
const node = nodes[hostname];
|
||||||
|
if (!node || Date.now() - node.lastSeen > 30_000) {
|
||||||
|
return res.status(503).json({ error: "GPU node offline — try again shortly" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceId = "sess_" + crypto.randomBytes(4).toString("hex");
|
||||||
|
const port = allocateSessionPort();
|
||||||
|
const password = "Ub" + crypto.randomBytes(6).toString("hex") + "!";
|
||||||
|
const reso = str(resolution, "1440x900");
|
||||||
|
const id = "ses_" + crypto.randomBytes(8).toString("hex");
|
||||||
|
|
||||||
|
q("INSERT INTO sessions (id,user_id,instance_id,node_hostname,node_ip,port,password,resolution,state,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
id, user.id, instanceId, hostname, node.ip, port, password, reso, "provisioning", Date.now());
|
||||||
|
dispatchJob(hostname, "provision_ubuntu", "", { instanceId, port, password, resolution: reso });
|
||||||
|
|
||||||
|
res.json({ id, instanceId, port, password, resolution: reso, state: "provisioning", url: `/session/${instanceId}/` });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/sessions", (req, res) => {
|
||||||
|
const userId = str(req.query.userId, "");
|
||||||
|
if (!userId) return res.status(400).json({ error: "userId required" });
|
||||||
|
res.json(all<any>("SELECT * FROM sessions WHERE user_id=? ORDER BY created_at DESC", userId));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/session/destroy", (req, res) => {
|
||||||
|
const sess = one<any>("SELECT * FROM sessions WHERE id=? AND user_id=?", str(req.body?.sessionId, ""), str(req.body?.userId, ""));
|
||||||
|
if (!sess) return res.status(404).json({ error: "not found" });
|
||||||
|
q("UPDATE sessions SET state='stopping' WHERE id=?", sess.id);
|
||||||
|
dispatchJob(sess.node_hostname, "destroy_ubuntu", "", { instanceId: sess.instance_id });
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
// ===== GPU NODE LAYER =====
|
// ===== GPU NODE LAYER =====
|
||||||
app.post("/api/node/register", (req, res) => {
|
app.post("/api/node/register", (req, res) => {
|
||||||
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
|
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
|
||||||
@@ -417,6 +480,13 @@ async function startServer() {
|
|||||||
job.status = req.body?.ok ? "done" : "failed";
|
job.status = req.body?.ok ? "done" : "failed";
|
||||||
job.result = String(req.body?.result ?? "");
|
job.result = String(req.body?.result ?? "");
|
||||||
job.completedAt = Date.now();
|
job.completedAt = Date.now();
|
||||||
|
// Reflect provisioning result onto the session row.
|
||||||
|
const p = job.payload as any;
|
||||||
|
if (job.kind === "provision_ubuntu" && p?.instanceId) {
|
||||||
|
q("UPDATE sessions SET state=? WHERE instance_id=?", req.body?.ok ? "running" : "failed", p.instanceId);
|
||||||
|
} else if (job.kind === "destroy_ubuntu" && p?.instanceId) {
|
||||||
|
q("UPDATE sessions SET state='stopped' WHERE instance_id=?", p.instanceId);
|
||||||
|
}
|
||||||
persistJobs();
|
persistJobs();
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
@@ -475,6 +545,22 @@ async function startServer() {
|
|||||||
} catch (e) { console.error("[billing]", e); }
|
} catch (e) { console.error("[billing]", e); }
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
|
// ===== SESSION noVNC PROXY (WebSocket-capable) =====
|
||||||
|
const sessionProxy = createProxyMiddleware({
|
||||||
|
target: "http://127.0.0.1:1",
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
pathFilter: "/session/**",
|
||||||
|
router: (req) => {
|
||||||
|
const m = (req.url || "").match(/^\/session\/([^/]+)/);
|
||||||
|
if (!m) return "http://127.0.0.1:1";
|
||||||
|
const sess = one<any>("SELECT * FROM sessions WHERE instance_id=?", m[1]);
|
||||||
|
return sess ? `http://${sess.node_ip}:${sess.port}` : "http://127.0.0.1:1";
|
||||||
|
},
|
||||||
|
pathRewrite: (path) => path.replace(/^\/session\/[^/]+/, "") || "/",
|
||||||
|
});
|
||||||
|
app.use(sessionProxy);
|
||||||
|
|
||||||
// ===== STATIC =====
|
// ===== STATIC =====
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa" });
|
const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa" });
|
||||||
@@ -488,11 +574,12 @@ async function startServer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(PORT, "0.0.0.0", () => {
|
const server = app.listen(PORT, "0.0.0.0", () => {
|
||||||
console.log(`[VortexGPU] rent-a-PC gateway on :${PORT}`);
|
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] Proxmox ${PVE_HOST} | win tpl ${PVE_TEMPLATE_WIN} | linux tpl ${PVE_TEMPLATE_LINUX}`);
|
||||||
console.log(`[VortexGPU] GPU SKU: ${GPU_SKU}`);
|
console.log(`[VortexGPU] GPU SKU: ${GPU_SKU}`);
|
||||||
});
|
});
|
||||||
|
server.on("upgrade", sessionProxy.upgrade);
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer().catch((e) => { console.error(e); process.exit(1); });
|
startServer().catch((e) => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
68
src/App.tsx
68
src/App.tsx
@@ -24,12 +24,24 @@ interface ApiVm {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiSession {
|
||||||
|
id: string;
|
||||||
|
instance_id: string;
|
||||||
|
node_hostname: string;
|
||||||
|
port: number;
|
||||||
|
password: string;
|
||||||
|
resolution: string;
|
||||||
|
state: string;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface Session { id: string; username: string; balance_minutes: number; unlimited?: boolean; }
|
interface Session { id: string; username: string; balance_minutes: number; unlimited?: boolean; }
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
const [vms, setVms] = useState<ApiVm[]>([]);
|
const [vms, setVms] = useState<ApiVm[]>([]);
|
||||||
const [gpuSku, setGpuSku] = useState('NVIDIA GeForce RTX 5090 32GB');
|
const [sessions, setSessions] = useState<ApiSession[]>([]);
|
||||||
|
const [gpuSku, setGpuSku] = useState('NVIDIA GeForce RTX 4080 SUPER 16GB');
|
||||||
const [maxVms, setMaxVms] = useState(3);
|
const [maxVms, setMaxVms] = useState(3);
|
||||||
const [loginName, setLoginName] = useState('');
|
const [loginName, setLoginName] = useState('');
|
||||||
const [loginPass, setLoginPass] = useState('');
|
const [loginPass, setLoginPass] = useState('');
|
||||||
@@ -50,6 +62,8 @@ export default function App() {
|
|||||||
setGpuSku(d.gpu_sku || gpuSku);
|
setGpuSku(d.gpu_sku || gpuSku);
|
||||||
setMaxVms(d.max_vms || 3);
|
setMaxVms(d.max_vms || 3);
|
||||||
}
|
}
|
||||||
|
const sr = await fetch(`/api/sessions?userId=${session.id}`);
|
||||||
|
if (sr.ok) setSessions(await sr.json());
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}, [session, gpuSku]);
|
}, [session, gpuSku]);
|
||||||
|
|
||||||
@@ -93,6 +107,22 @@ export default function App() {
|
|||||||
refresh();
|
refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSpawnSession = async () => {
|
||||||
|
if (!session) return;
|
||||||
|
setLoading(true); setError('');
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/session/spawn', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: session.id, resolution: '1440x900' }) });
|
||||||
|
const d = await r.json();
|
||||||
|
if (r.ok) refresh();
|
||||||
|
else setError(d.error || 'spawn failed');
|
||||||
|
} catch { setError('network error'); } finally { setLoading(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDestroySession = async (sessionId: string) => {
|
||||||
|
await fetch('/api/session/destroy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: session!.id, sessionId }) });
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#05070d] flex items-center justify-center p-6 font-sans">
|
<div className="min-h-screen bg-[#05070d] flex items-center justify-center p-6 font-sans">
|
||||||
@@ -184,6 +214,42 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ubuntu GPU Sessions */}
|
||||||
|
<div className="bg-zinc-900/40 border border-emerald-800/40 rounded-2xl p-5">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h2 className="font-bold text-lg flex items-center gap-2"><Terminal className="w-5 h-5 text-emerald-400" /> Ubuntu GPU Sessions</h2>
|
||||||
|
<span className="text-xs text-zinc-500">RTX 4080 SUPER 16GB · in-browser desktop</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-zinc-500 mb-4">Spawn a full Ubuntu desktop in your browser with the 4080 attached (noVNC). Install anything, run any GPU job.</p>
|
||||||
|
<button onClick={handleSpawnSession} disabled={loading} className="w-full py-3 bg-gradient-to-r from-emerald-500 to-teal-600 text-black font-bold rounded-xl text-sm disabled:opacity-40">
|
||||||
|
{loading ? 'Spawning...' : '⚡ Spawn Ubuntu Session'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{sessions.length > 0 && (
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-3">
|
||||||
|
{sessions.map((s) => (
|
||||||
|
<div key={s.id} className="bg-black/40 border border-zinc-800 rounded-xl p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Terminal className="w-4 h-4 text-emerald-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-sm">{s.instance_id}</div>
|
||||||
|
<div className="text-[10px] text-zinc-500">{s.resolution} · node {s.node_hostname}:{s.port}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${s.state === 'running' ? 'bg-emerald-500/20 text-emerald-300' : s.state === 'provisioning' ? 'bg-amber-500/20 text-amber-300' : s.state === 'failed' ? 'bg-red-500/20 text-red-300' : 'bg-zinc-800 text-zinc-400'}`}>{s.state}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-3">
|
||||||
|
<a href={`/session/${s.instance_id}/`} target="_blank" rel="noopener noreferrer" className="flex-1 text-center py-2 bg-emerald-500 text-black font-bold rounded-lg text-xs"><ExternalLink className="w-3.5 h-3.5 inline" /> Open Desktop</a>
|
||||||
|
<button onClick={() => handleDestroySession(s.id)} className="px-3 py-2 border border-red-500/40 text-red-300 rounded-lg text-xs font-bold hover:bg-red-500/10">Stop</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-[10px] text-zinc-500 font-mono">VNC password: <span className="text-amber-300">{s.password}</span></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Machines */}
|
{/* Machines */}
|
||||||
{vms.length === 0 ? (
|
{vms.length === 0 ? (
|
||||||
<div className="text-center py-16 border border-dashed border-zinc-800 rounded-2xl">
|
<div className="text-center py-16 border border-dashed border-zinc-800 rounded-2xl">
|
||||||
|
|||||||
Reference in New Issue
Block a user