@fudrouter/fsrouter 0.6.73 → 0.6.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/headroom/detect.js +97 -7
- package/dist/lib/headroom/process.js +136 -4
- package/dist/routes/headroom/extras/route.js +58 -0
- package/dist/routes/headroom/restart/route.js +33 -0
- package/package.json +1 -1
- package/public/assets/EndpointPageClient.BnlQVTIB.js +6 -0
- package/public/assets/{Loading.Bj-WGl4k.js → Loading.C1il44u0.js} +1 -1
- package/public/assets/{NoAuthProxyCard.qAv57b-0.js → NoAuthProxyCard.WHGGEUIp.js} +1 -1
- package/public/assets/index.COMgQZYj.css +1 -0
- package/public/assets/index.D4FwOBBU.js +89 -0
- package/public/assets/{page.Qkkv8roc.js → page.0Rt4rWPG.js} +1 -1
- package/public/assets/{page.Dx_GRRdT.js → page.1numFcXc.js} +1 -1
- package/public/assets/{page.vla5Mzki.js → page.1w9bTOPd.js} +1 -1
- package/public/assets/{page.CzlQTyAL.js → page.3ARdH6tE.js} +1 -1
- package/public/assets/{page.sOFOrxqq.js → page.A4gD6oiw.js} +1 -1
- package/public/assets/{page.D5gj98Oe.js → page.AhxgKhsm.js} +1 -1
- package/public/assets/{page.DUI8kqOY.js → page.B1og8_GD.js} +1 -1
- package/public/assets/{page.CssmrzTT.js → page.B5nk3H0f.js} +1 -1
- package/public/assets/{page.DX4Nno6M.js → page.BGDrqCcI.js} +1 -1
- package/public/assets/{page.DzZLqI5Q.js → page.BJIIZWfZ.js} +1 -1
- package/public/assets/{page.BdmJ3yY3.js → page.BdB6udZG.js} +1 -1
- package/public/assets/{page.Ir7KvKcF.js → page.BfGkHTDS.js} +1 -1
- package/public/assets/{page.CPZav1_c.js → page.Bfr4417O.js} +1 -1
- package/public/assets/{page.ChX1NqmC.js → page.BpcyRUKz.js} +1 -1
- package/public/assets/{page.O2ddJYoT.js → page.BvcbHe5b.js} +1 -1
- package/public/assets/{page.C72FTTJr.js → page.C6WsIDgW.js} +1 -1
- package/public/assets/{page.Dh5f0XJC.js → page.CCEJ5tPH.js} +1 -1
- package/public/assets/{page.DxMDTbSx.js → page.CMUifPIU.js} +1 -1
- package/public/assets/{page.1qjqvrwX.js → page.CR7Yees9.js} +1 -1
- package/public/assets/{page.CsxWmPoI.js → page.CXCEdPh6.js} +1 -1
- package/public/assets/{page.DPyOkPVo.js → page.CsroRewc.js} +1 -1
- package/public/assets/{page.CQlVGoXg.js → page.CtLa-V_T.js} +1 -1
- package/public/assets/{page.BnmTC5jG.js → page.DINPUA6N.js} +1 -1
- package/public/assets/page.DSEK9UW0.js +1 -0
- package/public/assets/{page.i4KDuWcM.js → page.DSTzvOQ8.js} +1 -1
- package/public/assets/{page.CSs3-gdt.js → page.DT7cpJ8d.js} +1 -1
- package/public/assets/{page.DVIHRbxB.js → page.DT_RnrSs.js} +1 -1
- package/public/assets/{page.Cj5h09Zo.js → page.DU7jwIPk.js} +1 -1
- package/public/assets/{page.DBhu4Arl.js → page.DcmbyZGV.js} +1 -1
- package/public/assets/{page.CbkENBd5.js → page.DjguJpCk.js} +1 -1
- package/public/assets/{page.CnTop8Zt.js → page.Dr6yAoTO.js} +1 -1
- package/public/assets/{page.CXd_R9yU.js → page.FR_Izkgi.js} +1 -1
- package/public/assets/{page.BSLJ4tx5.js → page.GhgvWftU.js} +1 -1
- package/public/assets/{page.BbO1pspY.js → page.UoGOSKkx.js} +1 -1
- package/public/assets/page.cAR8tR7Z.js +1 -0
- package/public/assets/{page.DibhF7f-.js → page.feKgaG20.js} +1 -1
- package/public/assets/{page.D1oKew1j.js → page.t4nKFefI.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/EndpointPageClient.LyNNvZ4z.js +0 -6
- package/public/assets/index.BuPfZebu.css +0 -1
- package/public/assets/index.CShjRV_B.js +0 -89
- package/public/assets/page.DD_czjjP.js +0 -1
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execFileSync, execSync } from "child_process";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
|
+
// Extras that improve headroom compression quality. `proxy` is the base;
|
|
5
|
+
// `code` adds tree-sitter AST compression; `ml` adds Kompress-v2 HF model.
|
|
6
|
+
// Other `[all]` extras (image, voice, otel, reports, evals, ...) are not
|
|
7
|
+
// useful for the 9router proxy use case, so we don't track them here.
|
|
8
|
+
export const HEADROOM_COMPRESSION_EXTRAS = ["code", "ml"];
|
|
9
|
+
|
|
10
|
+
// Marker packages that each extra pulls in. Detected from `pip list --format=json`
|
|
11
|
+
// so one call can answer both the installed version and active extras.
|
|
12
|
+
export const EXTRA_MARKERS = {
|
|
13
|
+
code: ["tree-sitter", "tree-sitter-language-pack"],
|
|
14
|
+
ml: ["torch", "huggingface-hub"],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const HEADROOM_PIP_TIMEOUT_MS = 8000;
|
|
18
|
+
|
|
4
19
|
const IS_WIN = process.platform === "win32";
|
|
5
20
|
const WHICH_CMD = IS_WIN ? "where" : "which";
|
|
6
21
|
|
|
@@ -49,8 +64,33 @@ export function findHeadroomBinary() {
|
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
// Find a Python interpreter >= 3.10 (headroom-ai requires it). Returns null if none.
|
|
67
|
+
// `python3`, `python3.13`, `python` can point at different envs on any OS. Prefer
|
|
68
|
+
// the interpreter that can also see the installed `headroom-ai` package so the
|
|
69
|
+
// dashboard probes and install action operate on the same interpreter as the CLI.
|
|
70
|
+
// Falls back to the first version-eligible candidate when headroom-ai is not yet
|
|
71
|
+
// installed anywhere (needed for the initial install).
|
|
72
|
+
// Interpreters to probe, most specific first: the python next to the headroom
|
|
73
|
+
// binary (guaranteed to have headroom-ai), then full paths from EXTRA_BINS, then
|
|
74
|
+
// bare names resolved via PATH.
|
|
75
|
+
function pythonCandidates() {
|
|
76
|
+
const list = [];
|
|
77
|
+
const bin = findHeadroomBinary();
|
|
78
|
+
if (bin) {
|
|
79
|
+
const dir = path.dirname(bin);
|
|
80
|
+
const names = IS_WIN ? ["python.exe", "python3.exe"] : ["python3", "python3.13", "python"];
|
|
81
|
+
for (const n of names) list.push(path.join(dir, n));
|
|
82
|
+
}
|
|
83
|
+
for (const dir of EXTRA_BINS) {
|
|
84
|
+
if (!dir) continue;
|
|
85
|
+
for (const n of PYTHON_CANDIDATES) list.push(path.join(dir, IS_WIN ? `${n}.exe` : n));
|
|
86
|
+
}
|
|
87
|
+
list.push(...PYTHON_CANDIDATES);
|
|
88
|
+
return list;
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
export function findPython310() {
|
|
53
|
-
|
|
92
|
+
let fallback = null;
|
|
93
|
+
for (const candidate of pythonCandidates()) {
|
|
54
94
|
try {
|
|
55
95
|
const ver = execSync(`${candidate} --version`, {
|
|
56
96
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -60,14 +100,24 @@ export function findPython310() {
|
|
|
60
100
|
const match = ver.match(/(\d+)\.(\d+)/);
|
|
61
101
|
if (!match) continue;
|
|
62
102
|
const [major, minor] = [parseInt(match[1], 10), parseInt(match[2], 10)];
|
|
63
|
-
if (major > MIN_VERSION[0] || (major === MIN_VERSION[0] && minor >= MIN_VERSION[1]))
|
|
103
|
+
if (!(major > MIN_VERSION[0] || (major === MIN_VERSION[0] && minor >= MIN_VERSION[1]))) continue;
|
|
104
|
+
if (!fallback) fallback = candidate;
|
|
105
|
+
try {
|
|
106
|
+
execFileSync(candidate, ["-m", "pip", "show", "headroom-ai"], {
|
|
107
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
timeout: HEADROOM_PIP_TIMEOUT_MS,
|
|
110
|
+
env: { ...process.env, PATH: EXTENDED_PATH },
|
|
111
|
+
});
|
|
64
112
|
return candidate;
|
|
113
|
+
} catch {
|
|
114
|
+
// Keep scanning until an interpreter that sees headroom-ai is found.
|
|
65
115
|
}
|
|
66
116
|
} catch {
|
|
67
117
|
// candidate not present, try next
|
|
68
118
|
}
|
|
69
119
|
}
|
|
70
|
-
return
|
|
120
|
+
return fallback;
|
|
71
121
|
}
|
|
72
122
|
|
|
73
123
|
// Probe whether a Headroom proxy is reachable at the given URL by hitting /health.
|
|
@@ -93,10 +143,50 @@ export function isLoopbackHeadroomUrl(url) {
|
|
|
93
143
|
|
|
94
144
|
// Aggregate status for the dashboard: installed, running, python interpreter.
|
|
95
145
|
export async function getHeadroomStatus(url) {
|
|
96
|
-
const
|
|
146
|
+
const path = findHeadroomBinary();
|
|
97
147
|
const python = findPython310();
|
|
98
|
-
const installed = Boolean(
|
|
148
|
+
const installed = Boolean(path);
|
|
99
149
|
const running = await probeProxyRunning(url);
|
|
100
150
|
const localUrl = isLoopbackHeadroomUrl(url);
|
|
101
|
-
|
|
151
|
+
const extrasStatus = installed ? getInstalledHeadroomExtras(python) : { installed: false, version: null, extras: { code: false, ml: false } };
|
|
152
|
+
return {
|
|
153
|
+
installed,
|
|
154
|
+
path,
|
|
155
|
+
running,
|
|
156
|
+
python,
|
|
157
|
+
localUrl,
|
|
158
|
+
canStart: installed && localUrl,
|
|
159
|
+
version: extrasStatus.version,
|
|
160
|
+
extras: extrasStatus.extras,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parse installed headroom-ai version + which compression extras are
|
|
165
|
+
// actually installed (detected via marker package presence). One `pip list`
|
|
166
|
+
// call is enough to answer both questions.
|
|
167
|
+
//
|
|
168
|
+
// Returns: { installed: bool, version: string|null, extras: { code, ml } }
|
|
169
|
+
export function getInstalledHeadroomExtras(python) {
|
|
170
|
+
const py = python || findPython310();
|
|
171
|
+
if (!py) return { installed: false, version: null, extras: { code: false, ml: false } };
|
|
172
|
+
try {
|
|
173
|
+
const out = execFileSync(py, ["-m", "pip", "list", "--format=json", "--disable-pip-version-check"], {
|
|
174
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
175
|
+
windowsHide: true,
|
|
176
|
+
timeout: HEADROOM_PIP_TIMEOUT_MS,
|
|
177
|
+
env: { ...process.env, PATH: EXTENDED_PATH },
|
|
178
|
+
}).toString();
|
|
179
|
+
const packages = JSON.parse(out);
|
|
180
|
+
const names = new Set(packages.map((p) => String(p.name || "").toLowerCase()));
|
|
181
|
+
const installed = names.has("headroom-ai");
|
|
182
|
+
if (!installed) return { installed: false, version: null, extras: { code: false, ml: false } };
|
|
183
|
+
const version = packages.find((p) => p.name?.toLowerCase() === "headroom-ai")?.version || null;
|
|
184
|
+
const extras = {};
|
|
185
|
+
for (const extra of HEADROOM_COMPRESSION_EXTRAS) {
|
|
186
|
+
extras[extra] = EXTRA_MARKERS[extra].some((m) => names.has(m));
|
|
187
|
+
}
|
|
188
|
+
return { installed: true, version, extras };
|
|
189
|
+
} catch {
|
|
190
|
+
return { installed: false, version: null, extras: { code: false, ml: false } };
|
|
191
|
+
}
|
|
102
192
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
|
-
import { DATA_DIR } from
|
|
5
|
-
import { findHeadroomBinary } from "./detect.js";
|
|
4
|
+
import { DATA_DIR } from '../../lib/dataDir.js';
|
|
5
|
+
import { findHeadroomBinary, findPython310, HEADROOM_COMPRESSION_EXTRAS, EXTRA_MARKERS, getInstalledHeadroomExtras } from "./detect.js";
|
|
6
6
|
|
|
7
7
|
const HEADROOM_DIR = path.join(DATA_DIR, "headroom");
|
|
8
8
|
const PID_FILE = path.join(HEADROOM_DIR, "proxy.pid");
|
|
9
9
|
const LOG_FILE = path.join(HEADROOM_DIR, "proxy.log");
|
|
10
|
+
const INSTALL_LOG_FILE = path.join(HEADROOM_DIR, "install.log");
|
|
10
11
|
const DEFAULT_PORT = 8787;
|
|
11
12
|
const STARTUP_TIMEOUT_MS = 8000;
|
|
12
13
|
|
|
@@ -41,7 +42,17 @@ export function getManagedPid() {
|
|
|
41
42
|
return pid && isPidAlive(pid) ? pid : null;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
// Build proxy CLI flags for the active compression extras. `[code]` (AST
|
|
46
|
+
// compression) is off by default in headroom → pass --code-aware to turn it on;
|
|
47
|
+
// `[ml]` (Kompress) is on by default → pass --disable-kompress to turn it off.
|
|
48
|
+
function extrasProxyArgs({ codeAware, kompress } = {}) {
|
|
49
|
+
const args = [];
|
|
50
|
+
if (codeAware) args.push("--code-aware");
|
|
51
|
+
if (kompress === false) args.push("--disable-kompress");
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function startHeadroomProxy({ port = DEFAULT_PORT, codeAware = false, kompress = true } = {}) {
|
|
45
56
|
const safePort = Number(port) > 0 && Number(port) < 65536 ? Number(port) : DEFAULT_PORT;
|
|
46
57
|
const binary = findHeadroomBinary();
|
|
47
58
|
if (!binary) {
|
|
@@ -57,7 +68,8 @@ export async function startHeadroomProxy({ port = DEFAULT_PORT } = {}) {
|
|
|
57
68
|
// spawn stdio requires fd numbers, not WriteStream objects.
|
|
58
69
|
const outFd = fs.openSync(LOG_FILE, "a");
|
|
59
70
|
|
|
60
|
-
const
|
|
71
|
+
const args = ["proxy", "--port", String(safePort), ...extrasProxyArgs({ codeAware, kompress })];
|
|
72
|
+
const child = spawn(binary, args, {
|
|
61
73
|
stdio: ["ignore", outFd, outFd],
|
|
62
74
|
detached: true,
|
|
63
75
|
windowsHide: true,
|
|
@@ -118,6 +130,25 @@ export function stopHeadroomProxy() {
|
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
// Stop the managed proxy (if any), wait for the pid to die, then start again
|
|
134
|
+
// with the given flags. Used when toggling active extras that require a restart.
|
|
135
|
+
export async function restartHeadroomProxy(opts = {}) {
|
|
136
|
+
const pid = getManagedPid();
|
|
137
|
+
if (pid) {
|
|
138
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* already gone */ }
|
|
139
|
+
// Wait up to ~3s for graceful exit, force-kill if still alive.
|
|
140
|
+
for (let i = 0; i < 30 && isPidAlive(pid); i++) {
|
|
141
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
142
|
+
}
|
|
143
|
+
if (isPidAlive(pid)) {
|
|
144
|
+
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
|
|
145
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
146
|
+
}
|
|
147
|
+
clearPid();
|
|
148
|
+
}
|
|
149
|
+
return startHeadroomProxy(opts);
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
export function getHeadroomLogTail(maxLines = 200) {
|
|
122
153
|
try {
|
|
123
154
|
if (!fs.existsSync(LOG_FILE)) return "";
|
|
@@ -126,3 +157,104 @@ export function getHeadroomLogTail(maxLines = 200) {
|
|
|
126
157
|
return lines.slice(-maxLines).join("\n");
|
|
127
158
|
} catch { return ""; }
|
|
128
159
|
}
|
|
160
|
+
|
|
161
|
+
// Install (or upgrade) headroom-ai with the requested compression extras.
|
|
162
|
+
// `extras` is a whitelist from HEADROOM_COMPRESSION_EXTRAS — anything else
|
|
163
|
+
// is rejected to keep the install surface predictable. Always installs the
|
|
164
|
+
// `proxy` base + whatever extras the user picked, regardless of what is
|
|
165
|
+
// already present.
|
|
166
|
+
export async function installHeadroomExtras(extras = []) {
|
|
167
|
+
const requested = Array.isArray(extras) ? extras.filter((e) => HEADROOM_COMPRESSION_EXTRAS.includes(e)) : [];
|
|
168
|
+
const py = findPython310();
|
|
169
|
+
if (!py) {
|
|
170
|
+
const err = new Error("Python >= 3.10 not found");
|
|
171
|
+
err.code = "NO_PYTHON";
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
if (!findHeadroomBinary()) {
|
|
175
|
+
const err = new Error("headroom-ai not installed (run `pip install headroom-ai[proxy]` first)");
|
|
176
|
+
err.code = "NOT_INSTALLED";
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
// pip install string is built from a closed set (HEADROOM_COMPRESSION_EXTRAS),
|
|
180
|
+
// so it cannot be poisoned by caller input — the comma-list is a fixed
|
|
181
|
+
// ['proxy', ...requested]. No shell interpolation.
|
|
182
|
+
const extrasList = ["proxy", ...requested].join(",");
|
|
183
|
+
const spec = `headroom-ai[${extrasList}]`;
|
|
184
|
+
const args = ["-m", "pip", "install", "--upgrade", spec];
|
|
185
|
+
|
|
186
|
+
ensureDir();
|
|
187
|
+
// Truncate ("w") so the log reflects only the current install for live progress.
|
|
188
|
+
const outFd = fs.openSync(INSTALL_LOG_FILE, "w");
|
|
189
|
+
const child = spawn(py, args, {
|
|
190
|
+
stdio: ["ignore", outFd, outFd],
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
env: { ...process.env },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
child.once("error", (e) => { fs.closeSync(outFd); reject(e); });
|
|
197
|
+
child.once("exit", (code) => {
|
|
198
|
+
fs.closeSync(outFd);
|
|
199
|
+
if (code === 0) {
|
|
200
|
+
const status = getInstalledHeadroomExtras(py);
|
|
201
|
+
resolve({ success: true, code, spec, extras: requested, ...status });
|
|
202
|
+
} else {
|
|
203
|
+
const err = new Error(`pip install exited with code=${code} — see headroom/install.log`);
|
|
204
|
+
err.code = "INSTALL_FAILED";
|
|
205
|
+
reject(err);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Uninstall the marker packages that back a single extra (e.g. `ml` → torch,
|
|
212
|
+
// huggingface-hub). `headroom-ai` base and the `proxy` extra are never removed.
|
|
213
|
+
export async function uninstallHeadroomExtras(extras = []) {
|
|
214
|
+
const requested = Array.isArray(extras) ? extras.filter((e) => HEADROOM_COMPRESSION_EXTRAS.includes(e)) : [];
|
|
215
|
+
const py = findPython310();
|
|
216
|
+
if (!py) {
|
|
217
|
+
const err = new Error("Python >= 3.10 not found");
|
|
218
|
+
err.code = "NO_PYTHON";
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
const pkgs = [...new Set(requested.flatMap((e) => EXTRA_MARKERS[e] || []))];
|
|
222
|
+
if (pkgs.length === 0) {
|
|
223
|
+
const err = new Error("No valid extras to remove");
|
|
224
|
+
err.code = "INVALID_EXTRAS";
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
const args = ["-m", "pip", "uninstall", "-y", ...pkgs];
|
|
228
|
+
|
|
229
|
+
ensureDir();
|
|
230
|
+
const outFd = fs.openSync(INSTALL_LOG_FILE, "w");
|
|
231
|
+
const child = spawn(py, args, {
|
|
232
|
+
stdio: ["ignore", outFd, outFd],
|
|
233
|
+
windowsHide: true,
|
|
234
|
+
env: { ...process.env },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
child.once("error", (e) => { fs.closeSync(outFd); reject(e); });
|
|
239
|
+
child.once("exit", (code) => {
|
|
240
|
+
fs.closeSync(outFd);
|
|
241
|
+
if (code === 0) {
|
|
242
|
+
const status = getInstalledHeadroomExtras(py);
|
|
243
|
+
resolve({ success: true, code, removed: pkgs, extras: requested, ...status });
|
|
244
|
+
} else {
|
|
245
|
+
const err = new Error(`pip uninstall exited with code=${code} — see headroom/install.log`);
|
|
246
|
+
err.code = "UNINSTALL_FAILED";
|
|
247
|
+
reject(err);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Read the tail of the install/uninstall log for live progress in the UI.
|
|
254
|
+
export function getInstallLogTail(maxLines = 15) {
|
|
255
|
+
try {
|
|
256
|
+
if (!fs.existsSync(INSTALL_LOG_FILE)) return "";
|
|
257
|
+
const lines = fs.readFileSync(INSTALL_LOG_FILE, "utf8").split(/\r?\n/).filter(Boolean);
|
|
258
|
+
return lines.slice(-maxLines).join("\n");
|
|
259
|
+
} catch { return ""; }
|
|
260
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { findPython310, getInstalledHeadroomExtras, getHeadroomLogTail } from "../../../lib/headroom/detect.js";
|
|
2
|
+
import { installHeadroomExtras, uninstallHeadroomExtras } from "../../../lib/headroom/process.js";
|
|
3
|
+
export async function GET(req, res) {
|
|
4
|
+
try {
|
|
5
|
+
if (new URL(req.url, "http://x").searchParams.get("log") === "1") {
|
|
6
|
+
const log = getHeadroomLogTail(15);
|
|
7
|
+
return res.json({ log });
|
|
8
|
+
}
|
|
9
|
+
const python = findPython310();
|
|
10
|
+
const status = getInstalledHeadroomExtras(python);
|
|
11
|
+
return res.json(status);
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
return res.status(500).json({ error: e.message });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function POST(req, res) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await req.json().catch(() => ({}));
|
|
20
|
+
const extras = Array.isArray(body.extras) ? body.extras : [];
|
|
21
|
+
if (extras.length === 0) {
|
|
22
|
+
return res.status(400).json({ error: "No extras requested" });
|
|
23
|
+
}
|
|
24
|
+
const python = findPython310();
|
|
25
|
+
if (!python) {
|
|
26
|
+
return res.status(400).json({ error: "NO_PYTHON", message: "Python >= 3.10 required" });
|
|
27
|
+
}
|
|
28
|
+
const result = await installHeadroomExtras(extras);
|
|
29
|
+
return res.json(result);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (e.code === "INVALID_EXTRAS" || e.code === "NOT_INSTALLED") {
|
|
33
|
+
return res.status(400).json({ error: e.code, message: e.message });
|
|
34
|
+
}
|
|
35
|
+
return res.status(500).json({ error: e.message });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function DELETE(req, res) {
|
|
39
|
+
try {
|
|
40
|
+
const body = await req.json().catch(() => ({}));
|
|
41
|
+
const extras = Array.isArray(body.extras) ? body.extras : [];
|
|
42
|
+
if (extras.length === 0) {
|
|
43
|
+
return res.status(400).json({ error: "No extras requested" });
|
|
44
|
+
}
|
|
45
|
+
const python = findPython310();
|
|
46
|
+
if (!python) {
|
|
47
|
+
return res.status(400).json({ error: "NO_PYTHON", message: "Python >= 3.10 required" });
|
|
48
|
+
}
|
|
49
|
+
const result = await uninstallHeadroomExtras(extras);
|
|
50
|
+
return res.json(result);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e.code === "INVALID_EXTRAS" || e.code === "NOT_INSTALLED") {
|
|
54
|
+
return res.status(400).json({ error: e.code, message: e.message });
|
|
55
|
+
}
|
|
56
|
+
return res.status(500).json({ error: e.message });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSettings } from "../../../lib/db/repos/settingsRepo.js";
|
|
2
|
+
import { DEFAULT_HEADROOM_URL, isLoopbackHeadroomUrl } from "../../../lib/headroom/detect.js";
|
|
3
|
+
import { restartHeadroomProxy } from "../../../lib/headroom/process.js";
|
|
4
|
+
function parsePortFromUrl(url) {
|
|
5
|
+
try {
|
|
6
|
+
const u = new URL(url);
|
|
7
|
+
const p = parseInt(u.port, 10);
|
|
8
|
+
if (p > 0 && p < 65536)
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export async function POST(req, res) {
|
|
15
|
+
try {
|
|
16
|
+
const settings = await getSettings();
|
|
17
|
+
const url = settings.headroomUrl || DEFAULT_HEADROOM_URL;
|
|
18
|
+
if (!isLoopbackHeadroomUrl(url)) {
|
|
19
|
+
return res.status(400).json({ error: "External Headroom proxies must be started outside AMRouter", code: "EXTERNAL_PROXY" });
|
|
20
|
+
}
|
|
21
|
+
const port = parsePortFromUrl(url) || 8787;
|
|
22
|
+
const result = await restartHeadroomProxy({
|
|
23
|
+
port,
|
|
24
|
+
codeAware: settings.headroomCodeAware === true,
|
|
25
|
+
kompress: settings.headroomKompress !== false,
|
|
26
|
+
});
|
|
27
|
+
return res.json({ success: true, ...result });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const status = error.code === "NOT_INSTALLED" ? 400 : 500;
|
|
31
|
+
return res.status(status).json({ error: error.message, code: error.code || null });
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{r as ct,v as rs,u as is,j as e,B as c,M as $,C as os,f as cs}from"./index.D4FwOBBU.js";import{b as s}from"./router.Cjj1--8t.js";import{C as dt,I as te,T as Me}from"./NoAuthProxyCard.WHGGEUIp.js";import{C as ut}from"./Loading.C1il44u0.js";const ds=["zh-CN","zh-TW"],us=[{icon:"public",title:"Access Anywhere",desc:"Use your API from any network"},{icon:"group",title:"Share Endpoint",desc:"Share URL with team members"},{icon:"code",title:"Use in Cursor/Cline",desc:"Connect AI tools remotely"},{icon:"lock",title:"Encrypted",desc:"End-to-end TLS via Cloudflare"}],He=2e3,mt=3e5,ms=5e3,Fe=5,hs=1e4,xs=5e3;async function ht(h){if(!h)return!1;try{return(await fetch(`${h}/api/health`,{mode:"cors",cache:"no-store",signal:AbortSignal.timeout(xs)})).ok}catch{return!1}}async function ps(...h){const d=h.filter(Boolean).map(ht);return d.length?new Promise(x=>{let j=d.length;d.forEach(U=>U.then(b=>{b?x(!0):--j===0&&x(!1)}))}):!1}const Ke=[{id:"lite",label:"Lite",desc:"Drop filler, keep grammar"},{id:"full",label:"Full",desc:"Drop articles, fragments OK"},{id:"ultra",label:"Ultra",desc:"Telegraphic, max compression"},{id:"wenyan-lite",label:"文 Lite",desc:"Classical Chinese, light compression",wenyan:!0},{id:"wenyan",label:"文 Full",desc:"Maximum 文言文, 80-90% reduction",wenyan:!0},{id:"wenyan-ultra",label:"文 Ultra",desc:"Extreme classical compression",wenyan:!0}];function fs({machineId:h}){const[d,x]=s.useState([]),[j,U]=s.useState(!0),[b,N]=s.useState(!1),[se,ae]=s.useState(""),[je,we]=s.useState(null),[w,z]=s.useState(null),[ne,We]=s.useState(!1),[B,xt]=s.useState(!0),[qe,pt]=s.useState(!0),[$e,ze]=s.useState(!1),[js,ft]=s.useState(!0),[ws,gt]=s.useState(!1),[Be,Ve]=s.useState("full"),[vs,bt]=s.useState(!1),[Je,Ge]=s.useState(""),[ks,yt]=s.useState(!1),[L,ve]=s.useState({installed:!1,running:!1,python:null,loading:!0}),[Ns,Cs]=s.useState(!1),[Ts,le]=s.useState(!1),[Ss,Xe]=s.useState(""),[Es,jt]=s.useState(!1),[Ps,wt]=s.useState("full"),[_s,vt]=s.useState(!1),[As,kt]=s.useState("full"),[Nt,Ye]=s.useState("en"),[Ct,ke]=s.useState(!0),[p,re]=s.useState(!1),[R,ie]=s.useState(!1),[I,oe]=s.useState(""),[D,Ne]=s.useState(""),[P,C]=s.useState(!1),[Tt,T]=s.useState(""),[ce,v]=s.useState(null),[St,V]=s.useState(!1),[Et,J]=s.useState(!1),[f,O]=s.useState(!1),[M,de]=s.useState(!1),[H,F]=s.useState(""),[_,G]=s.useState(!1),[Pt,A]=s.useState(""),[K,u]=s.useState(null),[Qe,Ze]=s.useState(""),[_t,et]=s.useState(""),[Ce,ue]=s.useState(null),[me,Te]=s.useState(!1),[Se,Ee]=s.useState([]),[At,tt]=s.useState(""),[Ut,Pe]=s.useState(!1),[Lt,W]=s.useState(!1),[Rt,X]=s.useState(!1),he=s.useRef(null),Y=s.useRef(0),Q=s.useRef(0),xe=s.useRef(!1),pe=s.useRef(!1),fe=s.useRef(!1),ge=s.useRef(!1),[It,_e]=s.useState(!1),[Dt,Ae]=s.useState(!1),[Ue,st]=s.useState(new Set),[Ot,Mt]=s.useState(!1);s.useEffect(()=>{typeof window<"u"&&Mt(!["localhost","127.0.0.1","::1"].includes(window.location.hostname))},[]),s.useEffect(()=>(Ye(ct()),rs(()=>Ye(ct()))),[]);const Le=ds.includes(Nt);Le||Ke.filter(t=>!t.wenyan),s.useEffect(()=>{const t=Ke.find(a=>a.id===Be);t!=null&&t.wenyan&&!Le&&(Ve("ultra"),$t({cavemanLevel:"ultra"}))},[Le,Be]);const{copied:q,copy:Z}=is(),Ht=!B||!qe,Ft=B?"Change the default dashboard password before activating the tunnel.":'Enable "Require login" and set a custom password before activating the tunnel.';s.useEffect(()=>{he.current&&(he.current.scrollTop=he.current.scrollHeight)},[Se]),s.useEffect(()=>{nt(),Kt()},[]),s.useEffect(()=>{if(!(p||f))return;const n=(!p||R)&&(!f||M),r=()=>{document.hidden||at()};if(document.addEventListener("visibilitychange",r),n)return()=>document.removeEventListener("visibilitychange",r);const o=setInterval(()=>{document.hidden||at()},ms);return()=>{clearInterval(o),document.removeEventListener("visibilitychange",r)}},[p,f,R,M]),s.useEffect(()=>{const t=async()=>{if(!document.hidden){if(p&&(I||D)){const o=await ps(D,I);xe.current=o,o?(Y.current=0,ie(!0),fe.current||(fe.current=!0,_e(!0))):(Y.current+=1,Y.current>=Fe&&ie(!1))}else xe.current=!1;if(f&&H){const o=await ht(H);pe.current=o,o?(Q.current=0,de(!0),ge.current||(ge.current=!0,Ae(!0))):(Q.current+=1,Q.current>=Fe&&de(!1))}else pe.current=!1}};if(!(p&&(I||D)||f&&H)||(t(),(!p||R)&&(!f||M)))return;const r=setInterval(t,hs);return()=>clearInterval(r)},[p,I,D,f,H,R,M]);const be=s.useCallback((t,a,l,n,r,o)=>{a.current?(l.current=0,n(!0),r.current||(r.current=!0,o(!0))):(l.current+=1,l.current>=Fe&&n(!1))},[]),at=async()=>{var t,a,l,n,r,o,m;try{const y=await fetch("/api/tunnel/status",{cache:"no-store"});if(!y.ok)return;const g=await y.json(),i=((t=g.tunnel)==null?void 0:t.settingsEnabled)??((a=g.tunnel)==null?void 0:a.enabled)??!1,E=((l=g.tunnel)==null?void 0:l.tunnelUrl)||"";oe(E),Ne(((n=g.tunnel)==null?void 0:n.publicUrl)||""),re(i),be(null,xe,Y,ie,fe,_e);const k=((r=g.tailscale)==null?void 0:r.settingsEnabled)??((o=g.tailscale)==null?void 0:o.enabled)??!1,Oe=((m=g.tailscale)==null?void 0:m.tunnelUrl)||"";F(Oe),O(k),be(null,pe,Q,de,ge,Ae)}catch{}},Kt=async()=>{var t,a,l,n,r,o,m;ke(!0);try{const[y,g]=await Promise.all([fetch("/api/settings"),fetch("/api/tunnel/status",{cache:"no-store"})]);if(y.ok){const i=await y.json();We(i.requireApiKey||!1),xt(i.requireLogin!==!1),pt(i.hasPassword||!1),ze(i.tunnelDashboardAccess||!1),ft(i.rtkEnabled!==!1),gt(!!i.cavemanEnabled),Ve(i.cavemanLevel||"full"),bt(!!i.headroomEnabled),Ge(i.headroomUrl||""),yt(!!i.headroomCompressUserMessages),jt(!!i.ponytailEnabled),wt(i.ponytailLevel||"full"),vt(!!i.paleoEnabled),kt(i.paleoLevel||"full"),ee()}if(g.ok){const i=await g.json(),E=((t=i.tunnel)==null?void 0:t.settingsEnabled)??((a=i.tunnel)==null?void 0:a.enabled)??!1,k=((l=i.tunnel)==null?void 0:l.tunnelUrl)||"";oe(k),Ne(((n=i.tunnel)==null?void 0:n.publicUrl)||""),re(E),be(null,xe,Y,ie,fe,_e);const Oe=((r=i.tailscale)==null?void 0:r.settingsEnabled)??((o=i.tailscale)==null?void 0:o.enabled)??!1,ls=((m=i.tailscale)==null?void 0:m.tunnelUrl)||"";F(ls),O(Oe),be(null,pe,Q,de,ge,Ae)}}catch(y){console.log("Error loading settings:",y)}finally{ke(!1)}},Wt=async t=>{try{(await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({tunnelDashboardAccess:t})})).ok&&ze(t)}catch(a){console.log("Error updating tunnelDashboardAccess:",a)}},qt=async t=>{try{(await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({requireApiKey:t})})).ok&&We(t)}catch(a){console.log("Error updating requireApiKey:",a)}},$t=async t=>{try{await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(a){console.log("Error updating setting:",a)}},ee=s.useCallback(async()=>{ve(t=>({...t,loading:!0}));try{const a=await(await fetch("/api/headroom/status",{headers:{"Cache-Control":"no-store"}})).json();ve({...a,loading:!1}),a.url&&!Je&&Ge(a.url)}catch{ve({installed:!1,running:!1,python:null,loading:!1})}},[Je]);s.useCallback(async()=>{Xe(""),le(!0);try{const t=await fetch("/api/headroom/start",{method:"POST"}),a=await t.json().catch(()=>({}));if(!t.ok)throw new Error(a.error||"Failed to start proxy");await ee()}catch(t){Xe(t.message)}finally{le(!1)}},[ee]),s.useCallback(async()=>{le(!0);try{await fetch("/api/headroom/stop",{method:"POST"}),await ee()}finally{le(!1)}},[ee]);const nt=async()=>{try{const t=await fetch("/api/keys"),a=await t.json();t.ok&&x(a.keys||[])}catch(t){console.log("Error fetching data:",t)}finally{U(!1)}},zt=async(...t)=>{var n;C(!0),T("Waiting for tunnel ready...");const a=t.filter(Boolean).map(r=>`${r}/api/health`),l=Date.now();for(;Date.now()-l<mt;){if(await new Promise(o=>setTimeout(o,He)),await Promise.any(a.map(async o=>{if((await fetch(o,{mode:"cors",cache:"no-store"})).ok)return!0;throw new Error("not ready")})).catch(()=>!1))return re(!0),C(!1),T(""),!0;if((Date.now()-l)%1e4<He)try{const o=await fetch("/api/tunnel/status");if(o.ok&&!((n=(await o.json()).tunnel)!=null&&n.enabled))return v({type:"error",message:"Tunnel process stopped unexpectedly."}),C(!1),T(""),!1}catch{}}return v({type:"error",message:"Tunnel created but not reachable. Please try again."}),C(!1),T(""),!1},Bt=async()=>{V(!1),C(!0),v(null),T("Creating tunnel...");let t=!0;(async()=>{var l;for(;t;){try{const n=await fetch("/api/tunnel/status");if(n.ok){const r=await n.json();(l=r.download)!=null&&l.downloading?T(`Downloading cloudflared... ${r.download.progress}%`):t&&T("Creating tunnel...")}}catch{}await new Promise(n=>setTimeout(n,1e3))}})();try{const l=await fetch("/api/tunnel/enable",{method:"POST"});t=!1;const n=await l.json();if(!l.ok){v({type:"error",message:n.error||"Failed to enable tunnel"});return}const r=n.tunnelUrl;if(!r){v({type:"error",message:"No tunnel URL returned"});return}oe(r),Ne(n.publicUrl||""),await zt(n.publicUrl,r)}catch(l){v({type:"error",message:l.message})}finally{t=!1,C(!1),T("")}},Vt=async()=>{C(!0),v(null);try{const t=await fetch("/api/tunnel/disable",{method:"POST"}),a=await t.json();t.ok?(re(!1),oe(""),J(!1),v({type:"success",message:"Tunnel disabled"})):v({type:"error",message:a.error||"Failed to disable tunnel"})}catch(t){v({type:"error",message:t.message})}finally{C(!1)}},Jt=async()=>{ue(null);try{const t=await fetch("/api/tunnel/tailscale-check");if(t.ok){const a=await t.json();return ue(a.installed),a}}catch{}return ue(!1),{installed:!1}},Gt=async()=>{Te(!0),u(null),Ee([]);try{const t=await fetch("/api/tunnel/tailscale-install",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sudoPassword:At})});tt("");const a=t.body.getReader(),l=new TextDecoder;let n="";for(;;){const{done:r,value:o}=await a.read();if(r)break;n+=l.decode(o,{stream:!0});const m=n.split(`
|
|
2
|
+
|
|
3
|
+
`);n=m.pop()||"";for(const y of m){const g=y.split(`
|
|
4
|
+
`);let i="progress",E=null;for(const k of g)if(k.startsWith("event: ")&&(i=k.slice(7).trim()),k.startsWith("data: "))try{E=JSON.parse(k.slice(6))}catch{}if(E)if(i==="progress")Ee(k=>[...k.slice(-50),E.message]);else if(i==="done"){ue(!0),Te(!1),W(!1),Ie();return}else i==="error"&&u({type:"error",message:E.error||"Install failed"})}}}catch(t){u({type:"error",message:t.message})}finally{Te(!1)}},Re=async t=>{A("Waiting for Tailscale ready...");const a=`${t}/api/health`,l=Date.now();for(;Date.now()-l<mt;){await new Promise(n=>setTimeout(n,He));try{const n=await fetch(a,{mode:"no-cors",cache:"no-store"});if(n.ok||n.type==="opaque")return!0}catch{}}return!1},lt=(t,a)=>{Ze(t),et(a)},S=()=>{Ze(""),et("")},Ie=async()=>{W(!1),Pe(!0),G(!0),u(null),A("Connecting..."),S();try{const t=await fetch("/api/tunnel/tailscale-enable",{method:"POST"}),a=await t.json();if(t.ok&&a.success){F(a.tunnelUrl||"");const l=await Re(a.tunnelUrl);O(!0),u(l?null:{type:"warning",message:"Connected but not reachable yet."});return}if(a.needsLogin&&a.authUrl){lt(a.authUrl,"Open Login Page"),A('Login required — click "Open Login Page" to continue');for(let l=0;l<40;l++){await new Promise(n=>setTimeout(n,3e3));try{const n=await fetch("/api/tunnel/tailscale-check");if(n.ok&&(await n.json()).loggedIn){S(),A("Starting funnel...");const o=await fetch("/api/tunnel/tailscale-enable",{method:"POST"}),m=await o.json();if(o.ok&&m.success){F(m.tunnelUrl||"");const y=await Re(m.tunnelUrl);O(!0),u(y?null:{type:"warning",message:"Connected but not reachable yet."})}else m.funnelNotEnabled&&m.enableUrl?await rt(m.enableUrl):u({type:"error",message:m.error||"Failed to start funnel"});return}}catch{}}S(),u({type:"error",message:"Login timed out. Please try again."});return}if(a.funnelNotEnabled&&a.enableUrl){await rt(a.enableUrl);return}u({type:"error",message:a.error||"Failed to connect"})}catch(t){u({type:"error",message:t.message})}finally{G(!1),Pe(!1),A(""),S()}},rt=async t=>{lt(t,"Open Funnel Settings"),A('Click "Open Funnel Settings" to enable Funnel...');for(let a=0;a<40;a++){await new Promise(l=>setTimeout(l,3e3));try{const l=await fetch("/api/tunnel/tailscale-enable",{method:"POST"}),n=await l.json();if(l.ok&&n.success){S(),F(n.tunnelUrl||"");const r=await Re(n.tunnelUrl);O(!0),u(r?null:{type:"warning",message:"Connected but not reachable yet."});return}if(n.funnelNotEnabled)continue;if(n.error){S(),u({type:"error",message:n.error});return}}catch{}}S(),u({type:"error",message:"Timed out waiting for Funnel to be enabled."})},Xt=async()=>{G(!0),u(null);try{const t=await fetch("/api/tunnel/tailscale-disable",{method:"POST"}),a=await t.json();t.ok?(O(!1),F(""),X(!1),u({type:"success",message:"Tailscale disabled"})):u({type:"error",message:a.error||"Failed to disable Tailscale"})}catch(t){u({type:"error",message:t.message})}finally{G(!1)}},it=async()=>{u(null),Ee([]);const t=await Jt();t!=null&&t.installed&&(t!=null&&t.hasCachedPassword)?Ie():W(!0)},Yt=async()=>{if(se.trim())try{const t=await fetch("/api/keys",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:se})}),a=await t.json();t.ok&&(we(a.key),await nt(),ae(""),N(!1))}catch(t){console.log("Error creating key:",t)}},Qt=async t=>{z({title:"Delete API Key",message:"Delete this API key?",onConfirm:async()=>{z(null);try{(await fetch(`/api/keys/${t}`,{method:"DELETE"})).ok&&(x(d.filter(l=>l.id!==t)),st(l=>{const n=new Set(l);return n.delete(t),n}))}catch(a){console.log("Error deleting key:",a)}}})},ot=async(t,a)=>{try{(await fetch(`/api/keys/${t}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({isActive:a})})).ok&&x(n=>n.map(r=>r.id===t?{...r,isActive:a}:r))}catch(l){console.log("Error toggling key:",l)}},Zt=t=>t?t.length>8?t.slice(0,8)+"...":t:"",es=t=>{st(a=>{const l=new Set(a);return l.has(t)?l.delete(t):l.add(t),l})},[ts,ss]=s.useState("/v1");if(s.useEffect(()=>{typeof window<"u"&&ss(`${window.location.origin}/v1`)},[]),j)return e.jsxs("div",{className:"flex flex-col gap-8",children:[e.jsx(ut,{}),e.jsx(ut,{})]});const as=ts,ns=!!L.running,De=L.localUrl!==!1;return L.canStart,De&&L.managedPid,L.loading||ns||De&&L.installed,e.jsxs("div",{className:"flex flex-col gap-8",children:[e.jsxs(dt,{children:[e.jsxs("h2",{className:"text-lg font-semibold mb-4 flex items-center gap-2",children:[e.jsx("span",{className:"material-symbols-outlined text-primary",children:"api"}),"API Endpoint"]}),e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx(gs,{label:"Local",url:as,copyId:"local_url",copied:q,onCopy:Z}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${p?"bg-primary/10 text-primary":"bg-surface-2 text-text-muted"}`,children:"Tunnel"}),p&&!P&&R?e.jsxs(e.Fragment,{children:[e.jsx(te,{value:`${D||I}/v1`,readOnly:!0,className:"flex-1 font-mono text-sm"}),e.jsx("button",{onClick:()=>Z(`${D||I}/v1`,"tunnel_url"),className:"p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:q==="tunnel_url"?"check":"content_copy"})}),e.jsx("button",{onClick:()=>J(!0),className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Disable Tunnel",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):p&&!P&&!R?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-amber-300 dark:border-amber-800 bg-amber-500/5 text-sm text-amber-600 dark:text-amber-400",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),It?"Tunnel reconnecting...":"Tunnel checking..."]}),e.jsx("button",{onClick:()=>J(!0),className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Disable Tunnel",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):P?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),Tt||"Creating tunnel..."]}),e.jsx("button",{onClick:()=>{C(!1),T("")},className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Stop",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):(ce==null?void 0:ce.type)==="error"?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400",children:[e.jsx("span",{className:"material-symbols-outlined text-sm",children:"error"}),ce.message]}),e.jsx(c,{size:"sm",icon:"cloud_upload",onClick:()=>V(!0),children:"Enable"})]}):Ct?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),"Checking..."]}),e.jsx("button",{onClick:()=>ke(!1),className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Stop",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):e.jsx(c,{size:"sm",icon:"cloud_upload",onClick:()=>{V(!0)},children:"Enable"})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${f?"bg-primary/10 text-primary":"bg-surface-2 text-text-muted"}`,children:"Tailscale"}),f&&!_&&M?e.jsxs(e.Fragment,{children:[e.jsx(te,{value:`${H}/v1`,readOnly:!0,className:"flex-1 font-mono text-sm"}),e.jsx("button",{onClick:()=>Z(`${H}/v1`,"ts_url"),className:"p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:q==="ts_url"?"check":"content_copy"})}),e.jsx("button",{onClick:()=>X(!0),className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Disable Tailscale",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):f&&!_&&!M?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-amber-300 dark:border-amber-800 bg-amber-500/5 text-sm text-amber-600 dark:text-amber-400",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),Dt?"Tailscale reconnecting...":"Tailscale checking..."]}),e.jsx("button",{onClick:()=>X(!0),className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Disable Tailscale",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):_||Ut?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),Pt||"Connecting..."]}),Qe&&e.jsx(c,{size:"sm",icon:"open_in_new",onClick:()=>window.open(Qe,"tailscale_auth","width=600,height=700,noopener,noreferrer"),children:_t||"Open"}),e.jsx("button",{onClick:()=>{G(!1),Pe(!1),A(""),S()},className:"p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0",title:"Stop",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"power_settings_new"})})]}):(K==null?void 0:K.type)==="error"?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400",children:[e.jsx("span",{className:"material-symbols-outlined text-sm",children:"error"}),K.message]}),e.jsx(c,{size:"sm",icon:"vpn_lock",onClick:it,children:"Enable"})]}):e.jsx(c,{size:"sm",icon:"vpn_lock",onClick:()=>{it()},className:"bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!",children:"Enable"})]})]}),Ht&&!p&&!f&&e.jsx("div",{className:"mt-4",children:e.jsx(ye,{message:Ft,action:{label:"Open settings",href:"/dashboard/profile"}})}),(p||f)&&e.jsxs("div",{className:"mt-4 flex flex-col gap-2",children:[!ne&&e.jsx(ye,{message:"Require API key is disabled — your endpoint is publicly accessible without authentication.",action:{label:"Enable",href:"#require-api-key"}}),(!B||!qe)&&e.jsx(ye,{message:B?"Dashboard uses the default password — change it in Profile settings.":"Require login is disabled — anyone can access your dashboard via tunnel.",action:{label:B?"Change password":"Enable",href:"/dashboard/profile"}})]}),(p||f)&&e.jsxs("div",{className:"mt-4 pt-4 border-t border-border flex items-center gap-3",children:[e.jsx(Me,{checked:$e,onChange:()=>Wt(!$e)}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("p",{className:"font-medium text-sm",children:"Allow dashboard access via tunnel"}),e.jsx(ys,{text:"When enabled, the dashboard can be accessed through your tunnel or Tailscale URL (login still required). When disabled, dashboard access via tunnel/Tailscale is completely blocked."})]})]})]}),e.jsxs(dt,{id:"require-api-key",children:[e.jsxs("div",{className:"flex items-center justify-between mb-4",children:[e.jsxs("h2",{className:"text-lg font-semibold flex items-center gap-2",children:[e.jsx("span",{className:"material-symbols-outlined text-primary",children:"vpn_key"}),"API Keys"]}),e.jsx(c,{icon:"add",onClick:()=>N(!0),children:"Create Key"})]}),e.jsxs("div",{className:"flex items-center justify-between pb-4 mb-4 border-b border-border",children:[e.jsxs("div",{children:[e.jsx("p",{className:"font-medium",children:"Require API key"}),e.jsx("p",{className:"text-sm text-text-muted",children:"Requests without a valid key will be rejected"})]}),e.jsx(Me,{checked:ne,onChange:()=>qt(!ne)})]}),Ot&&!ne&&e.jsx("div",{className:"mb-4 -mt-2",children:e.jsx(ye,{message:"Endpoint is exposed without an API key."})}),d.length===0?e.jsxs("div",{className:"text-center py-12",children:[e.jsx("div",{className:"inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4",children:e.jsx("span",{className:"material-symbols-outlined text-[32px]",children:"vpn_key"})}),e.jsx("p",{className:"text-text-main font-medium mb-1",children:"No API keys yet"}),e.jsx("p",{className:"text-sm text-text-muted mb-4",children:"Create your first API key to get started"}),e.jsx(c,{icon:"add",onClick:()=>N(!0),children:"Create Key"})]}):e.jsx("div",{className:"flex flex-col",children:d.map(t=>e.jsxs("div",{className:`group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0 ${t.isActive===!1?"opacity-60":""}`,children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("p",{className:"text-sm font-medium",children:t.name}),e.jsxs("div",{className:"flex items-center gap-2 mt-1",children:[e.jsx("code",{className:"text-xs text-text-muted font-mono",children:Ue.has(t.id)?t.key:Zt(t.key)}),e.jsx("button",{onClick:()=>es(t.id),className:"p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all",title:Ue.has(t.id)?"Hide key":"Show key",children:e.jsx("span",{className:"material-symbols-outlined text-[14px]",children:Ue.has(t.id)?"visibility_off":"visibility"})}),e.jsx("button",{onClick:()=>Z(t.key,t.id),className:"p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all",children:e.jsx("span",{className:"material-symbols-outlined text-[14px]",children:q===t.id?"check":"content_copy"})})]}),e.jsxs("p",{className:"text-xs text-text-muted mt-1",children:["Created ",new Date(t.createdAt).toLocaleDateString()]}),t.isActive===!1&&e.jsx("p",{className:"text-xs text-orange-500 mt-1",children:"Paused"})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(Me,{size:"sm",checked:t.isActive??!0,onChange:a=>{t.isActive&&!a?z({title:"Pause API Key",message:`Pause API key "${t.name}"?
|
|
5
|
+
|
|
6
|
+
This key will stop working immediately but can be resumed later.`,onConfirm:async()=>{z(null),ot(t.id,a)}}):ot(t.id,a)},title:t.isActive?"Pause key":"Resume key"}),e.jsx("button",{onClick:()=>Qt(t.id),className:"p-2 hover:bg-red-500/10 rounded text-red-500 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:"delete"})})]})]},t.id))})]}),e.jsx($,{isOpen:b,title:"Create API Key",onClose:()=>{N(!1),ae("")},children:e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx(te,{label:"Key Name",value:se,onChange:t=>ae(t.target.value),placeholder:"Production Key"}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(c,{onClick:Yt,fullWidth:!0,disabled:!se.trim(),children:"Create"}),e.jsx(c,{onClick:()=>{N(!1),ae("")},variant:"ghost",fullWidth:!0,children:"Cancel"})]})]})}),e.jsx($,{isOpen:!!je,title:"API Key Created",onClose:()=>we(null),children:e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4",children:[e.jsx("p",{className:"text-sm text-yellow-800 dark:text-yellow-200 mb-2 font-medium",children:"Save this key now!"}),e.jsx("p",{className:"text-sm text-yellow-700 dark:text-yellow-300",children:"This is the only time you will see this key. Store it securely."})]}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(te,{value:je||"",readOnly:!0,className:"flex-1 font-mono text-sm"}),e.jsx(c,{variant:"secondary",icon:q==="created_key"?"check":"content_copy",onClick:()=>Z(je,"created_key"),children:q==="created_key"?"Copied!":"Copy"})]}),e.jsx(c,{onClick:()=>we(null),fullWidth:!0,children:"Done"})]})}),e.jsx($,{isOpen:St,title:"Enable Tunnel",onClose:()=>V(!1),children:e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx("div",{className:"bg-surface-2 border border-border-subtle rounded-lg p-4",children:e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx("span",{className:"material-symbols-outlined text-primary",children:"cloud_upload"}),e.jsxs("div",{children:[e.jsx("p",{className:"text-sm text-text-main font-medium mb-1",children:"Cloudflare Tunnel"}),e.jsx("p",{className:"text-sm text-text-muted",children:"Expose your local FSRouter to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere."})]})]})}),e.jsx("div",{className:"grid grid-cols-2 gap-3",children:us.map(t=>e.jsxs("div",{className:"flex flex-col items-center text-center p-3 rounded-lg bg-sidebar/50",children:[e.jsx("span",{className:"material-symbols-outlined text-xl text-primary mb-1",children:t.icon}),e.jsx("p",{className:"text-xs font-semibold",children:t.title}),e.jsx("p",{className:"text-xs text-text-muted",children:t.desc})]},t.title))}),e.jsx("p",{className:"text-xs text-text-muted",children:"Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(c,{onClick:Bt,fullWidth:!0,children:"Start Tunnel"}),e.jsx(c,{onClick:()=>V(!1),variant:"ghost",fullWidth:!0,children:"Cancel"})]})]})}),e.jsx($,{isOpen:Et,title:"Disable Tunnel",onClose:()=>!P&&J(!1),children:e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx("p",{className:"text-sm text-text-muted",children:"The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(c,{onClick:Vt,fullWidth:!0,disabled:P,variant:"danger",children:P?"Disabling...":"Disable"}),e.jsx(c,{onClick:()=>J(!1),variant:"ghost",fullWidth:!0,disabled:P,children:"Cancel"})]})]})}),e.jsx($,{isOpen:Lt,title:"Tailscale Funnel",onClose:()=>{me||(W(!1),tt(""),u(null))},children:e.jsxs("div",{className:"flex flex-col gap-4",children:[Ce===null&&e.jsxs("p",{className:"text-sm text-text-muted flex items-center gap-2",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),"Checking..."]}),Ce===!1&&!me&&e.jsxs("div",{className:"flex flex-col gap-3",children:[e.jsx("p",{className:"text-sm text-text-muted",children:"Tailscale is not installed. Install it to enable Funnel."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(c,{onClick:Gt,fullWidth:!0,children:"Install Tailscale"}),e.jsx(c,{onClick:()=>W(!1),variant:"ghost",fullWidth:!0,children:"Cancel"})]})]}),me&&e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex items-center gap-2 text-sm text-text-muted",children:[e.jsx("span",{className:"material-symbols-outlined animate-spin text-sm",children:"progress_activity"}),"Installing Tailscale..."]}),Se.length>0&&e.jsx("div",{ref:he,className:"bg-black/5 dark:bg-white/5 rounded p-2 max-h-40 overflow-y-auto font-mono text-xs text-text-muted",children:Se.map((t,a)=>e.jsx("div",{children:t},a))})]}),Ce===!0&&!me&&e.jsxs("div",{className:"flex flex-col gap-3",children:[e.jsxs("div",{className:"flex items-center gap-2 text-sm text-green-600 dark:text-green-400",children:[e.jsx("span",{className:"material-symbols-outlined text-[16px]",children:"check_circle"}),"Tailscale installed"]}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(c,{onClick:()=>Ie(),fullWidth:!0,children:"Connect"}),e.jsx(c,{onClick:()=>W(!1),variant:"ghost",fullWidth:!0,children:"Cancel"})]})]}),K&&e.jsx(bs,{status:K})]})}),e.jsx($,{isOpen:Rt,title:"Disable Tailscale",onClose:()=>!_&&X(!1),children:e.jsxs("div",{className:"flex flex-col gap-4",children:[e.jsx("p",{className:"text-sm text-text-muted",children:"Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(c,{onClick:Xt,fullWidth:!0,disabled:_,variant:"danger",children:_?"Disabling...":"Disable"}),e.jsx(c,{onClick:()=>X(!1),variant:"ghost",fullWidth:!0,disabled:_,children:"Cancel"})]})]})}),e.jsx(os,{isOpen:!!w,onClose:()=>z(null),onConfirm:w==null?void 0:w.onConfirm,title:(w==null?void 0:w.title)||"Confirm",message:w==null?void 0:w.message,variant:"danger"})]})}function gs({label:h,url:d,copyId:x,copied:j,onCopy:U,badge:b,actions:N}){return e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${b==="CF"||b==="TS"?"bg-primary/10 text-primary":"bg-surface-2 text-text-muted"}`,children:h}),e.jsx(te,{value:d,readOnly:!0,className:"flex-1 font-mono text-sm"}),e.jsx("button",{onClick:()=>U(d,x),className:"p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0",children:e.jsx("span",{className:"material-symbols-outlined text-[18px]",children:j===x?"check":"content_copy"})}),N]})}function bs({status:h,className:d=""}){const x=j=>j.split(/(https?:\/\/[^\s]+)/g).map((b,N)=>/^https?:\/\//.test(b)?e.jsx("a",{href:b,target:"_blank",rel:"noreferrer",className:"underline font-medium",children:b},N):b);return e.jsx("div",{className:`p-2 rounded text-sm ${d} ${h.type==="success"?"bg-green-500/10 text-green-600 dark:text-green-400":h.type==="warning"?"bg-yellow-500/10 text-yellow-600 dark:text-yellow-400":h.type==="info"?"bg-blue-500/10 text-blue-600 dark:text-blue-400":"bg-red-500/10 text-red-600 dark:text-red-400"}`,children:x(h.message)})}function ys({text:h}){return e.jsxs("span",{className:"relative group inline-flex items-center",children:[e.jsx("span",{className:"material-symbols-outlined text-[14px] text-text-muted cursor-help",children:"help"}),e.jsx("span",{className:"pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 z-50 w-64 rounded bg-gray-900 dark:bg-gray-800 text-white text-xs px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg",children:h})]})}function ye({message:h,action:d}){return e.jsxs("div",{className:"flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-700 dark:text-amber-400",children:[e.jsx("span",{className:"material-symbols-outlined text-[16px] shrink-0 mt-0.5",children:"warning"}),e.jsx("p",{className:"text-xs flex-1",children:h}),d&&e.jsx("a",{href:d.href,className:"text-xs font-medium underline shrink-0 hover:opacity-80",onClick:d.href.startsWith("#")?x=>{var j;x.preventDefault(),(j=document.getElementById(d.href.slice(1)))==null||j.scrollIntoView({behavior:"smooth"})}:void 0,children:d.label})]})}fs.propTypes={machineId:cs.string.isRequired};export{fs as A};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e,n as t}from"./index.
|
|
1
|
+
import{j as e,n as t}from"./index.D4FwOBBU.js";function r({size:n="md",className:s}){const i={sm:"size-4",md:"size-6",lg:"size-8",xl:"size-12"};return e.jsx("span",{className:t("material-symbols-outlined animate-spin text-brand-500",i[n],s),children:"progress_activity"})}function c({message:n="Loading..."}){return e.jsxs("div",{className:"fixed inset-0 z-50 flex flex-col items-center justify-center bg-bg",children:[e.jsx(r,{size:"xl"}),e.jsx("p",{className:"mt-4 text-text-muted",children:n})]})}function a({className:n,...s}){return e.jsx("div",{className:t("animate-pulse rounded-[10px] bg-surface-2",n),...s})}function d(){return e.jsxs("div",{className:"p-6 rounded-[14px] border border-border-subtle bg-surface shadow-[var(--shadow-soft)]",children:[e.jsxs("div",{className:"flex items-center justify-between mb-4",children:[e.jsx(a,{className:"h-4 w-24"}),e.jsx(a,{className:"size-10 rounded-[10px]"})]}),e.jsx(a,{className:"h-8 w-16 mb-2"}),e.jsx(a,{className:"h-3 w-20"})]})}function x({type:n="spinner",...s}){switch(n){case"page":return e.jsx(c,{...s});case"skeleton":return e.jsx(a,{...s});case"card":return e.jsx(d,{...s});default:return e.jsx(r,{...s})}}export{d as C,x as L};
|