ework-web 0.10.1 → 0.10.3

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.1",
3
+ "version": "0.10.3",
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, daemonPort: number): string {
88
+ function buildSetupScript(envBlock: string, daemonPort: number, mysqlHost: string, mysqlPort: string): 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; }`,
@@ -95,16 +95,22 @@ function buildSetupScript(envBlock: string, daemonPort: number): string {
95
95
  `mkdir -p "$HOME/.local/lib"`,
96
96
  `npm config set prefix "$HOME/.local/lib"`,
97
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..."`,
98
+ `echo "[1/5] checking MySQL connectivity to ${mysqlHost}:${mysqlPort}..."`,
99
+ `timeout 5 bash -c "echo > /dev/tcp/${mysqlHost}/${mysqlPort}" 2>/dev/null && echo "MySQL port reachable" || { echo "MySQL UNREACHABLE at ${mysqlHost}:${mysqlPort} — daemon will fail to connect"; exit 1; }`,
100
+ `echo "[2/5] installing ework-aio..."`,
99
101
  `npm install -g ework-aio`,
100
- `echo "[2/4] writing daemon config..."`,
102
+ `echo "[3/5] writing daemon config..."`,
101
103
  `mkdir -p ~/.local/share/ework-aio/ework-daemon`,
102
- `cat > ~/.local/share/ework-aio/ework-daemon/.env <<'EWORK_DAEMON_ENV_EOF'`,
104
+ `REMOTE_IP=$(hostname -I 2>/dev/null | awk '{print $1}')`,
105
+ `DAEMON_ENDPOINT_LINE=""`,
106
+ `if [ -n "$REMOTE_IP" ]; then DAEMON_ENDPOINT_LINE="DAEMON_ENDPOINT=$REMOTE_IP:${String(daemonPort)}"; fi`,
107
+ `{ cat <<'EWORK_DAEMON_ENV_EOF'`,
103
108
  envBlock,
104
109
  `EWORK_DAEMON_ENV_EOF`,
105
- `echo "[3/4] starting daemon..."`,
110
+ `echo "$DAEMON_ENDPOINT_LINE"; } > ~/.local/share/ework-aio/ework-daemon/.env`,
111
+ `echo "[4/5] starting daemon..."`,
106
112
  `ework-aio start daemon`,
107
- `echo "[4/4] verifying daemon is alive..."`,
113
+ `echo "[5/5] verifying daemon is alive..."`,
108
114
  `sleep 2`,
109
115
  `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)"`,
110
116
  `echo DAEMON_STARTED`,
@@ -128,7 +134,7 @@ export async function deployRemoteDaemon(opts: DeployOpts): Promise<DeployResult
128
134
  }
129
135
  const env = parseEnvFile(envContent);
130
136
  const envBlock = buildEnvBlock(env, opts);
131
- const script = buildSetupScript(envBlock, opts.daemonPort ?? 3101);
137
+ const script = buildSetupScript(envBlock, opts.daemonPort ?? 3101, opts.mysqlHost, String(env.get("WORK_DB_PORT") ?? "3306"));
132
138
 
133
139
  const args = [
134
140
  "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
  }
@@ -970,25 +973,31 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
970
973
  if (targets.length === 0 || !mysqlHost) {
971
974
  return json({ ok: false, error: "至少需要一个目标 + mysqlHost" }, 400);
972
975
  }
973
- if (targets.length === 1) {
974
- const [single] = targets;
975
- if (!single) return json({ ok: false, error: "no target" }, 400);
976
- const result = await deployRemoteDaemon({ ...single, timeoutMs });
977
- return json(result, result.ok ? 200 : 422);
978
- }
979
976
 
980
977
  const { readable, writable } = new TransformStream();
981
978
  const writer = writable.getWriter();
982
979
  const enc = new TextEncoder();
983
980
  const send = (s: string) => writer.write(enc.encode(`data: ${JSON.stringify(s)}\n\n`));
984
981
 
985
- deployBatch(targets, timeoutMs, (label, chunk) => send(`[${label}] ${chunk}`))
986
- .then((results) => {
987
- const ok = [...results.values()].every((r) => r.ok);
988
- send(`\n${ok ? "ALL OK" : "SOME FAILED"}: ${[...results.entries()].map(([k, v]) => `${k}=${v.ok ? "✓" : "✗"}`).join(", ")}`);
989
- })
990
- .catch((e) => send(`ERROR: ${e instanceof Error ? e.message : String(e)}`))
991
- .finally(() => writer.close());
982
+ if (targets.length === 1) {
983
+ const [single] = targets;
984
+ send(`▶ 部署到 ${single!.sshHost}:${single!.daemonPort ?? 3101}(超时 ${timeoutMs / 1000}s)...\n\n`);
985
+ deployRemoteDaemon({ ...single!, timeoutMs, onOutput: (chunk) => send(chunk) })
986
+ .then((result) => {
987
+ send(`\n${result.ok ? "✓ 成功" : "✗ 失败"}: ${result.ok ? "daemon 已部署" : (result.error || "未知错误")}\n`);
988
+ if (result.output) send(result.output);
989
+ })
990
+ .catch((e) => send(`\n✗ 异常: ${e instanceof Error ? e.message : String(e)}\n`))
991
+ .finally(() => writer.close());
992
+ } else {
993
+ deployBatch(targets, timeoutMs, (label, chunk) => send(`[${label}] ${chunk}`))
994
+ .then((results) => {
995
+ const ok = [...results.values()].every((r) => r.ok);
996
+ send(`\n${ok ? "ALL OK" : "SOME FAILED"}: ${[...results.entries()].map(([k, v]) => `${k}=${v.ok ? "✓" : "✗"}`).join(", ")}`);
997
+ })
998
+ .catch((e) => send(`ERROR: ${e instanceof Error ? e.message : String(e)}`))
999
+ .finally(() => writer.close());
1000
+ }
992
1001
 
993
1002
  return new Response(readable, {
994
1003
  headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
@@ -159,6 +159,10 @@
159
159
  setResult('正在重启 daemon #' + id + '…', 'loading');
160
160
  try {
161
161
  var res = await fetch('/api/daemons/' + id + '/restart', { method: 'POST' });
162
+ if (res.status === 401) {
163
+ setResult('✗ 登录已过期,请刷新页面重新登录', 'err');
164
+ return;
165
+ }
162
166
  var data = await res.json();
163
167
  if (data.ok) {
164
168
  setResult('✓ daemon #' + id + ' 已重新激活', 'ok');
@@ -243,6 +247,11 @@
243
247
  if (deployBtn) deployBtn.disabled = false;
244
248
  return;
245
249
  }
250
+ if (/^(127\.|localhost$|0\.0\.0\.0$)/.test(mysqlHost)) {
251
+ setDeployResult('✗ MySQL 主机必须是远程机器可达的地址(不能是 ' + mysqlHost + ')。请使用 LAN IP,如 192.168.x.x', 'err');
252
+ if (deployBtn) deployBtn.disabled = false;
253
+ return;
254
+ }
246
255
 
247
256
  deployResultEl.textContent = '';
248
257
  appendDeployLog('▶ 部署 ' + targets.length + ' 个目标(超时 ' + (timeoutMs / 1000) + 's/个)...\n\n');