ework-web 0.10.0 → 0.10.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
@@ -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; }`,
@@ -104,7 +104,9 @@ function buildSetupScript(envBlock: string): string {
104
104
  `EWORK_DAEMON_ENV_EOF`,
105
105
  `echo "[3/4] starting daemon..."`,
106
106
  `ework-aio start daemon`,
107
- `echo "[4/4] done"`,
107
+ `echo "[4/4] verifying daemon is alive..."`,
108
+ `sleep 2`,
109
+ `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
110
  `echo DAEMON_STARTED`,
109
111
  ].join("\n");
110
112
  }
@@ -126,7 +128,7 @@ export async function deployRemoteDaemon(opts: DeployOpts): Promise<DeployResult
126
128
  }
127
129
  const env = parseEnvFile(envContent);
128
130
  const envBlock = buildEnvBlock(env, opts);
129
- const script = buildSetupScript(envBlock);
131
+ const script = buildSetupScript(envBlock, opts.daemonPort ?? 3101);
130
132
 
131
133
  const args = [
132
134
  "ssh",
package/src/index.ts CHANGED
@@ -887,6 +887,35 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
887
887
  }
888
888
 
889
889
  const daemonIdRe = /^\/api\/daemons\/(\d+)$/;
890
+ const daemonRestartRe = /^\/api\/daemons\/(\d+)\/restart$/;
891
+ const daemonRemoveRe = /^\/api\/daemons\/(\d+)\/remove$/;
892
+
893
+ if (req.method === "POST" && daemonRestartRe.test(url.pathname)) {
894
+ if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
895
+ const match = url.pathname.match(daemonRestartRe);
896
+ const id = match ? Number(match[1]) : NaN;
897
+ if (!Number.isFinite(id)) return json({ error: "invalid id" }, 400);
898
+ const rows = await getDB().all<{ endpoint: string; status: string }>(
899
+ `SELECT internal_endpoint AS endpoint, status FROM {{d_daemons}} WHERE id = ?`, [id]
900
+ );
901
+ if (rows.length === 0) return json({ ok: false, error: "daemon not found" }, 404);
902
+ await getDB().run(`UPDATE {{d_daemons}} SET status = 'active', last_heartbeat = datetime('now') WHERE id = ?`, [id]);
903
+ return json({ ok: true, note: "reactivated — daemon process will re-register via heartbeat" });
904
+ }
905
+
906
+ if (req.method === "DELETE" && daemonRemoveRe.test(url.pathname)) {
907
+ if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
908
+ const match = url.pathname.match(daemonRemoveRe);
909
+ const id = match ? Number(match[1]) : NaN;
910
+ if (!Number.isFinite(id)) return json({ error: "invalid id" }, 400);
911
+ const rows = await getDB().all<{ status: string }>(`SELECT status FROM {{d_daemons}} WHERE id = ?`, [id]);
912
+ if (rows.length === 0) return json({ ok: false, error: "daemon not found" }, 404);
913
+ const st = String(rows[0]?.status ?? "").toLowerCase();
914
+ if (st === "active") return json({ ok: false, error: "stop (drain) the daemon first before removing" }, 400);
915
+ await getDB().run(`DELETE FROM {{d_daemons}} WHERE id = ?`, [id]);
916
+ return json({ ok: true });
917
+ }
918
+
890
919
  if (req.method === "DELETE" && daemonIdRe.test(url.pathname)) {
891
920
  if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
892
921
  const match = url.pathname.match(daemonIdRe);
@@ -52,9 +52,14 @@
52
52
  return;
53
53
  }
54
54
  var rows = daemons.map(function (d) {
55
- var stopBtn = String(d.status || '').toLowerCase() === 'active'
56
- ? '<button type="button" class="stop" data-id="' + d.id + '">停止</button>'
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>' + stopBtn + '</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></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
- listEl.querySelectorAll('button.stop').forEach(function (b) {
74
- b.addEventListener('click', function () { stopDaemon(Number(b.getAttribute('data-id'))); });
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,39 @@
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
+ var data = await res.json();
163
+ if (data.ok) {
164
+ setResult('✓ daemon #' + id + ' 已重新激活', 'ok');
165
+ await loadDaemons();
166
+ } else {
167
+ setResult('✗ ' + (data.error || '未知错误'), 'err');
168
+ }
169
+ } catch (e) {
170
+ setResult('✗ ' + (e.message || String(e)), 'err');
171
+ }
172
+ }
173
+
174
+ async function removeDaemon(id) {
175
+ if (!confirm('确认删除 daemon #' + id + '?\n这会从数据库永久移除该记录。')) return;
176
+ setResult('正在删除 daemon #' + id + '…', 'loading');
177
+ try {
178
+ var res = await fetch('/api/daemons/' + id + '/remove', { method: 'DELETE' });
179
+ var data = await res.json();
180
+ if (data.ok) {
181
+ setResult('✓ daemon #' + id + ' 已删除', 'ok');
182
+ await loadDaemons();
183
+ } else {
184
+ setResult('✗ ' + (data.error || '未知错误'), 'err');
185
+ }
186
+ } catch (e) {
187
+ setResult('✗ ' + (e.message || String(e)), 'err');
188
+ }
189
+ }
190
+
148
191
  function setDeployResult(text, kind) {
149
192
  if (!deployResultEl) return;
150
193
  deployResultEl.className = 'db-result db-' + (kind === 'ok' ? 'ok' : kind === 'err' ? 'err' : 'loading');
@@ -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>