Compare commits
2
Commits
03c1f14dba
...
4239886b1a
No files matched your search
@@ -269,6 +269,26 @@ pnpm build
|
||||
|
||||
Then start control-plane and open `/ui/` on the same host/port.
|
||||
|
||||
For UI development with hot reload, run `pnpm --dir ui dev` and open
|
||||
`http://127.0.0.1:5173/ui/` (Vite chooses another port if occupied). API requests
|
||||
and terminal/event WebSockets proxy to `http://127.0.0.1:8080` by default.
|
||||
Override the backend with `WAKEY_DEV_API_TARGET=http://127.0.0.1:18080 pnpm --dir ui dev`,
|
||||
or set it in `ui/.env` using `ui/.env.example`. Restart Vite after changing it.
|
||||
This setting is dev-only; built assets still use the serving CC's origin.
|
||||
|
||||
For the remote CC, first open an SSH tunnel:
|
||||
|
||||
```sh
|
||||
ssh -N -L 127.0.0.1:18080:127.0.0.1:23501 cloud
|
||||
```
|
||||
|
||||
Use `http://127.0.0.1:18080` as the dev target. This connects directly to CC
|
||||
through SSH, avoiding Cloudflare Access browser redirects/cookie handling in the
|
||||
proxy. The dev server binds to loopback; do not expose this authenticated tunnel
|
||||
through a public Vite server. Changes made in the dev UI affect the selected
|
||||
backend, including production if that is what you tunnel to. HTTPS targets retain
|
||||
certificate verification; the proxy does not add Cloudflare credentials.
|
||||
|
||||
## VPS Deploy (Manual Updates)
|
||||
|
||||
Suggested layout on VPS:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Vite dev server only. Use an origin, without /ui or /api suffixes.
|
||||
WAKEY_DEV_API_TARGET=http://127.0.0.1:8080
|
||||
+7
-7
@@ -15,13 +15,13 @@
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@xterm/addon-clipboard": "0.3.0-beta.301",
|
||||
"@xterm/addon-fit": "0.12.0-beta.301",
|
||||
"@xterm/addon-image": "0.10.0-beta.301",
|
||||
"@xterm/addon-unicode-graphemes": "0.5.0-beta.301",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.301",
|
||||
"@xterm/addon-web-links": "0.13.0-beta.301",
|
||||
"@xterm/xterm": "6.1.0-beta.301",
|
||||
"@xterm/addon-clipboard": "0.3.0-beta.313",
|
||||
"@xterm/addon-fit": "0.12.0-beta.311",
|
||||
"@xterm/addon-image": "0.10.0-beta.310",
|
||||
"@xterm/addon-unicode-graphemes": "0.5.0-beta.310",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.310",
|
||||
"@xterm/addon-web-links": "0.13.0-beta.310",
|
||||
"@xterm/xterm": "6.1.0-beta.315",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
|
||||
Generated
+47
-47
@@ -18,26 +18,26 @@ importers:
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2([email protected](@types/[email protected])([email protected]))
|
||||
'@xterm/addon-clipboard':
|
||||
specifier: 0.3.0-beta.301
|
||||
version: 0.3.0-beta.301(@xterm/[email protected]01)
|
||||
specifier: 0.3.0-beta.313
|
||||
version: 0.3.0-beta.313(@xterm/[email protected]5)
|
||||
'@xterm/addon-fit':
|
||||
specifier: 0.12.0-beta.301
|
||||
version: 0.12.0-beta.301(@xterm/[email protected]01)
|
||||
specifier: 0.12.0-beta.311
|
||||
version: 0.12.0-beta.311(@xterm/[email protected]5)
|
||||
'@xterm/addon-image':
|
||||
specifier: 0.10.0-beta.301
|
||||
version: 0.10.0-beta.301(@xterm/[email protected]01)
|
||||
specifier: 0.10.0-beta.310
|
||||
version: 0.10.0-beta.310(@xterm/[email protected]5)
|
||||
'@xterm/addon-unicode-graphemes':
|
||||
specifier: 0.5.0-beta.301
|
||||
version: 0.5.0-beta.301(@xterm/[email protected]01)
|
||||
specifier: 0.5.0-beta.310
|
||||
version: 0.5.0-beta.310(@xterm/[email protected]5)
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.301
|
||||
version: 0.10.0-beta.301(@xterm/[email protected]01)
|
||||
specifier: 0.10.0-beta.310
|
||||
version: 0.10.0-beta.310(@xterm/[email protected]5)
|
||||
'@xterm/addon-web-links':
|
||||
specifier: 0.13.0-beta.301
|
||||
version: 0.13.0-beta.301(@xterm/[email protected]01)
|
||||
specifier: 0.13.0-beta.310
|
||||
version: 0.13.0-beta.310(@xterm/[email protected]5)
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.301
|
||||
version: 6.1.0-beta.301
|
||||
specifier: 6.1.0-beta.315
|
||||
version: 6.1.0-beta.315
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -836,38 +836,38 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-DRL0EybPhWzgndaTeakVbh/HvJF7yzxRtfNCnIudFZQaGugFYsEUZNOaV2NZev8z5kLwor6miH1j3DNMI8Paxg==}
|
||||
'@xterm/[email protected]3':
|
||||
resolution: {integrity: sha512-Jvt9vzccZ1vQ0VHsQsvW0j9TGThsCeY9T3x2TP+pHhYB5NVgOIlKhNiRRGTdMxsJd8eW3fxEpTGVhf88aeb8Ug==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.301
|
||||
'@xterm/xterm': ^6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-MukRyJLAFQrW/++aM209jBZ8G9J/Ioe4SGS6c3ba87v+zYuX/I66JrDB7sVfykQ9t7DlT+c1MWl1jTlX2kyx1w==}
|
||||
'@xterm/[email protected]11':
|
||||
resolution: {integrity: sha512-M40mAWDoAVKz+0zs5ipmBJ+tNilw8CzUbbGIX05NANmTiw0MS47FYeJ4d2El4zdvFphDJjtVLrxTKXuz2wUM6Q==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.304
|
||||
'@xterm/xterm': ^6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-PJZL4GlQ1ReIgbffFMaOAFxc3wOit21/G2UEM82RpH3hNHjn6MH8ilz3ToDeBjYuOqU7ienBaeIuMbNktX4/HA==}
|
||||
'@xterm/[email protected]0':
|
||||
resolution: {integrity: sha512-8fzgYV7HC9iWlw8ChZgnPmCXeBoYkC3Kyo9C0vyOQWD7Cm/bebKNNWhzNlDt5Qxbv+UBPbgkcO4i3dTYnggkRw==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.304
|
||||
'@xterm/xterm': ^6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-EBZIZYS6RItWTGO/KpLqV6y3IKGYyugbP07lK0x6G7185FVA61L2t0/XVtVEQNpczsbPo8zAxBhpoV72TP8CYw==}
|
||||
'@xterm/[email protected]0':
|
||||
resolution: {integrity: sha512-oCvQV7rKNekf7KcNdVicPhOo47W160QS6tIhXG7VpQ9OKGKTrAnMAxD+fr/nTlbd3h86+zvMqJpfd7NKEm/fYA==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.304
|
||||
'@xterm/xterm': ^6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-SS5BzfbvfFIikE4xZFAtHu6n0Kl5R1ZCPx41RL2WULLSE+HFo/WmYee53tMxNK/QUjpcQWepeVB7HebCPhHnig==}
|
||||
'@xterm/[email protected]0':
|
||||
resolution: {integrity: sha512-kQQIWMaQX7ViV1ofrYQnydR4/yNqCgOXYSYXG+x9pvNFU5/3KzZbqeQOXuM1y3cZYzR+Mw2cy3oRp/ssnK9cww==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.304
|
||||
'@xterm/xterm': ^6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-BpY297K+FGLsTQemnA6uOg6hk85DwVuUvWCqE9rIAcDxOOC7fP9GKAZTIfaEos/EZk2Ei23vD6w0P5Br6+RU7Q==}
|
||||
'@xterm/[email protected]0':
|
||||
resolution: {integrity: sha512-sxY3n5N3BCafchHLpqZSP44SncYhgt7rYSqk797naAeTJgymL3kG7fNIRob6SeV3nvf3MH0EIB6B3xxATtHYsQ==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.304
|
||||
'@xterm/xterm': ^6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01':
|
||||
resolution: {integrity: sha512-y1pH42+Mx+h5E2xDgyilYXBKYkn8/6+anToiTmIt8gVww/2gHFqheK0vW+m9LADQsZ5AFs+SQvcK+tOyxWfRJA==}
|
||||
'@xterm/[email protected]5':
|
||||
resolution: {integrity: sha512-w7MeKxi6vbxE3IOFRiwgugkM6FZonE9gF9KyJ0OiE5HbYZOm38bz+CTGZg3+wqA6porJnHfjzX1KaihInP+iWg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
@@ -2862,31 +2862,31 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
||||
'@xterm/[email protected]3(@xterm/[email protected]5)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.301
|
||||
'@xterm/xterm': 6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
||||
'@xterm/[email protected]11(@xterm/[email protected]5)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.301
|
||||
'@xterm/xterm': 6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
||||
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.301
|
||||
'@xterm/xterm': 6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
||||
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.301
|
||||
'@xterm/xterm': 6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
||||
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.301
|
||||
'@xterm/xterm': 6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
||||
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.301
|
||||
'@xterm/xterm': 6.1.0-beta.315
|
||||
|
||||
'@xterm/[email protected]01': {}
|
||||
'@xterm/[email protected]5': {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer as createHttpServer, request } from "node:http";
|
||||
import { once } from "node:events";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
test("dev proxy preserves API requests and WebSocket upgrades under /ui base", async () => {
|
||||
const sockets = new Set();
|
||||
const backend = createHttpServer(async (req, res) => {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) chunks.push(chunk);
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
body: Buffer.concat(chunks).toString(),
|
||||
host: req.headers.host,
|
||||
}),
|
||||
);
|
||||
});
|
||||
backend.on("connection", (socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
});
|
||||
backend.on("upgrade", (req, socket) => {
|
||||
socket.write(
|
||||
"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n",
|
||||
);
|
||||
});
|
||||
backend.listen(0, "127.0.0.1");
|
||||
await once(backend, "listening");
|
||||
const target = `http://127.0.0.1:${backend.address().port}`;
|
||||
const previous = process.env.WAKEY_DEV_API_TARGET;
|
||||
process.env.WAKEY_DEV_API_TARGET = target;
|
||||
let vite;
|
||||
try {
|
||||
vite = await createServer({
|
||||
root: fileURLToPath(new URL("../", import.meta.url)),
|
||||
server: { port: 0 },
|
||||
});
|
||||
await vite.listen();
|
||||
const origin = `http://127.0.0.1:${vite.httpServer.address().port}`;
|
||||
const response = await fetch(
|
||||
`${origin}/api/v1/control/fleet/devices/refresh?test=1`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: '{"agent_ids":[]}',
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), {
|
||||
method: "POST",
|
||||
url: "/api/v1/control/fleet/devices/refresh?test=1",
|
||||
body: '{"agent_ids":[]}',
|
||||
host: new URL(target).host,
|
||||
});
|
||||
assert.equal((await fetch(`${origin}/healthz`)).status, 200);
|
||||
const page = await fetch(`${origin}/ui/`);
|
||||
assert.equal(page.status, 200);
|
||||
assert.match(await page.text(), /@vite\/client/);
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = request(
|
||||
`${origin}/api/v1/control/terminals/events/ws?test=1`,
|
||||
{
|
||||
headers: {
|
||||
Connection: "Upgrade",
|
||||
Upgrade: "websocket",
|
||||
"Sec-WebSocket-Version": "13",
|
||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
||||
},
|
||||
},
|
||||
);
|
||||
req.on("upgrade", (res, socket) => {
|
||||
socket.destroy();
|
||||
resolve();
|
||||
});
|
||||
req.on("response", (res) => {
|
||||
res.resume();
|
||||
reject(new Error(`Expected WS upgrade, got ${res.statusCode}`));
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.setTimeout(5000, () =>
|
||||
req.destroy(new Error("WS upgrade timed out")),
|
||||
);
|
||||
req.end();
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.WAKEY_DEV_API_TARGET;
|
||||
else process.env.WAKEY_DEV_API_TARGET = previous;
|
||||
for (const socket of sockets) socket.destroy();
|
||||
if (vite) await vite.close();
|
||||
await new Promise((resolve) => backend.close(resolve));
|
||||
}
|
||||
});
|
||||
+48
-17
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -21,22 +21,53 @@ const ReactCompilerConfig = {
|
||||
runtimeModule: "react-compiler-runtime", // Redirects the missing specifier
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]],
|
||||
export default defineConfig(({ command, mode, isPreview }) => {
|
||||
const env = loadEnv(mode, __dirname, "WAKEY_DEV_");
|
||||
const target = env.WAKEY_DEV_API_TARGET || "http://127.0.0.1:8080";
|
||||
const dev = command === "serve" && !isPreview;
|
||||
if (dev) {
|
||||
const url = new URL(target);
|
||||
if (
|
||||
!["http:", "https:"].includes(url.protocol) ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.pathname !== "/" ||
|
||||
url.search ||
|
||||
url.hash
|
||||
) {
|
||||
throw new Error(
|
||||
"WAKEY_DEV_API_TARGET must be an HTTP(S) origin without credentials or a path",
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]],
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
base: "/ui/",
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
base: "/ui/",
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: debugEnabled(),
|
||||
},
|
||||
build: {
|
||||
sourcemap: debugEnabled(),
|
||||
},
|
||||
// Keep browser requests same-origin, including terminal/event WebSockets.
|
||||
// WAKEY_DEV_* is read only by this config, never exposed as VITE_* client data.
|
||||
server: dev
|
||||
? {
|
||||
host: "127.0.0.1",
|
||||
proxy: {
|
||||
"^/api/": { target, changeOrigin: true, ws: true },
|
||||
"^/healthz$": { target, changeOrigin: true },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user