ework-web 0.9.0 → 0.10.0
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/package.json +1 -1
- package/src/daemon-deploy.ts +72 -20
- package/src/index.ts +55 -12
- package/src/static/daemon-mgr.js +90 -19
- package/src/views/settings.ts +4 -4
package/package.json
CHANGED
package/src/daemon-deploy.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from "path";
|
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface DeployTarget {
|
|
6
6
|
sshHost: string;
|
|
7
7
|
sshUser: string;
|
|
8
8
|
sshPort: number;
|
|
@@ -11,14 +11,22 @@ export interface DeployOpts {
|
|
|
11
11
|
mysqlHost: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface DeployOpts extends DeployTarget {
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
onOutput?: (chunk: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export interface DeployResult {
|
|
15
20
|
ok: boolean;
|
|
16
21
|
output: string;
|
|
17
22
|
error?: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
export interface BatchTarget {
|
|
26
|
+
host: string;
|
|
27
|
+
daemonPort?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
const FORWARD_KEYS = [
|
|
23
31
|
"WORK_DB_DRIVER",
|
|
24
32
|
"WORK_DB_USER",
|
|
@@ -63,15 +71,15 @@ function parseEnvFile(content: string): Map<string, string> {
|
|
|
63
71
|
return out;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
function buildEnvBlock(env: Map<string, string>,
|
|
74
|
+
function buildEnvBlock(env: Map<string, string>, target: DeployTarget): string {
|
|
67
75
|
const lines: string[] = [];
|
|
68
76
|
for (const k of FORWARD_KEYS) {
|
|
69
77
|
const v = env.get(k);
|
|
70
78
|
if (v === undefined) continue;
|
|
71
79
|
lines.push(`${k}=${v}`);
|
|
72
80
|
}
|
|
73
|
-
lines.push(`WORK_DB_HOST=${
|
|
74
|
-
lines.push(`DAEMON_PORT=${String(
|
|
81
|
+
lines.push(`WORK_DB_HOST=${target.mysqlHost}`);
|
|
82
|
+
lines.push(`DAEMON_PORT=${String(target.daemonPort ?? 3101)}`);
|
|
75
83
|
lines.push(`DAEMON_HOST=0.0.0.0`);
|
|
76
84
|
lines.push(`DAEMON_ENV=production`);
|
|
77
85
|
return lines.join("\n");
|
|
@@ -81,13 +89,22 @@ function buildSetupScript(envBlock: string): string {
|
|
|
81
89
|
return [
|
|
82
90
|
"set -e",
|
|
83
91
|
`command -v npm >/dev/null 2>&1 || { curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -; sudo apt-get install -y nodejs; }`,
|
|
84
|
-
`command -v bun >/dev/null 2>&1 || { curl -fsSL https://bun.sh/install | bash;
|
|
92
|
+
`command -v bun >/dev/null 2>&1 || { curl -fsSL https://bun.sh/install | bash; }`,
|
|
93
|
+
`export BUN_INSTALL="$HOME/.bun"`,
|
|
94
|
+
`export PATH="$HOME/.local/lib/bin:$BUN_INSTALL/bin:$PATH"`,
|
|
95
|
+
`mkdir -p "$HOME/.local/lib"`,
|
|
96
|
+
`npm config set prefix "$HOME/.local/lib"`,
|
|
97
|
+
`grep -q '.local/lib/bin' "$HOME/.bashrc" 2>/dev/null || echo 'export PATH="$HOME/.local/lib/bin:$HOME/.bun/bin:$PATH"' >> "$HOME/.bashrc"`,
|
|
98
|
+
`echo "[1/4] installing ework-aio..."`,
|
|
85
99
|
`npm install -g ework-aio`,
|
|
100
|
+
`echo "[2/4] writing daemon config..."`,
|
|
86
101
|
`mkdir -p ~/.local/share/ework-aio/ework-daemon`,
|
|
87
102
|
`cat > ~/.local/share/ework-aio/ework-daemon/.env <<'EWORK_DAEMON_ENV_EOF'`,
|
|
88
103
|
envBlock,
|
|
89
104
|
`EWORK_DAEMON_ENV_EOF`,
|
|
105
|
+
`echo "[3/4] starting daemon..."`,
|
|
90
106
|
`ework-aio start daemon`,
|
|
107
|
+
`echo "[4/4] done"`,
|
|
91
108
|
`echo DAEMON_STARTED`,
|
|
92
109
|
].join("\n");
|
|
93
110
|
}
|
|
@@ -98,7 +115,7 @@ export async function deployRemoteDaemon(opts: DeployOpts): Promise<DeployResult
|
|
|
98
115
|
return {
|
|
99
116
|
ok: false,
|
|
100
117
|
output: "",
|
|
101
|
-
error: "找不到本地 daemon .env
|
|
118
|
+
error: "找不到本地 daemon .env",
|
|
102
119
|
};
|
|
103
120
|
}
|
|
104
121
|
let envContent: string;
|
|
@@ -135,23 +152,37 @@ export async function deployRemoteDaemon(opts: DeployOpts): Promise<DeployResult
|
|
|
135
152
|
const timer = setTimeout(() => {
|
|
136
153
|
ac.abort();
|
|
137
154
|
try { proc.kill(); } catch { /* already dead */ }
|
|
138
|
-
},
|
|
155
|
+
}, opts.timeoutMs);
|
|
139
156
|
|
|
140
|
-
let
|
|
141
|
-
let stderrText = "";
|
|
157
|
+
let output = "";
|
|
142
158
|
let exitCode: number | null = null;
|
|
143
159
|
let timedOut = false;
|
|
160
|
+
|
|
161
|
+
const decoder = new TextDecoder();
|
|
162
|
+
const readStream = async (stream: ReadableStream<Uint8Array>) => {
|
|
163
|
+
const reader = stream.getReader();
|
|
164
|
+
try {
|
|
165
|
+
while (true) {
|
|
166
|
+
const { done, value } = await reader.read();
|
|
167
|
+
if (done) break;
|
|
168
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
169
|
+
output += chunk;
|
|
170
|
+
opts.onOutput?.(chunk);
|
|
171
|
+
}
|
|
172
|
+
} catch { /* stream closed */ }
|
|
173
|
+
};
|
|
174
|
+
|
|
144
175
|
try {
|
|
145
176
|
ac.signal.addEventListener("abort", () => { timedOut = true; });
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
proc.exited,
|
|
177
|
+
await Promise.all([
|
|
178
|
+
readStream(proc.stdout as ReadableStream<Uint8Array>),
|
|
179
|
+
readStream(proc.stderr as ReadableStream<Uint8Array>),
|
|
150
180
|
]);
|
|
181
|
+
exitCode = await proc.exited;
|
|
151
182
|
} catch (e) {
|
|
152
183
|
return {
|
|
153
184
|
ok: false,
|
|
154
|
-
output
|
|
185
|
+
output,
|
|
155
186
|
error: e instanceof Error ? e.message : String(e),
|
|
156
187
|
};
|
|
157
188
|
} finally {
|
|
@@ -161,14 +192,35 @@ export async function deployRemoteDaemon(opts: DeployOpts): Promise<DeployResult
|
|
|
161
192
|
if (timedOut) {
|
|
162
193
|
return {
|
|
163
194
|
ok: false,
|
|
164
|
-
output
|
|
165
|
-
error:
|
|
195
|
+
output,
|
|
196
|
+
error: `SSH 部署超时(${String(Math.trunc(opts.timeoutMs / 1000))}s)`,
|
|
166
197
|
};
|
|
167
198
|
}
|
|
168
|
-
const combined = stdoutText + (stderrText ? "\n--- stderr ---\n" + stderrText : "");
|
|
169
199
|
return {
|
|
170
200
|
ok: exitCode === 0,
|
|
171
|
-
output
|
|
201
|
+
output,
|
|
172
202
|
error: exitCode === 0 ? undefined : `ssh 退出码 ${String(exitCode)}`,
|
|
173
203
|
};
|
|
174
204
|
}
|
|
205
|
+
|
|
206
|
+
export async function deployBatch(
|
|
207
|
+
targets: DeployTarget[],
|
|
208
|
+
timeoutMs: number,
|
|
209
|
+
onOutput: (target: string, chunk: string) => void,
|
|
210
|
+
): Promise<Map<string, DeployResult>> {
|
|
211
|
+
const results = new Map<string, DeployResult>();
|
|
212
|
+
await Promise.all(
|
|
213
|
+
targets.map(async (t) => {
|
|
214
|
+
const label = `${t.sshUser}@${t.sshHost}:${String(t.sshPort)}`;
|
|
215
|
+
onOutput(label, `▶ 开始部署到 ${label}\n`);
|
|
216
|
+
const r = await deployRemoteDaemon({
|
|
217
|
+
...t,
|
|
218
|
+
timeoutMs,
|
|
219
|
+
onOutput: (chunk) => onOutput(label, chunk),
|
|
220
|
+
});
|
|
221
|
+
results.set(label, r);
|
|
222
|
+
onOutput(label, r.ok ? `✓ ${label} 部署成功\n` : `✗ ${label} ${r.error ?? "失败"}\n`);
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
return results;
|
|
226
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ import { buildProjectMembersPage } from "./views/projectMembers";
|
|
|
93
93
|
import { buildProjectUpstreamsPage, trySetUpstreamUrls } from "./views/projectUpstreams";
|
|
94
94
|
import { buildProjectModelPage } from "./views/projectModel";
|
|
95
95
|
import { handleGiteaApi } from "./giteaApi";
|
|
96
|
-
import { deployRemoteDaemon } from "./daemon-deploy";
|
|
96
|
+
import { deployRemoteDaemon, deployBatch, type DeployTarget } from "./daemon-deploy";
|
|
97
97
|
|
|
98
98
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
99
99
|
const STATIC_DIR = join(__dirname, "static");
|
|
@@ -902,25 +902,68 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
902
902
|
|
|
903
903
|
if (req.method === "POST" && url.pathname === "/api/daemons/deploy") {
|
|
904
904
|
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
905
|
-
if (!rateLimit(`daemons-deploy:${ip}`,
|
|
905
|
+
if (!rateLimit(`daemons-deploy:${ip}`, 10, 10 / 3600)) return json({ error: "rate limited" }, 429);
|
|
906
906
|
const payload = await req.json().catch(() => ({} as unknown));
|
|
907
907
|
if (!payload || typeof payload !== "object") return json({ ok: false, error: "invalid body" }, 400);
|
|
908
908
|
const obj = payload as Record<string, unknown>;
|
|
909
|
-
const
|
|
910
|
-
const sshUser = typeof obj.sshUser === "string" ? obj.sshUser.trim() : "";
|
|
909
|
+
const sshUser = typeof obj.sshUser === "string" ? obj.sshUser.trim() : "root";
|
|
911
910
|
const mysqlHost = typeof obj.mysqlHost === "string" ? obj.mysqlHost.trim() : "";
|
|
912
|
-
if (!sshHost || !sshUser || !mysqlHost) {
|
|
913
|
-
return json({ ok: false, error: "sshHost, sshUser, mysqlHost 必填" }, 400);
|
|
914
|
-
}
|
|
915
911
|
const sshPort = typeof obj.sshPort === "number" && Number.isFinite(obj.sshPort) && obj.sshPort > 0 && obj.sshPort < 65536
|
|
916
912
|
? Math.trunc(obj.sshPort)
|
|
917
913
|
: 22;
|
|
918
914
|
const sshKeyFile = typeof obj.sshKeyFile === "string" && obj.sshKeyFile.trim() ? obj.sshKeyFile.trim() : undefined;
|
|
919
|
-
const
|
|
920
|
-
? Math.trunc(obj.
|
|
921
|
-
:
|
|
922
|
-
|
|
923
|
-
|
|
915
|
+
const timeoutMs = typeof obj.timeoutMs === "number" && Number.isFinite(obj.timeoutMs) && obj.timeoutMs >= 30_000 && obj.timeoutMs <= 600_000
|
|
916
|
+
? Math.trunc(obj.timeoutMs)
|
|
917
|
+
: 180_000;
|
|
918
|
+
|
|
919
|
+
const targets: DeployTarget[] = [];
|
|
920
|
+
if (Array.isArray(obj.targets)) {
|
|
921
|
+
for (const t of obj.targets) {
|
|
922
|
+
if (!t || typeof t !== "object") continue;
|
|
923
|
+
const r = t as Record<string, unknown>;
|
|
924
|
+
const host = typeof r.host === "string" ? r.host.trim() : "";
|
|
925
|
+
if (!host) continue;
|
|
926
|
+
targets.push({
|
|
927
|
+
sshHost: host, sshUser, sshPort, sshKeyFile, mysqlHost,
|
|
928
|
+
daemonPort: typeof r.daemonPort === "number" ? Math.trunc(r.daemonPort) : undefined,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
const sshHost = typeof obj.sshHost === "string" ? obj.sshHost.trim() : "";
|
|
933
|
+
if (sshHost) {
|
|
934
|
+
targets.push({
|
|
935
|
+
sshHost, sshUser, sshPort, sshKeyFile, mysqlHost,
|
|
936
|
+
daemonPort: typeof obj.daemonPort === "number" ? Math.trunc(obj.daemonPort) : undefined,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (targets.length === 0 || !mysqlHost) {
|
|
942
|
+
return json({ ok: false, error: "至少需要一个目标 + mysqlHost" }, 400);
|
|
943
|
+
}
|
|
944
|
+
if (targets.length === 1) {
|
|
945
|
+
const [single] = targets;
|
|
946
|
+
if (!single) return json({ ok: false, error: "no target" }, 400);
|
|
947
|
+
const result = await deployRemoteDaemon({ ...single, timeoutMs });
|
|
948
|
+
return json(result, result.ok ? 200 : 422);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const { readable, writable } = new TransformStream();
|
|
952
|
+
const writer = writable.getWriter();
|
|
953
|
+
const enc = new TextEncoder();
|
|
954
|
+
const send = (s: string) => writer.write(enc.encode(`data: ${JSON.stringify(s)}\n\n`));
|
|
955
|
+
|
|
956
|
+
deployBatch(targets, timeoutMs, (label, chunk) => send(`[${label}] ${chunk}`))
|
|
957
|
+
.then((results) => {
|
|
958
|
+
const ok = [...results.values()].every((r) => r.ok);
|
|
959
|
+
send(`\n${ok ? "ALL OK" : "SOME FAILED"}: ${[...results.entries()].map(([k, v]) => `${k}=${v.ok ? "✓" : "✗"}`).join(", ")}`);
|
|
960
|
+
})
|
|
961
|
+
.catch((e) => send(`ERROR: ${e instanceof Error ? e.message : String(e)}`))
|
|
962
|
+
.finally(() => writer.close());
|
|
963
|
+
|
|
964
|
+
return new Response(readable, {
|
|
965
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
|
|
966
|
+
});
|
|
924
967
|
}
|
|
925
968
|
|
|
926
969
|
if (url.pathname === "/settings") {
|
package/src/static/daemon-mgr.js
CHANGED
|
@@ -156,40 +156,111 @@
|
|
|
156
156
|
return el && el.value != null ? el.value.trim() : '';
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function appendDeployLog(text) {
|
|
160
|
+
if (!deployResultEl) return;
|
|
161
|
+
deployResultEl.className = 'db-result db-loading';
|
|
162
|
+
deployResultEl.textContent += text;
|
|
163
|
+
deployResultEl.scrollTop = deployResultEl.scrollHeight;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseTargets(raw) {
|
|
167
|
+
var targets = [];
|
|
168
|
+
var lines = raw.split('\n');
|
|
169
|
+
for (var i = 0; i < lines.length; i++) {
|
|
170
|
+
var line = lines[i].trim();
|
|
171
|
+
if (!line || line.startsWith('#')) continue;
|
|
172
|
+
var parts = line.split(':');
|
|
173
|
+
var host = parts[0].trim();
|
|
174
|
+
var port = parts.length > 1 ? parseInt(parts[1].trim(), 10) : NaN;
|
|
175
|
+
if (host) {
|
|
176
|
+
var t = { host: host };
|
|
177
|
+
if (Number.isFinite(port) && port > 0) t.daemonPort = port;
|
|
178
|
+
targets.push(t);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return targets;
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
async function deployRemote() {
|
|
160
185
|
if (deployBtn) deployBtn.disabled = true;
|
|
161
|
-
|
|
162
|
-
var
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
};
|
|
167
|
-
var sshPort = Number(val('deploy-ssh-port'));
|
|
168
|
-
if (Number.isFinite(sshPort) && sshPort > 0) body.sshPort = sshPort;
|
|
169
|
-
var dPort = Number(val('deploy-daemon-port'));
|
|
170
|
-
if (Number.isFinite(dPort) && dPort > 0) body.daemonPort = dPort;
|
|
186
|
+
var targets = parseTargets(val('deploy-targets'));
|
|
187
|
+
var mysqlHost = val('deploy-mysql-host');
|
|
188
|
+
var sshUser = val('deploy-user') || 'root';
|
|
189
|
+
var sshPort = Number(val('deploy-ssh-port')) || 22;
|
|
190
|
+
var timeoutMs = (Number(val('deploy-timeout')) || 180) * 1000;
|
|
171
191
|
var keyFile = val('deploy-key');
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
setDeployResult('✗
|
|
192
|
+
|
|
193
|
+
if (targets.length === 0) {
|
|
194
|
+
setDeployResult('✗ 请填写至少一个目标机器', 'err');
|
|
195
|
+
if (deployBtn) deployBtn.disabled = false;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!mysqlHost) {
|
|
199
|
+
setDeployResult('✗ MySQL 主机必填', 'err');
|
|
175
200
|
if (deployBtn) deployBtn.disabled = false;
|
|
176
201
|
return;
|
|
177
202
|
}
|
|
203
|
+
|
|
204
|
+
deployResultEl.textContent = '';
|
|
205
|
+
appendDeployLog('▶ 部署 ' + targets.length + ' 个目标(超时 ' + (timeoutMs / 1000) + 's/个)...\n\n');
|
|
206
|
+
|
|
207
|
+
var body = {
|
|
208
|
+
targets: targets,
|
|
209
|
+
sshUser: sshUser,
|
|
210
|
+
sshPort: sshPort,
|
|
211
|
+
mysqlHost: mysqlHost,
|
|
212
|
+
timeoutMs: timeoutMs
|
|
213
|
+
};
|
|
214
|
+
if (keyFile) body.sshKeyFile = keyFile;
|
|
215
|
+
|
|
178
216
|
try {
|
|
179
217
|
var res = await fetch('/api/daemons/deploy', {
|
|
180
218
|
method: 'POST',
|
|
181
219
|
headers: { 'Content-Type': 'application/json' },
|
|
182
220
|
body: JSON.stringify(body)
|
|
183
221
|
});
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
222
|
+
|
|
223
|
+
if (res.headers.get('Content-Type') && res.headers.get('Content-Type').includes('text/event-stream')) {
|
|
224
|
+
var reader = res.body.getReader();
|
|
225
|
+
var decoder = new TextDecoder();
|
|
226
|
+
var allOk = true;
|
|
227
|
+
while (true) {
|
|
228
|
+
var chunk = await reader.read();
|
|
229
|
+
if (chunk.done) break;
|
|
230
|
+
var text = decoder.decode(chunk.value, { stream: true });
|
|
231
|
+
var lines = text.split('\n');
|
|
232
|
+
for (var i = 0; i < lines.length; i++) {
|
|
233
|
+
var line = lines[i].trim();
|
|
234
|
+
if (!line.startsWith('data: ')) continue;
|
|
235
|
+
try {
|
|
236
|
+
var msg = JSON.parse(line.slice(6));
|
|
237
|
+
appendDeployLog(msg);
|
|
238
|
+
if (msg.indexOf('✗') >= 0) allOk = false;
|
|
239
|
+
} catch (e) { /* skip non-JSON */ }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (allOk) {
|
|
243
|
+
deployResultEl.className = 'db-result db-ok';
|
|
244
|
+
await loadDaemons();
|
|
245
|
+
} else {
|
|
246
|
+
deployResultEl.className = 'db-result db-err';
|
|
247
|
+
}
|
|
188
248
|
} else {
|
|
189
|
-
|
|
249
|
+
var data = await res.json();
|
|
250
|
+
if (data.ok) {
|
|
251
|
+
appendDeployLog('✓ 部署成功\n');
|
|
252
|
+
if (data.output) appendDeployLog(data.output);
|
|
253
|
+
deployResultEl.className = 'db-result db-ok';
|
|
254
|
+
await loadDaemons();
|
|
255
|
+
} else {
|
|
256
|
+
appendDeployLog('✗ ' + (data.error || '未知错误') + '\n');
|
|
257
|
+
if (data.output) appendDeployLog(data.output);
|
|
258
|
+
deployResultEl.className = 'db-result db-err';
|
|
259
|
+
}
|
|
190
260
|
}
|
|
191
261
|
} catch (e) {
|
|
192
|
-
|
|
262
|
+
appendDeployLog('✗ ' + (e.message || String(e)) + '\n');
|
|
263
|
+
deployResultEl.className = 'db-result db-err';
|
|
193
264
|
} finally {
|
|
194
265
|
if (deployBtn) deployBtn.disabled = false;
|
|
195
266
|
}
|
package/src/views/settings.ts
CHANGED
|
@@ -81,16 +81,16 @@ function buildDaemonSection(viewer: UserRow): string {
|
|
|
81
81
|
<details style="margin-top:.7rem">
|
|
82
82
|
<summary style="cursor:pointer;font-size:13px;color:var(--text-muted)">📡 SSH 远程部署</summary>
|
|
83
83
|
<div style="margin-top:.5rem">
|
|
84
|
-
<label class="sf"><span>SSH 主机</span><input type="text" id="deploy-host" placeholder="192.168.1.100" autocomplete="off"></label>
|
|
85
84
|
<label class="sf"><span>SSH 用户</span><input type="text" id="deploy-user" placeholder="root" value="root" autocomplete="off"></label>
|
|
86
85
|
<label class="sf"><span>SSH 端口</span><input type="number" id="deploy-ssh-port" value="22" min="1" max="65535" autocomplete="off"></label>
|
|
87
86
|
<label class="sf"><span>SSH 密钥</span><input type="text" id="deploy-key" placeholder="~/.ssh/id_rsa" autocomplete="off"></label>
|
|
88
|
-
<label class="sf"><span>Daemon 端口</span><input type="number" id="deploy-daemon-port" placeholder="3101" min="1" max="65535" autocomplete="off"></label>
|
|
89
87
|
<label class="sf"><span>MySQL 主机(远程可见)</span><input type="text" id="deploy-mysql-host" placeholder="192.168.1.1" autocomplete="off"></label>
|
|
90
|
-
<
|
|
88
|
+
<label class="sf"><span>超时(秒)</span><input type="number" id="deploy-timeout" value="180" min="30" max="600" autocomplete="off"></label>
|
|
89
|
+
<label class="sf" style="align-items:flex-start"><span>目标机器</span><textarea id="deploy-targets" rows="4" style="flex:1;padding:.35rem .5rem;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:12px;font-family:ui-monospace,monospace" placeholder="每行一个,格式: 192.168.1.100 192.168.1.101:3201 10.0.0.5:3202 (不带端口则用默认 3101)"></textarea></label>
|
|
90
|
+
<div class="db-controls"><button type="button" id="daemon-deploy">部署(支持批量)</button></div>
|
|
91
91
|
</div>
|
|
92
92
|
</details>
|
|
93
|
-
<div id="deploy-result" class="db-result"></div>
|
|
93
|
+
<div id="deploy-result" class="db-result" style="max-height:400px;overflow-y:auto;font-family:ui-monospace,monospace;font-size:11px"></div>
|
|
94
94
|
<script src="/static/daemon-mgr.js"></script>
|
|
95
95
|
</section>`;
|
|
96
96
|
}
|