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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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; }`,
@@ -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
- `cat > ~/.local/share/ework-aio/ework-daemon/.env <<'EWORK_DAEMON_ENV_EOF'`,
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] done"`,
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);
@@ -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,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');
@@ -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>