ework-web 0.10.0 → 0.10.2
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 +10 -4
- package/src/index.ts +32 -0
- package/src/static/daemon-mgr.js +55 -8
- package/src/views/settings.ts +3 -0
package/package.json
CHANGED
package/src/daemon-deploy.ts
CHANGED
|
@@ -85,7 +85,7 @@ function buildEnvBlock(env: Map<string, string>, target: DeployTarget): string {
|
|
|
85
85
|
return lines.join("\n");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function buildSetupScript(envBlock: string): string {
|
|
88
|
+
function buildSetupScript(envBlock: string, daemonPort: number): string {
|
|
89
89
|
return [
|
|
90
90
|
"set -e",
|
|
91
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; }`,
|
|
@@ -99,12 +99,18 @@ function buildSetupScript(envBlock: string): string {
|
|
|
99
99
|
`npm install -g ework-aio`,
|
|
100
100
|
`echo "[2/4] writing daemon config..."`,
|
|
101
101
|
`mkdir -p ~/.local/share/ework-aio/ework-daemon`,
|
|
102
|
-
`
|
|
102
|
+
`REMOTE_IP=$(hostname -I 2>/dev/null | awk '{print $1}')`,
|
|
103
|
+
`DAEMON_ENDPOINT_LINE=""`,
|
|
104
|
+
`if [ -n "$REMOTE_IP" ]; then DAEMON_ENDPOINT_LINE="DAEMON_ENDPOINT=$REMOTE_IP:${String(daemonPort)}"; fi`,
|
|
105
|
+
`{ cat <<'EWORK_DAEMON_ENV_EOF'`,
|
|
103
106
|
envBlock,
|
|
104
107
|
`EWORK_DAEMON_ENV_EOF`,
|
|
108
|
+
`echo "$DAEMON_ENDPOINT_LINE"; } > ~/.local/share/ework-aio/ework-daemon/.env`,
|
|
105
109
|
`echo "[3/4] starting daemon..."`,
|
|
106
110
|
`ework-aio start daemon`,
|
|
107
|
-
`echo "[4/4]
|
|
111
|
+
`echo "[4/4] verifying daemon is alive..."`,
|
|
112
|
+
`sleep 2`,
|
|
113
|
+
`curl -sf --max-time 3 http://127.0.0.1:${String(daemonPort)}/api/status >/dev/null 2>&1 && echo "DAEMON_ALIVE" || echo "DAEMON_WARNING: status check failed (may need a moment to boot)"`,
|
|
108
114
|
`echo DAEMON_STARTED`,
|
|
109
115
|
].join("\n");
|
|
110
116
|
}
|
|
@@ -126,7 +132,7 @@ export async function deployRemoteDaemon(opts: DeployOpts): Promise<DeployResult
|
|
|
126
132
|
}
|
|
127
133
|
const env = parseEnvFile(envContent);
|
|
128
134
|
const envBlock = buildEnvBlock(env, opts);
|
|
129
|
-
const script = buildSetupScript(envBlock);
|
|
135
|
+
const script = buildSetupScript(envBlock, opts.daemonPort ?? 3101);
|
|
130
136
|
|
|
131
137
|
const args = [
|
|
132
138
|
"ssh",
|
package/src/index.ts
CHANGED
|
@@ -479,6 +479,9 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
479
479
|
|
|
480
480
|
const auth = await checkAuth(req, cfg, ip);
|
|
481
481
|
if (!auth.ok) {
|
|
482
|
+
if (url.pathname.startsWith("/api/")) {
|
|
483
|
+
return json({ error: "authentication required" }, 401);
|
|
484
|
+
}
|
|
482
485
|
const next = sanitizeNext(url.pathname + url.search);
|
|
483
486
|
return Response.redirect(`${url.origin}/login?next=${encodeURIComponent(next)}`, 302);
|
|
484
487
|
}
|
|
@@ -887,6 +890,35 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
887
890
|
}
|
|
888
891
|
|
|
889
892
|
const daemonIdRe = /^\/api\/daemons\/(\d+)$/;
|
|
893
|
+
const daemonRestartRe = /^\/api\/daemons\/(\d+)\/restart$/;
|
|
894
|
+
const daemonRemoveRe = /^\/api\/daemons\/(\d+)\/remove$/;
|
|
895
|
+
|
|
896
|
+
if (req.method === "POST" && daemonRestartRe.test(url.pathname)) {
|
|
897
|
+
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
898
|
+
const match = url.pathname.match(daemonRestartRe);
|
|
899
|
+
const id = match ? Number(match[1]) : NaN;
|
|
900
|
+
if (!Number.isFinite(id)) return json({ error: "invalid id" }, 400);
|
|
901
|
+
const rows = await getDB().all<{ endpoint: string; status: string }>(
|
|
902
|
+
`SELECT internal_endpoint AS endpoint, status FROM {{d_daemons}} WHERE id = ?`, [id]
|
|
903
|
+
);
|
|
904
|
+
if (rows.length === 0) return json({ ok: false, error: "daemon not found" }, 404);
|
|
905
|
+
await getDB().run(`UPDATE {{d_daemons}} SET status = 'active', last_heartbeat = datetime('now') WHERE id = ?`, [id]);
|
|
906
|
+
return json({ ok: true, note: "reactivated — daemon process will re-register via heartbeat" });
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (req.method === "DELETE" && daemonRemoveRe.test(url.pathname)) {
|
|
910
|
+
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
911
|
+
const match = url.pathname.match(daemonRemoveRe);
|
|
912
|
+
const id = match ? Number(match[1]) : NaN;
|
|
913
|
+
if (!Number.isFinite(id)) return json({ error: "invalid id" }, 400);
|
|
914
|
+
const rows = await getDB().all<{ status: string }>(`SELECT status FROM {{d_daemons}} WHERE id = ?`, [id]);
|
|
915
|
+
if (rows.length === 0) return json({ ok: false, error: "daemon not found" }, 404);
|
|
916
|
+
const st = String(rows[0]?.status ?? "").toLowerCase();
|
|
917
|
+
if (st === "active") return json({ ok: false, error: "stop (drain) the daemon first before removing" }, 400);
|
|
918
|
+
await getDB().run(`DELETE FROM {{d_daemons}} WHERE id = ?`, [id]);
|
|
919
|
+
return json({ ok: true });
|
|
920
|
+
}
|
|
921
|
+
|
|
890
922
|
if (req.method === "DELETE" && daemonIdRe.test(url.pathname)) {
|
|
891
923
|
if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
|
|
892
924
|
const match = url.pathname.match(daemonIdRe);
|
package/src/static/daemon-mgr.js
CHANGED
|
@@ -52,9 +52,14 @@
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
var rows = daemons.map(function (d) {
|
|
55
|
-
var
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
var stRaw = String(d.status || 'unknown').toLowerCase();
|
|
56
|
+
var actions = '';
|
|
57
|
+
if (stRaw === 'active') {
|
|
58
|
+
actions = '<button type="button" class="stop" data-id="' + d.id + '">停止</button>';
|
|
59
|
+
} else {
|
|
60
|
+
actions = '<button type="button" class="restart" data-id="' + d.id + '">重启</button>' +
|
|
61
|
+
'<button type="button" class="remove" data-id="' + d.id + '">删除</button>';
|
|
62
|
+
}
|
|
58
63
|
return '<tr>' +
|
|
59
64
|
'<td class="cap">' + d.id + '</td>' +
|
|
60
65
|
'<td class="loc">' + locationCell(d.endpoint) + '</td>' +
|
|
@@ -63,16 +68,21 @@
|
|
|
63
68
|
'<td class="heartbeat">' + fmtTime(d.lastHeartbeat) + '</td>' +
|
|
64
69
|
'<td class="registered">' + fmtTime(d.registeredAt) + '</td>' +
|
|
65
70
|
'<td>' + statusPill(d.status) + '</td>' +
|
|
66
|
-
'<td>' +
|
|
71
|
+
'<td>' + actions + '</td>' +
|
|
67
72
|
'</tr>';
|
|
68
73
|
}).join('');
|
|
69
74
|
listEl.innerHTML =
|
|
70
75
|
'<table><thead><tr>' +
|
|
71
|
-
'<th>ID</th><th>位置</th><th>Endpoint</th><th>容量</th><th>心跳</th><th>注册于</th><th>状态</th><th
|
|
76
|
+
'<th>ID</th><th>位置</th><th>Endpoint</th><th>容量</th><th>心跳</th><th>注册于</th><th>状态</th><th>操作</th>' +
|
|
72
77
|
'</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
var bindBtn = function (selector, fn) {
|
|
79
|
+
listEl.querySelectorAll(selector).forEach(function (b) {
|
|
80
|
+
b.addEventListener('click', function () { fn(Number(b.getAttribute('data-id'))); });
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
bindBtn('button.stop', stopDaemon);
|
|
84
|
+
bindBtn('button.restart', restartDaemon);
|
|
85
|
+
bindBtn('button.remove', removeDaemon);
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
function setResult(text, kind) {
|
|
@@ -145,6 +155,43 @@
|
|
|
145
155
|
}
|
|
146
156
|
}
|
|
147
157
|
|
|
158
|
+
async function restartDaemon(id) {
|
|
159
|
+
setResult('正在重启 daemon #' + id + '…', 'loading');
|
|
160
|
+
try {
|
|
161
|
+
var res = await fetch('/api/daemons/' + id + '/restart', { method: 'POST' });
|
|
162
|
+
if (res.status === 401) {
|
|
163
|
+
setResult('✗ 登录已过期,请刷新页面重新登录', 'err');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
var data = await res.json();
|
|
167
|
+
if (data.ok) {
|
|
168
|
+
setResult('✓ daemon #' + id + ' 已重新激活', 'ok');
|
|
169
|
+
await loadDaemons();
|
|
170
|
+
} else {
|
|
171
|
+
setResult('✗ ' + (data.error || '未知错误'), 'err');
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
setResult('✗ ' + (e.message || String(e)), 'err');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function removeDaemon(id) {
|
|
179
|
+
if (!confirm('确认删除 daemon #' + id + '?\n这会从数据库永久移除该记录。')) return;
|
|
180
|
+
setResult('正在删除 daemon #' + id + '…', 'loading');
|
|
181
|
+
try {
|
|
182
|
+
var res = await fetch('/api/daemons/' + id + '/remove', { method: 'DELETE' });
|
|
183
|
+
var data = await res.json();
|
|
184
|
+
if (data.ok) {
|
|
185
|
+
setResult('✓ daemon #' + id + ' 已删除', 'ok');
|
|
186
|
+
await loadDaemons();
|
|
187
|
+
} else {
|
|
188
|
+
setResult('✗ ' + (data.error || '未知错误'), 'err');
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
setResult('✗ ' + (e.message || String(e)), 'err');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
148
195
|
function setDeployResult(text, kind) {
|
|
149
196
|
if (!deployResultEl) return;
|
|
150
197
|
deployResultEl.className = 'db-result db-' + (kind === 'ok' ? 'ok' : kind === 'err' ? 'err' : 'loading');
|
package/src/views/settings.ts
CHANGED
|
@@ -153,6 +153,9 @@ button.secondary{background:transparent;color:var(--text-muted);border:1px solid
|
|
|
153
153
|
.daemon-list td.loc{white-space:nowrap}
|
|
154
154
|
.daemon-list button.stop{padding:.25rem .6rem;font-size:11px;background:transparent;color:#e87c7c;border:1px solid rgba(220,53,69,.4)}
|
|
155
155
|
.daemon-list button.stop:hover{background:rgba(220,53,69,.12)}
|
|
156
|
+
.daemon-list button.restart{padding:.25rem .6rem;font-size:11px;background:transparent;color:#5eb88a;border:1px solid rgba(40,167,69,.4);margin-right:.3rem}
|
|
157
|
+
.daemon-list button.restart:hover{background:rgba(40,167,69,.12)}
|
|
158
|
+
.daemon-list button.remove{padding:.25rem .6rem;font-size:11px;background:transparent;color:var(--text-muted);border:1px solid var(--border)}
|
|
156
159
|
.daemon-empty{color:var(--text-muted);font-size:13px;padding:.5rem 0}
|
|
157
160
|
</style></head><body>
|
|
158
161
|
<header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a><span style="opacity:.8"> · 设置</span></header>
|