aws-runtime-bridge 1.9.9 → 1.9.20

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.
Files changed (121) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +40 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +353 -7
  4. package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
  5. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  6. package/dist/adapter/OpencodeSdkAdapter.js +36 -3
  7. package/dist/adapter/instance-ai-config-resolver.d.ts +49 -0
  8. package/dist/adapter/instance-ai-config-resolver.d.ts.map +1 -0
  9. package/dist/adapter/instance-ai-config-resolver.js +46 -0
  10. package/dist/adapter/types.d.ts +27 -2
  11. package/dist/adapter/types.d.ts.map +1 -1
  12. package/dist/index.js +7 -3
  13. package/dist/routes/dashboard.d.ts.map +1 -1
  14. package/dist/routes/dashboard.js +1092 -8
  15. package/dist/routes/events.d.ts +7 -0
  16. package/dist/routes/events.d.ts.map +1 -0
  17. package/dist/routes/events.js +22 -0
  18. package/dist/routes/file-browser.d.ts +1 -1
  19. package/dist/routes/file-browser.d.ts.map +1 -1
  20. package/dist/routes/file-browser.js +25 -9
  21. package/dist/routes/file-snapshot.d.ts +10 -0
  22. package/dist/routes/file-snapshot.d.ts.map +1 -0
  23. package/dist/routes/file-snapshot.js +156 -0
  24. package/dist/routes/git.d.ts +10 -0
  25. package/dist/routes/git.d.ts.map +1 -1
  26. package/dist/routes/git.js +305 -2
  27. package/dist/routes/git.test.js +39 -2
  28. package/dist/routes/instance.js +3 -1
  29. package/dist/routes/memory.d.ts +3 -0
  30. package/dist/routes/memory.d.ts.map +1 -1
  31. package/dist/routes/memory.js +3 -0
  32. package/dist/routes/terminal.d.ts +2 -1
  33. package/dist/routes/terminal.d.ts.map +1 -1
  34. package/dist/routes/terminal.js +378 -41
  35. package/dist/routes/terminal.test.js +33 -7
  36. package/dist/services/agent-event-store.d.ts +59 -0
  37. package/dist/services/agent-event-store.d.ts.map +1 -0
  38. package/dist/services/agent-event-store.js +68 -0
  39. package/dist/services/event-delta-pusher.d.ts +40 -0
  40. package/dist/services/event-delta-pusher.d.ts.map +1 -0
  41. package/dist/services/event-delta-pusher.js +95 -0
  42. package/dist/services/fileSnapshotStore.d.ts +96 -0
  43. package/dist/services/fileSnapshotStore.d.ts.map +1 -0
  44. package/dist/services/fileSnapshotStore.js +500 -0
  45. package/dist/services/instance-ai-config-service.d.ts +49 -0
  46. package/dist/services/instance-ai-config-service.d.ts.map +1 -0
  47. package/dist/services/instance-ai-config-service.js +174 -0
  48. package/dist/services/panel-auth.d.ts +23 -0
  49. package/dist/services/panel-auth.d.ts.map +1 -1
  50. package/dist/services/panel-auth.js +64 -1
  51. package/dist/services/ring-buffer.d.ts +67 -0
  52. package/dist/services/ring-buffer.d.ts.map +1 -0
  53. package/dist/services/ring-buffer.js +103 -0
  54. package/dist/services/session-output.d.ts +47 -3
  55. package/dist/services/session-output.d.ts.map +1 -1
  56. package/dist/services/session-output.js +112 -2
  57. package/dist/utils/instance-ai-config-debug-logger.d.ts +37 -0
  58. package/dist/utils/instance-ai-config-debug-logger.d.ts.map +1 -0
  59. package/dist/utils/instance-ai-config-debug-logger.js +63 -0
  60. package/dist/utils/logger.d.ts +18 -0
  61. package/dist/utils/logger.d.ts.map +1 -1
  62. package/dist/utils/logger.js +48 -4
  63. package/package/acode/dist/built-in-file-tools.d.ts +1 -1
  64. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  65. package/package/acode/dist/built-in-file-tools.js +5 -3
  66. package/package/acode/dist/builtins/commandArtifacts.d.ts +8 -1
  67. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -1
  68. package/package/acode/dist/builtins/commandArtifacts.js +24 -4
  69. package/package/acode/dist/builtins/shellCommandResult.d.ts +1 -0
  70. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -1
  71. package/package/acode/dist/builtins/shellCommandResult.js +3 -3
  72. package/package/acode/dist/builtins/shellCommandRunner.d.ts +1 -0
  73. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -1
  74. package/package/acode/dist/builtins/shellCommandRunner.js +1 -0
  75. package/package/acode/dist/cli.js +2 -0
  76. package/package/acode/dist/runtime.d.ts.map +1 -1
  77. package/package/acode/dist/runtime.js +156 -5
  78. package/package/acode/dist/types.d.ts +1 -0
  79. package/package/acode/dist/types.d.ts.map +1 -1
  80. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +5 -0
  81. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  82. package/package/aws-client-agent-mcp/dist/agent-client.js +16 -0
  83. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  84. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +136 -0
  85. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -0
  86. package/package/aws-client-agent-mcp/dist/context-manager.js +473 -0
  87. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -0
  88. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +6 -0
  89. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +1 -0
  90. package/package/aws-client-agent-mcp/dist/context-manager.test.js +307 -0
  91. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -0
  92. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +170 -2
  93. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  94. package/package/aws-client-agent-mcp/dist/mcp-server.js +947 -9
  95. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  96. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +103 -1
  97. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  98. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -1
  99. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  100. package/package/aws-client-agent-mcp/dist/mcp-tools.js +62 -2
  101. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  102. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +46 -1
  103. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  104. package/package/aws-client-agent-mcp/dist/memory-store.js +164 -12
  105. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  106. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +8 -0
  107. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
  108. package/package/aws-client-agent-mcp/dist/memory-tools.js +88 -1
  109. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  110. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +7 -1
  111. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  112. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  113. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  114. package/package/aws-client-agent-mcp/dist/types.d.ts +103 -2
  115. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  116. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +27 -2
  118. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  119. package/package/aws-client-agent-mcp/dist/websocket-client.js +122 -5
  120. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  121. package/package.json +1 -1
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { Router } from "express";
8
8
  import os from "node:os";
9
- import { getRegistrationState } from "../services/auto-register.js";
9
+ import { execSync } from "node:child_process";
10
10
  import { getRuntimeBindingPublicState, getRuntimePairingCode } from "../services/runtime-binding.js";
11
11
  import { getAgentProcessManager } from "../services/agent-process-manager.js";
12
12
  import { collectSystemMetrics } from "../services/system-metrics.js";
@@ -14,7 +14,9 @@ import { loadPersistedSessions } from "../services/terminal-persistence.js";
14
14
  import { createLogger } from "../utils/logger.js";
15
15
  import { getBridgeVersion } from "../config.js";
16
16
  import { sdkSessions } from "./terminal.js";
17
- import { createSession, validateSession, destroySession, validateCredentials, } from "../services/panel-auth.js";
17
+ import { createSession, getPanelCredentials, validateSession, destroySession, validateCredentials, hasConfiguredCredentials, isDefaultCredentials, setPanelCredentials, getAllSessions, getActiveSessionCount, } from "../services/panel-auth.js";
18
+ import { getRecentLogs } from "../utils/logger.js";
19
+ import { autoRegister, getRegistrationState, getAutoRegisterConfigFilePath } from "../services/auto-register.js";
18
20
  const log = createLogger("dashboard");
19
21
  // ── 内存缓存(两级) ─────────────────────────────────────
20
22
  // 一级:完整响应缓存,匹配前端轮询间隔(10s),避免大部分请求走 I/O
@@ -119,9 +121,10 @@ dashboardRouter.post("/runtime/login", (req, res) => {
119
121
  return;
120
122
  }
121
123
  const token = createSession();
124
+ const needSetup = !hasConfiguredCredentials();
122
125
  // 设置 HttpOnly cookie,24 小时过期
123
126
  res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400`);
124
- res.json({ ok: true });
127
+ res.json({ ok: true, needSetup });
125
128
  });
126
129
  /** POST /runtime/logout 登出 */
127
130
  dashboardRouter.post("/runtime/logout", (req, res) => {
@@ -132,6 +135,170 @@ dashboardRouter.post("/runtime/logout", (req, res) => {
132
135
  res.setHeader("Set-Cookie", `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`);
133
136
  res.json({ ok: true });
134
137
  });
138
+ /**
139
+ * GET /runtime/credentials-status
140
+ * 返回当前凭据是否仍是默认值 root/root。
141
+ * 需要面板登录会话。
142
+ */
143
+ dashboardRouter.get("/runtime/credentials-status", (req, res) => {
144
+ if (!requirePanelAuth(req, res))
145
+ return;
146
+ const creds = getPanelCredentials();
147
+ res.json({ ok: true, isDefault: isDefaultCredentials(), configured: hasConfiguredCredentials(), username: creds.username });
148
+ });
149
+ /**
150
+ * POST /runtime/change-credentials
151
+ * 修改面板登录凭据,写入 ~/.aws-bridge/config.json。
152
+ * 需要面板登录会话。修改密码需校验原密码;用户名可同时修改。
153
+ */
154
+ dashboardRouter.post("/runtime/change-credentials", (req, res) => {
155
+ if (!requirePanelAuth(req, res))
156
+ return;
157
+ const { username, password, passwordConfirm, oldPassword } = req.body || {};
158
+ // 校验原密码(仅凭据已配置且新密码不为空时要求)
159
+ if (password && String(password).trim()) {
160
+ // 密码一致性校验
161
+ if (String(password) !== String(passwordConfirm)) {
162
+ res.status(400).json({ ok: false, error: "passwords do not match" });
163
+ return;
164
+ }
165
+ if (hasConfiguredCredentials()) {
166
+ if (!oldPassword) {
167
+ res.status(400).json({ ok: false, error: "old password required" });
168
+ return;
169
+ }
170
+ const current = getPanelCredentials();
171
+ if (String(oldPassword) !== current.password) {
172
+ res.status(403).json({ ok: false, error: "old password is incorrect" });
173
+ return;
174
+ }
175
+ }
176
+ // 首次设置时不校验原密码
177
+ }
178
+ // 至少需要用户名或密码的变更
179
+ if (!username || !String(username).trim()) {
180
+ res.status(400).json({ ok: false, error: "username required" });
181
+ return;
182
+ }
183
+ const finalPassword = (password && String(password).trim()) ? String(password).trim() : getPanelCredentials().password;
184
+ setPanelCredentials(String(username).trim(), finalPassword);
185
+ res.json({ ok: true });
186
+ });
187
+ /**
188
+ * GET /runtime/node-info
189
+ * 返回节点详细信息,供设置面板展示。
190
+ * 需要面板登录会话。
191
+ */
192
+ dashboardRouter.get("/runtime/node-info", (req, res) => {
193
+ if (!requirePanelAuth(req, res))
194
+ return;
195
+ const binding = getRuntimeBindingPublicState();
196
+ const reg = getRegistrationState();
197
+ res.json({
198
+ ok: true,
199
+ hostname: os.hostname(),
200
+ platform: os.platform(),
201
+ arch: os.arch(),
202
+ uptime: Math.floor(process.uptime()),
203
+ nodeVersion: process.version,
204
+ bridgeVersion,
205
+ configPath: getAutoRegisterConfigFilePath(),
206
+ homeDir: process.env.AWS_RUNTIME_HOME_DIR || os.homedir(),
207
+ bindingStatus: binding.status,
208
+ paired: binding.paired,
209
+ instanceId: binding.instanceId || reg.instanceId,
210
+ schedulerBaseUrl: binding.schedulerBaseUrl,
211
+ registered: reg.registered,
212
+ pairingCode: binding.pairingCode,
213
+ activeSessions: getActiveSessionCount(),
214
+ });
215
+ });
216
+ /**
217
+ * GET /runtime/logs
218
+ * 返回最近的 bridge 日志。
219
+ * 需要面板登录会话。
220
+ */
221
+ dashboardRouter.get("/runtime/logs", (req, res) => {
222
+ if (!requirePanelAuth(req, res))
223
+ return;
224
+ const level = req.query.level || undefined;
225
+ const maxCount = Math.min(Number(req.query.count) || 200, 500);
226
+ const validLevels = ['debug', 'info', 'warn', 'error'];
227
+ const filterLevel = level && validLevels.includes(level)
228
+ ? level
229
+ : undefined;
230
+ const logs = getRecentLogs(maxCount, filterLevel);
231
+ res.json({ ok: true, logs });
232
+ });
233
+ /**
234
+ * POST /runtime/re-register
235
+ * 向调度中心重新发起注册。
236
+ * 需要面板登录会话。
237
+ */
238
+ dashboardRouter.post("/runtime/re-register", async (req, res) => {
239
+ if (!requirePanelAuth(req, res))
240
+ return;
241
+ try {
242
+ const success = await autoRegister();
243
+ res.json({ ok: success });
244
+ }
245
+ catch (err) {
246
+ const e = err;
247
+ res.status(500).json({ ok: false, error: e.message });
248
+ }
249
+ });
250
+ /**
251
+ * GET /runtime/admin/sessions
252
+ * 列出当前所有活跃的面板登录会话。
253
+ * 需要面板登录会话。
254
+ */
255
+ dashboardRouter.get("/runtime/admin/sessions", (req, res) => {
256
+ if (!requirePanelAuth(req, res))
257
+ return;
258
+ const list = getAllSessions().map(s => ({
259
+ token: s.token.slice(0, 8) + "...",
260
+ createdAt: new Date(s.createdAt).toISOString(),
261
+ lastUsedAt: new Date(s.lastUsedAt).toISOString(),
262
+ age: Math.floor((Date.now() - s.createdAt) / 1000),
263
+ }));
264
+ res.json({ ok: true, sessions: list });
265
+ });
266
+ /**
267
+ * POST /runtime/admin/sessions/:token/destroy
268
+ * 强制销毁指定的面板会话。
269
+ * 需要面板登录会话。
270
+ */
271
+ dashboardRouter.post("/runtime/admin/sessions/:token/destroy", (req, res) => {
272
+ if (!requirePanelAuth(req, res))
273
+ return;
274
+ destroySession(req.params.token);
275
+ res.json({ ok: true });
276
+ });
277
+ /**
278
+ * POST /runtime/kill-process
279
+ * 强制终止指定 PID 的进程。
280
+ * 需要面板登录会话。
281
+ */
282
+ dashboardRouter.post("/runtime/kill-process", (req, res) => {
283
+ if (!requirePanelAuth(req, res))
284
+ return;
285
+ const pid = Number(req.body.pid);
286
+ if (!pid || !Number.isInteger(pid) || pid <= 0) {
287
+ res.status(400).json({ ok: false, error: "Invalid PID" });
288
+ return;
289
+ }
290
+ try {
291
+ const cmd = os.platform() === "win32" ? `taskkill /PID ${pid} /F` : `kill -9 ${pid}`;
292
+ execSync(cmd, { timeout: 5000 });
293
+ log.info("Killed process", { pid });
294
+ res.json({ ok: true });
295
+ }
296
+ catch (err) {
297
+ const e = err;
298
+ log.warn("Failed to kill process", { pid, error: e.message });
299
+ res.status(500).json({ ok: false, error: e.message });
300
+ }
301
+ });
135
302
  /**
136
303
  * GET /runtime/dashboard
137
304
  * 聚合 bridge 所有状态数据。使用 2 秒缓存避免频繁 I/O,需要面板登录会话。
@@ -184,6 +351,9 @@ dashboardRouter.get("/runtime/dashboard", async (req, res) => {
184
351
  managementStatus: p.managementStatus,
185
352
  sessionId: record?.sessionId,
186
353
  state: record?.state,
354
+ command: p.command,
355
+ cwd: p.cwd,
356
+ startedAt: p.startedAt,
187
357
  };
188
358
  });
189
359
  const sortOrder = { managed: 0, orphan: 1, unknown: 2 };
@@ -254,6 +424,7 @@ dashboardRouter.get("/login", (_req, res) => {
254
424
  /**
255
425
  * GET / 仪表盘主页
256
426
  * 需要面板登录会话,未登录时跳转到 /login。
427
+ * 当凭据仍是默认值 root/root 时跳转到 /setup 引导首次设置。
257
428
  * 支持 ?lang=en 切换英文,默认中文
258
429
  */
259
430
  dashboardRouter.get("/", (req, res) => {
@@ -262,10 +433,28 @@ dashboardRouter.get("/", (req, res) => {
262
433
  res.redirect("/login");
263
434
  return;
264
435
  }
436
+ // 如果 config.json 尚未配置过凭据,引导首次设置
437
+ if (!hasConfiguredCredentials()) {
438
+ res.redirect("/setup");
439
+ return;
440
+ }
265
441
  const lang = req.query.lang === "en" ? "en" : "zh";
266
442
  res.type("text/html; charset=utf-8");
267
443
  res.send(DASHBOARD_HTML.replace("{{LANG}}", lang));
268
444
  });
445
+ /**
446
+ * GET /setup 首次登录设置页面
447
+ * 需要面板登录会话。提供用户名/密码修改表单,也支持跳过(保持 root/root)。
448
+ */
449
+ dashboardRouter.get("/setup", (req, res) => {
450
+ const token = getSessionToken(req);
451
+ if (!token || !validateSession(token)) {
452
+ res.redirect("/login");
453
+ return;
454
+ }
455
+ res.type("text/html; charset=utf-8");
456
+ res.send(SETUP_HTML);
457
+ });
269
458
  // ======================================================================
270
459
  // DASHBOARD HTML — 单页自包含,支持中文/英文界面,默认中文
271
460
  // ======================================================================
@@ -565,6 +754,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
565
754
  <span id="updateIndicator" style="display:none"></span>
566
755
  </span>
567
756
  <span class="lang-btn" id="langBtn" onclick="toggleLang()">中文</span>
757
+ <button onclick="openSettings()" id="settingsBtn" title="Settings / 设置">&#x2699;</button>
568
758
  <button onclick="refresh()" id="refreshBtn">&#x21bb;</button>
569
759
  </div>
570
760
  </div>
@@ -608,6 +798,200 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
608
798
  <div class="section-title" id="sectionPersisted">Persisted Sessions <span class="count" id="persistedCount"></span></div>
609
799
  <div class="table-wrap" id="persistedTable"><div class="empty" id="loadingPersisted">Loading...</div></div>
610
800
 
801
+ <!-- Settings Modal -->
802
+ <div class="modal-overlay" id="settingsModal" style="display:none" onclick="closeSettings(event)">
803
+ <div class="settings-panel" onclick="event.stopPropagation()">
804
+ <div class="settings-head">
805
+ <span id="settingsTitle"></span>
806
+ <button class="settings-close" onclick="closeSettings()">&times;</button>
807
+ </div>
808
+ <div class="settings-body">
809
+ <div class="settings-nav" id="settingsNav">
810
+ <div class="settings-nav-item active" data-page="account" onclick="switchSettingsPage('account')"><span id="navAccount"></span></div>
811
+ <div class="settings-nav-item" data-page="node" onclick="switchSettingsPage('node')"><span id="navNode"></span></div>
812
+ <div class="settings-nav-item" data-page="logs" onclick="switchSettingsPage('logs')"><span id="navLogs"></span></div>
813
+ <div class="settings-nav-item" data-page="scheduler" onclick="switchSettingsPage('scheduler')"><span id="navScheduler"></span></div>
814
+ <div class="settings-nav-item" data-page="maintenance" onclick="switchSettingsPage('maintenance')"><span id="navMaintenance"></span></div>
815
+ </div>
816
+ <div class="settings-content">
817
+ <!-- Account page -->
818
+ <div class="settings-page active" id="pageAccount">
819
+ <form id="settingsForm" onsubmit="return saveSettings(event)">
820
+ <div class="s-field">
821
+ <label id="lblUsername"></label>
822
+ <input type="text" id="setUsername" name="username" autocomplete="username">
823
+ </div>
824
+ <div class="s-field">
825
+ <label id="lblCurrentPwd"></label>
826
+ <input type="password" id="setOldPassword" name="oldPassword" autocomplete="current-password">
827
+ </div>
828
+ <div class="s-field">
829
+ <label id="lblNewPwd"></label>
830
+ <input type="password" id="setNewPassword" name="newPassword" autocomplete="new-password">
831
+ <span class="s-hint" id="hintKeepUnchanged"></span>
832
+ </div>
833
+ <div class="s-field">
834
+ <label id="lblConfirmPwd"></label>
835
+ <input type="password" id="setPasswordConfirm" name="passwordConfirm" autocomplete="new-password">
836
+ </div>
837
+ <div class="s-msg" id="settingsMsg"></div>
838
+ <div class="s-actions">
839
+ <button type="submit" class="s-btn s-btn-primary" id="settingsSaveBtn"></button>
840
+ </div>
841
+ </form>
842
+ </div>
843
+ <!-- Node Info page -->
844
+ <div class="settings-page" id="pageNode">
845
+ <div class="s-info-grid" id="nodeInfoGrid">
846
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeHostname"></span><span class="s-info-value" id="valNodeHostname">-</span></div>
847
+ <div class="s-info-row"><span class="s-info-label" id="lblNodePlatform"></span><span class="s-info-value" id="valNodePlatform">-</span></div>
848
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeUptime"></span><span class="s-info-value" id="valNodeUptime">-</span></div>
849
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeVersion"></span><span class="s-info-value" id="valNodeVersion">-</span></div>
850
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeNodeVer"></span><span class="s-info-value" id="valNodeNodeVer">-</span></div>
851
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeConfigPath"></span><span class="s-info-value" id="valNodeConfigPath">-</span></div>
852
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeHomeDir"></span><span class="s-info-value" id="valNodeHomeDir">-</span></div>
853
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeScheduler"></span><span class="s-info-value" id="valNodeScheduler">-</span></div>
854
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeBinding"></span><span class="s-info-value" id="valNodeBinding">-</span></div>
855
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeRegistered"></span><span class="s-info-value" id="valNodeRegistered">-</span></div>
856
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeInstanceId"></span><span class="s-info-value" id="valNodeInstanceId">-</span></div>
857
+ <div class="s-info-row"><span class="s-info-label" id="lblNodeActiveSessions"></span><span class="s-info-value" id="valNodeActiveSessions">-</span></div>
858
+ </div>
859
+ <div class="s-actions">
860
+ <button class="s-btn s-btn-secondary" onclick="refreshNodeInfo()">Refresh / 🔄</button>
861
+ </div>
862
+ </div>
863
+ <!-- Logs page -->
864
+ <div class="settings-page" id="pageLogs">
865
+ <div class="s-log-toolbar">
866
+ <select id="logLevelFilter" onchange="refreshLogs()">
867
+ <option value="">All / 全部</option>
868
+ <option value="info">INFO</option>
869
+ <option value="warn">WARN</option>
870
+ <option value="error">ERROR</option>
871
+ </select>
872
+ <label class="s-log-toggle"><input type="checkbox" id="logAutoRefresh" checked> <span id="lblLogAutoRefresh"></span></label>
873
+ <button class="s-btn s-btn-sm" onclick="refreshLogs()" id="btnRefreshLogs">🔄</button>
874
+ </div>
875
+ <div class="s-log-list" id="logList"><div class="s-log-empty" id="logEmpty"></div></div>
876
+ </div>
877
+ <!-- Scheduler page -->
878
+ <div class="settings-page" id="pageScheduler">
879
+ <div class="s-field">
880
+ <label id="lblSchedUrl"></label>
881
+ <div class="s-info-row"><span class="s-info-value" id="valSchedUrl">-</span></div>
882
+ </div>
883
+ <div class="s-field">
884
+ <p class="s-desc" id="descSched"></p>
885
+ </div>
886
+ <div class="s-msg" id="schedMsg"></div>
887
+ <div class="s-actions">
888
+ <button class="s-btn s-btn-primary" id="btnReRegister" onclick="doReRegister()"></button>
889
+ </div>
890
+ </div>
891
+ <!-- Maintenance page -->
892
+ <div class="settings-page" id="pageMaintenance">
893
+ <div class="s-field">
894
+ <label id="lblRestartBridge"></label>
895
+ <p class="s-desc" id="descRestart"></p>
896
+ </div>
897
+ <div class="s-restart-msg" id="restartMsg"></div>
898
+ <div class="s-actions">
899
+ <button class="s-btn s-btn-danger" id="btnRestartBridge" onclick="confirmRestartBridge()"></button>
900
+ </div>
901
+ </div>
902
+ </div>
903
+ </div>
904
+ </div>
905
+ </div>
906
+
907
+ <style>
908
+ .modal-overlay {
909
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
910
+ background: rgba(0,0,0,0.55); z-index: 1000;
911
+ display: flex; align-items: center; justify-content: center;
912
+ padding: 20px;
913
+ }
914
+ .settings-panel {
915
+ background: var(--surface); border: 1px solid var(--border);
916
+ border-radius: var(--radius); width: 640px; max-width: 100%;
917
+ max-height: 90vh; display: flex; flex-direction: column;
918
+ }
919
+ .settings-head {
920
+ display: flex; align-items: center; justify-content: space-between;
921
+ padding: 16px 20px; border-bottom: 1px solid var(--border);
922
+ }
923
+ .settings-head span { font-size: 16px; font-weight: 600; }
924
+ .settings-close {
925
+ background: none; border: none; color: var(--text2);
926
+ font-size: 22px; cursor: pointer; padding: 0 4px; line-height: 1;
927
+ }
928
+ .settings-close:hover { color: var(--text); }
929
+ .settings-body { display: flex; flex: 1; min-height: 320px; }
930
+ .settings-nav { width: 100px; flex-shrink: 0; border-right: 1px solid var(--border); padding: 8px 0; }
931
+ .settings-nav-item { padding: 7px 14px; font-size: 13px; cursor: pointer; color: var(--text2); transition: all .15s; }
932
+ .settings-nav-item:hover { color: var(--text); background: rgba(255,255,255,0.03); }
933
+ .settings-nav-item.active { color: var(--accent); background: rgba(91,141,239,0.1); }
934
+ .settings-content { flex: 1; padding: 20px; overflow-y: auto; }
935
+ .settings-page { display: none; }
936
+ .settings-page.active { display: block; }
937
+ .s-field { margin-bottom: 16px; }
938
+ .s-field label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--text2); }
939
+ .s-field input { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; outline: none; }
940
+ .s-field input:focus { border-color: var(--accent); }
941
+ .s-hint { display: block; font-size: 11px; color: var(--text2); margin-top: 4px; }
942
+ .s-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; display: none; }
943
+ .s-msg.error { display: block; background: rgba(240,84,84,0.12); color: var(--red); }
944
+ .s-msg.success { display: block; background: rgba(67,198,168,0.12); color: var(--green); }
945
+ .s-msg.info { display: block; background: rgba(91,141,239,0.12); color: var(--accent); }
946
+ .s-actions { margin-top: 12px; }
947
+ .s-btn { padding: 9px 24px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all .15s; border: none; }
948
+ .s-btn-sm { padding: 5px 12px; font-size: 13px; }
949
+ .s-btn-primary { background: var(--accent); color: #fff; }
950
+ .s-btn-primary:hover { background: var(--accent-hover); }
951
+ .s-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
952
+ .s-btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
953
+ .s-btn-secondary:hover { background: var(--border); }
954
+ .s-btn-danger { background: var(--red); color: #fff; }
955
+ .s-btn-danger:hover { background: #d94a4a; }
956
+ .s-btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
957
+ .s-desc { font-size: 13px; color: var(--text2); line-height: 1.6; margin: 0; }
958
+ .s-restart-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; display: none; }
959
+ .s-restart-msg.info { display: block; background: rgba(91,141,239,0.12); color: var(--accent); }
960
+ .s-restart-msg.error { display: block; background: rgba(240,84,84,0.12); color: var(--red); }
961
+ /* Info grid */
962
+ .s-info-grid { display: grid; gap: 2px; margin-bottom: 16px; }
963
+ .s-info-row { display: flex; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
964
+ .s-info-label { color: var(--text2); min-width: 110px; flex-shrink: 0; }
965
+ .s-info-value { color: var(--text); word-break: break-all; }
966
+ /* Log viewer */
967
+ .s-log-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
968
+ .s-log-toolbar select { background: var(--surface2); border: 1px solid var(--border); color: var(--text); padding: 5px 8px; border-radius: 4px; font-size: 12px; outline: none; }
969
+ .s-log-toolbar select:focus { border-color: var(--accent); }
970
+ .s-log-toggle { font-size: 12px; color: var(--text2); cursor: pointer; display: flex; align-items: center; gap: 4px; }
971
+ .s-log-toggle input { accent-color: var(--accent); }
972
+ .s-log-list { max-height: 360px; overflow-y: auto; font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace; font-size: 11px; line-height: 1.7; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 8px; }
973
+ .s-log-entry { padding: 1px 4px; white-space: pre-wrap; word-break: break-all; }
974
+ .s-log-entry .ll { font-weight: 600; }
975
+ .s-log-entry .ll.INFO { color: var(--accent); }
976
+ .s-log-entry .ll.WARN { color: var(--orange); }
977
+ .s-log-entry .ll.ERROR { color: var(--red); }
978
+ .s-log-entry .ts { color: var(--text2); }
979
+ .s-log-empty { color: var(--text2); font-size: 13px; text-align: center; padding: 24px; }
980
+ /* Table */
981
+ .s-table-wrap { max-height: 320px; overflow-y: auto; }
982
+ .s-table { width: 100%; border-collapse: collapse; font-size: 12px; }
983
+ .s-table th { text-align: left; padding: 7px 10px; font-size: 11px; font-weight: 600; color: var(--text2); background: var(--surface2); border-bottom: 1px solid var(--border); white-space: nowrap; }
984
+ .s-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }
985
+ .s-table tr:last-child td { border-bottom: none; }
986
+ .s-tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; }
987
+ .s-tag.green { background: rgba(67,198,168,0.12); color: var(--green); }
988
+ .s-tag.red { background: rgba(240,84,84,0.12); color: var(--red); }
989
+ .s-tag.orange { background: rgba(240,160,48,0.12); color: var(--orange); }
990
+ .btn-kill { padding: 2px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; cursor: pointer; border: 1px solid var(--red); background: transparent; color: var(--red); transition: all .15s; }
991
+ .btn-kill:hover { background: var(--red); color: #fff; }
992
+ .btn-kill:disabled { opacity: 0.4; cursor: not-allowed; }
993
+ </style>
994
+
611
995
  <script>
612
996
  // ── 多语言翻译表 ──────────────────────────────────────────
613
997
  const T = {
@@ -651,6 +1035,7 @@ const T = {
651
1035
  secUnknown: "\u5176\u4ed6\u8fdb\u7a0b",
652
1036
  orphanBanner: "\u4ee5\u4e0b\u8fdb\u7a0b\u4ecd\u5728\u8fd0\u884c\u4f46\u8131\u79bb\u4e86 bridge \u7ba1\u7406",
653
1037
  btnTerminate: "\u7ec8\u6b62",
1038
+ btnKill: "\u6740\u6b7b",
654
1039
  showUnknown: "\u663e\u793a",
655
1040
  hideUnknown: "\u9690\u85cf",
656
1041
  noOrphans: "\u65e0\u5b64\u513f\u8fdb\u7a0b",
@@ -674,8 +1059,62 @@ const T = {
674
1059
  thWorkspace: "\u5de5\u4f5c\u533a",
675
1060
  thProvider: "\u63d0\u4f9b\u65b9",
676
1061
  thCommand: "\u547d\u4ee4",
1062
+ thDirectory: "\u76ee\u5f55",
677
1063
  thStarted: "\u5f00\u59cb\u65f6\u95f4",
678
1064
  countManaged: "\u7ba1\u7406\u4e2d",
1065
+ // 设置
1066
+ setTitle: "\u8bbe\u7f6e",
1067
+ setAccount: "\u8d26\u53f7",
1068
+ setUsername: "\u7528\u6237\u540d",
1069
+ setCurrentPwd: "\u5f53\u524d\u5bc6\u7801",
1070
+ setNewPwd: "\u65b0\u5bc6\u7801",
1071
+ setConfirmPwd: "\u786e\u8ba4\u65b0\u5bc6\u7801",
1072
+ setSave: "\u4fdd\u5b58",
1073
+ setCancel: "\u53d6\u6d88",
1074
+ setKeepUnchanged: "\u4e0d\u586b\u5219\u4e0d\u4fee\u6539\u5bc6\u7801",
1075
+ setPwdRequired: "\u4fee\u6539\u5bc6\u7801\u9700\u8981\u8f93\u5165\u5f53\u524d\u5bc6\u7801",
1076
+ setPwdMismatch: "\u4e24\u6b21\u8f93\u5165\u7684\u65b0\u5bc6\u7801\u4e0d\u4e00\u81f4",
1077
+ setPwdTooShort: "\u5bc6\u7801\u81f3\u5c114\u4f4d",
1078
+ setSaved: "\u8bbe\u7f6e\u5df2\u4fdd\u5b58",
1079
+ setSaveFailed: "\u4fdd\u5b58\u5931\u8d25",
1080
+ setNetworkError: "\u7f51\u7edc\u9519\u8bef",
1081
+ setMaintenance: "\u7ef4\u62a4",
1082
+ setRestartBridge: "\u91cd\u542f Bridge",
1083
+ setRestartDesc: "\u91cd\u542f\u6865\u63a5\u670d\u52a1\uff0c\u6b63\u5728\u8fd0\u884c\u7684\u4f1a\u8bdd\u4e0d\u53d7\u5f71\u54cd\u3002",
1084
+ setRestartConfirm: "\u786e\u8ba4\u91cd\u542f Bridge\uff1f",
1085
+ setRestarting: "\u91cd\u542f\u4e2d...",
1086
+ // 节点信息
1087
+ nodeTitle: "\u8282\u70b9\u4fe1\u606f",
1088
+ nodeHostname: "\u4e3b\u673a\u540d",
1089
+ nodePlatform: "\u5e73\u53f0",
1090
+ nodeUptime: "\u8fd0\u884c\u65f6\u957f",
1091
+ nodeVersion: "Bridge \u7248\u672c",
1092
+ nodeNodeVer: "Node \u7248\u672c",
1093
+ nodeConfigPath: "\u914d\u7f6e\u6587\u4ef6\u8def\u5f84",
1094
+ nodeHomeDir: "\u8fd0\u884c\u65f6\u76ee\u5f55",
1095
+ nodeScheduler: "\u8c03\u5ea6\u4e2d\u5fc3",
1096
+ nodeBinding: "\u7ed1\u5b9a\u72b6\u6001",
1097
+ nodeRegistered: "\u6ce8\u518c\u72b6\u6001",
1098
+ nodeInstanceId: "\u5b9e\u4f8b ID",
1099
+ nodeActiveSessions: "\u6d3b\u8dc3\u4f1a\u8bdd",
1100
+ nodePaired: "\u5df2\u7ed1\u5b9a",
1101
+ nodeUnpaired: "\u672a\u7ed1\u5b9a",
1102
+ nodeRegYes: "\u5df2\u6ce8\u518c",
1103
+ nodeRegNo: "\u672a\u6ce8\u518c",
1104
+ // 日志
1105
+ logTitle: "\u65e5\u5fd7",
1106
+ logAutoRefresh: "\u81ea\u52a8\u5237\u65b0",
1107
+ logLevelAll: "\u5168\u90e8",
1108
+ logRefreshing: "\u5237\u65b0\u4e2d...",
1109
+ logEmpty: "\u6682\u65e0\u65e5\u5fd7",
1110
+ // 调度中心
1111
+ schedTitle: "\u8c03\u5ea6\u4e2d\u5fc3",
1112
+ schedUrl: "\u670d\u52a1\u5730\u5740",
1113
+ schedReRegister: "\u91cd\u65b0\u6ce8\u518c",
1114
+ schedReRegistering: "\u6ce8\u518c\u4e2d...",
1115
+ schedReRegistered: "\u91cd\u65b0\u6ce8\u518c\u6210\u529f",
1116
+ schedReRegisterFail: "\u91cd\u65b0\u6ce8\u518c\u5931\u8d25",
1117
+ schedDesc: "\u5411\u8c03\u5ea6\u4e2d\u5fc3\u53d1\u8d77\u91cd\u65b0\u6ce8\u518c\uff0c\u5efa\u8bae\u5728\u8fde\u63a5\u5f02\u5e38\u65f6\u4f7f\u7528\u3002",
679
1118
  // 状态文本
680
1119
  stHealthy: "\u5065\u5eb7",
681
1120
  stError: "\u5f02\u5e38",
@@ -726,6 +1165,7 @@ const T = {
726
1165
  secUnknown: "Other Processes",
727
1166
  orphanBanner: "These processes are running but no longer managed by bridge",
728
1167
  btnTerminate: "Terminate",
1168
+ btnKill: "Kill",
729
1169
  showUnknown: "Show",
730
1170
  hideUnknown: "Hide",
731
1171
  noOrphans: "No orphan processes",
@@ -748,8 +1188,63 @@ const T = {
748
1188
  thWorkspace: "Workspace",
749
1189
  thProvider: "Provider",
750
1190
  thCommand: "Command",
1191
+ thDirectory: "Directory",
751
1192
  thStarted: "Started",
752
1193
  countManaged: "managed",
1194
+ // Settings
1195
+ setTitle: "Settings",
1196
+ setAccount: "Account",
1197
+ setUsername: "Username",
1198
+ setCurrentPwd: "Current Password",
1199
+ setNewPwd: "New Password",
1200
+ setConfirmPwd: "Confirm New Password",
1201
+ setSave: "Save",
1202
+ setCancel: "Cancel",
1203
+ setKeepUnchanged: "Leave blank to keep current password",
1204
+ setPwdRequired: "Current password is required to change password",
1205
+ setPwdMismatch: "New passwords do not match",
1206
+ setPwdTooShort: "Password must be at least 4 characters",
1207
+ setSaved: "Settings saved",
1208
+ setSaveFailed: "Save failed",
1209
+ setNetworkError: "Network error",
1210
+ setMaintenance: "Maintenance",
1211
+ setRestartBridge: "Restart Bridge",
1212
+ setRestartDesc: "Restart the bridge service. Running sessions will be preserved.",
1213
+ setRestartConfirm: "Are you sure you want to restart Bridge?",
1214
+ setRestarting: "Restarting...",
1215
+ // Node Info
1216
+ nodeTitle: "Node Info",
1217
+ nodeHostname: "Hostname",
1218
+ nodePlatform: "Platform",
1219
+ nodeUptime: "Uptime",
1220
+ nodeVersion: "Bridge Version",
1221
+ nodeNodeVer: "Node Version",
1222
+ nodeConfigPath: "Config Path",
1223
+ nodeHomeDir: "Home Directory",
1224
+ nodeScheduler: "Scheduler",
1225
+ nodeBinding: "Binding",
1226
+ nodeRegistered: "Registration",
1227
+ nodeInstanceId: "Instance ID",
1228
+ nodeActiveSessions: "Active Sessions",
1229
+ nodePaired: "Paired",
1230
+ nodeUnpaired: "Unpaired",
1231
+ nodeRegYes: "Registered",
1232
+ nodeRegNo: "Not Registered",
1233
+ // Logs
1234
+ logTitle: "Logs",
1235
+ logAutoRefresh: "Auto Refresh",
1236
+ logLevelAll: "All",
1237
+ logRefreshing: "Refreshing...",
1238
+ logEmpty: "No logs yet",
1239
+ // Scheduler
1240
+ schedTitle: "Scheduler",
1241
+ schedUrl: "Server URL",
1242
+ schedReRegister: "Re-register",
1243
+ schedReRegistering: "Registering...",
1244
+ schedReRegistered: "Re-registered successfully",
1245
+ schedReRegisterFail: "Re-registration failed",
1246
+ schedDesc: "Re-register with the scheduler. Use when connection is abnormal.",
1247
+ // 状态文本
753
1248
  stHealthy: "Healthy",
754
1249
  stError: "Error",
755
1250
  stRunning: "Running",
@@ -783,6 +1278,10 @@ function toggleLang() {
783
1278
  localStorage.setItem("dashboard-lang", lang);
784
1279
  document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
785
1280
  document.getElementById("langBtn").textContent = t("langName");
1281
+ if (document.getElementById("settingsModal").style.display === "flex") {
1282
+ renderSettingsUI();
1283
+ settingsLang = lang;
1284
+ }
786
1285
  if (lastData) render(lastData);
787
1286
  }
788
1287
 
@@ -815,6 +1314,20 @@ function fmtDuration(seconds) {
815
1314
  parts.push(s + "s");
816
1315
  return parts.join(" ");
817
1316
  }
1317
+ function escHtml(s) {
1318
+ if (typeof s !== "string") return "";
1319
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1320
+ }
1321
+ function fmtShortDate(iso) {
1322
+ if (!iso) return "-";
1323
+ return iso.slice(0, 19).replace("T", " ");
1324
+ }
1325
+ function fmtShortPath(p) {
1326
+ if (!p) return "-";
1327
+ var parts = p.replace(/\\\\/g, "/").split("/").filter(Boolean);
1328
+ if (parts.length <= 2) return p;
1329
+ return parts.slice(-2).join("/");
1330
+ }
818
1331
 
819
1332
  function statusClass(v) {
820
1333
  if (v === true || v === "healthy" || v === "paired" || v === "running" || v === "managed" || v === "registered") return "status-green";
@@ -976,7 +1489,7 @@ function render(data) {
976
1489
  return '<tr><td>' + s.agentId + '</td><td>' + (pid ? '<code>' + pid + '</code>' : '<span class="status-muted">-</span>') + '</td>'
977
1490
  + '<td><span class="' + statusClass(st) + '">' + (t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
978
1491
  + '<td>' + (s.runtimeActionLabel || '-') + '</td>'
979
- + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.split(/[/\\\\]/).pop() : "-") + '</td>'
1492
+ + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-") + '</td>'
980
1493
  + '<td>' + (s.providerId || "-") + '</td></tr>';
981
1494
  }).join("") +
982
1495
  '</tbody></table>';
@@ -1004,9 +1517,9 @@ function render(data) {
1004
1517
  unknownTable.innerHTML = '<div class="empty">' + t("noProcesses") + '</div>';
1005
1518
  } else {
1006
1519
  unknownTable.innerHTML =
1007
- '<table><thead><tr><th>' + t("thPid") + '</th></tr></thead><tbody>' +
1520
+ '<table><thead><tr><th>' + t("thPid") + '</th><th>' + t("thCommand") + '</th><th>' + t("thDirectory") + '</th><th>' + t("thStarted") + '</th><th>' + t("thAction") + '</th></tr></thead><tbody>' +
1008
1521
  unknownProcs.map(p =>
1009
- '<tr><td><code>' + p.pid + '</code></td></tr>'
1522
+ '<tr><td><code>' + p.pid + '</code></td><td>' + escHtml(p.command || "-") + '</td><td title="' + escHtml(p.cwd || "") + '">' + escHtml(fmtShortPath(p.cwd)) + '</td><td>' + (p.startedAt ? fmtShortDate(p.startedAt) : "-") + '</td><td><button class="btn-kill" data-pid="' + p.pid + '">' + t("btnKill") + '</button></td></tr>'
1010
1523
  ).join("") +
1011
1524
  '</tbody></table>';
1012
1525
  }
@@ -1021,7 +1534,7 @@ function render(data) {
1021
1534
  '<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thStatus") + '</th><th>' + t("thWorkspace") + '</th><th>' + t("thCommand") + '</th><th>' + t("thStarted") + '</th></tr></thead><tbody>' +
1022
1535
  persisted.map(s =>
1023
1536
  '<tr><td>' + s.agentId + '</td><td>' + (s.status || "stopped") + '</td>'
1024
- + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.split(/[/\\\\]/).pop() : "-") + '</td>'
1537
+ + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-") + '</td>'
1025
1538
  + '<td>' + (s.command || "-") + '</td>'
1026
1539
  + '<td>' + (s.startedAt ? new Date(s.startedAt).toLocaleString() : "-") + '</td></tr>'
1027
1540
  ).join("") +
@@ -1172,6 +1685,281 @@ render = function(data) {
1172
1685
  checkUpdate();
1173
1686
  // Also check periodically (every 5 minutes)
1174
1687
  setInterval(checkUpdate, 300000);
1688
+
1689
+ // ── Settings Modal ────────────────────────────────────────
1690
+ let settingsLang = null;
1691
+ function renderSettingsUI() {
1692
+ document.getElementById("settingsTitle").textContent = t("setTitle");
1693
+ document.getElementById("navAccount").textContent = t("setAccount");
1694
+ document.getElementById("lblUsername").textContent = t("setUsername");
1695
+ document.getElementById("lblCurrentPwd").textContent = t("setCurrentPwd");
1696
+ document.getElementById("lblNewPwd").textContent = t("setNewPwd");
1697
+ document.getElementById("lblConfirmPwd").textContent = t("setConfirmPwd");
1698
+ document.getElementById("hintKeepUnchanged").textContent = t("setKeepUnchanged");
1699
+ document.getElementById("settingsSaveBtn").textContent = t("setSave");
1700
+ document.getElementById("navMaintenance").textContent = t("setMaintenance");
1701
+ document.getElementById("lblRestartBridge").textContent = t("setRestartBridge");
1702
+ document.getElementById("descRestart").textContent = t("setRestartDesc");
1703
+ document.getElementById("btnRestartBridge").textContent = t("setRestartBridge");
1704
+ document.getElementById("navNode").textContent = t("nodeTitle");
1705
+ document.getElementById("navLogs").textContent = t("logTitle");
1706
+ document.getElementById("navScheduler").textContent = t("schedTitle");
1707
+ document.getElementById("lblNodeHostname").textContent = t("nodeHostname");
1708
+ document.getElementById("lblNodePlatform").textContent = t("nodePlatform");
1709
+ document.getElementById("lblNodeUptime").textContent = t("nodeUptime");
1710
+ document.getElementById("lblNodeVersion").textContent = t("nodeVersion");
1711
+ document.getElementById("lblNodeNodeVer").textContent = t("nodeNodeVer");
1712
+ document.getElementById("lblNodeConfigPath").textContent = t("nodeConfigPath");
1713
+ document.getElementById("lblNodeHomeDir").textContent = t("nodeHomeDir");
1714
+ document.getElementById("lblNodeScheduler").textContent = t("nodeScheduler");
1715
+ document.getElementById("lblNodeBinding").textContent = t("nodeBinding");
1716
+ document.getElementById("lblNodeRegistered").textContent = t("nodeRegistered");
1717
+ document.getElementById("lblNodeInstanceId").textContent = t("nodeInstanceId");
1718
+ document.getElementById("lblNodeActiveSessions").textContent = t("nodeActiveSessions");
1719
+ document.getElementById("lblLogAutoRefresh").textContent = t("logAutoRefresh");
1720
+ document.getElementById("logEmpty").textContent = t("logEmpty");
1721
+ document.getElementById("lblSchedUrl").textContent = t("schedUrl");
1722
+ document.getElementById("descSched").textContent = t("schedDesc");
1723
+ document.getElementById("btnReRegister").textContent = t("schedReRegister");
1724
+ }
1725
+ function switchSettingsPage(page) {
1726
+ document.querySelectorAll(".settings-nav-item").forEach(function(el) { el.classList.remove("active"); });
1727
+ document.querySelectorAll(".settings-page").forEach(function(el) { el.classList.remove("active"); });
1728
+ document.querySelector('.settings-nav-item[data-page="' + page + '"]').classList.add("active");
1729
+ document.getElementById('page' + page.charAt(0).toUpperCase() + page.slice(1)).classList.add("active");
1730
+ }
1731
+ function openSettings() {
1732
+ if (settingsLang !== lang) { renderSettingsUI(); settingsLang = lang; }
1733
+ const modal = document.getElementById("settingsModal");
1734
+ const msg = document.getElementById("settingsMsg");
1735
+ msg.className = "s-msg";
1736
+ msg.style.display = "none";
1737
+ document.getElementById("settingsForm").reset();
1738
+ document.getElementById("settingsSaveBtn").disabled = false;
1739
+ document.getElementById("settingsSaveBtn").textContent = t("setSave");
1740
+ switchSettingsPage("account");
1741
+ // 预填当前用户名
1742
+ fetch("/runtime/credentials-status").then(r => r.json()).then(d => {
1743
+ if (d.ok) document.getElementById("setUsername").value = d.username || "root";
1744
+ }).catch(() => {});
1745
+ modal.style.display = "flex";
1746
+ }
1747
+ function closeSettings(e) {
1748
+ if (e && e.target !== e.currentTarget) return;
1749
+ document.getElementById("settingsModal").style.display = "none";
1750
+ }
1751
+ async function saveSettings(e) {
1752
+ e.preventDefault();
1753
+ const msg = document.getElementById("settingsMsg");
1754
+ const btn = document.getElementById("settingsSaveBtn");
1755
+ msg.className = "s-msg";
1756
+ msg.style.display = "none";
1757
+
1758
+ const username = document.getElementById("setUsername").value.trim();
1759
+ const oldPassword = document.getElementById("setOldPassword").value;
1760
+ const newPassword = document.getElementById("setNewPassword").value;
1761
+ const passwordConfirm = document.getElementById("setPasswordConfirm").value;
1762
+
1763
+ if (newPassword) {
1764
+ if (!oldPassword) {
1765
+ msg.textContent = t("setPwdRequired");
1766
+ msg.className = "s-msg error";
1767
+ return;
1768
+ }
1769
+ if (newPassword !== passwordConfirm) {
1770
+ msg.textContent = t("setPwdMismatch");
1771
+ msg.className = "s-msg error";
1772
+ return;
1773
+ }
1774
+ if (newPassword.length < 4) {
1775
+ msg.textContent = t("setPwdTooShort");
1776
+ msg.className = "s-msg error";
1777
+ return;
1778
+ }
1779
+ }
1780
+
1781
+ btn.disabled = true;
1782
+ btn.textContent = "Saving...";
1783
+
1784
+ try {
1785
+ const resp = await fetch("/runtime/change-credentials", {
1786
+ method: "POST",
1787
+ headers: { "Content-Type": "application/json" },
1788
+ body: JSON.stringify({
1789
+ username,
1790
+ oldPassword,
1791
+ password: newPassword || undefined,
1792
+ passwordConfirm: newPassword || undefined,
1793
+ }),
1794
+ });
1795
+ const data = await resp.json();
1796
+ if (data.ok) {
1797
+ msg.textContent = t("setSaved");
1798
+ msg.className = "s-msg success";
1799
+ setTimeout(() => closeSettings(), 1500);
1800
+ } else {
1801
+ msg.textContent = data.error || t("setSaveFailed");
1802
+ msg.className = "s-msg error";
1803
+ btn.disabled = false;
1804
+ btn.textContent = t("setSave");
1805
+ }
1806
+ } catch (err) {
1807
+ msg.textContent = t("setNetworkError");
1808
+ msg.className = "s-msg error";
1809
+ btn.disabled = false;
1810
+ btn.textContent = t("setSave");
1811
+ }
1812
+ }
1813
+ function confirmRestartBridge() {
1814
+ const msg = document.getElementById("restartMsg");
1815
+ msg.className = "s-restart-msg";
1816
+ msg.style.display = "none";
1817
+ if (!confirm(t("setRestartConfirm"))) return;
1818
+ const btn = document.getElementById("btnRestartBridge");
1819
+ btn.disabled = true;
1820
+ btn.textContent = t("setRestarting");
1821
+ msg.textContent = t("setRestarting");
1822
+ msg.className = "s-restart-msg info";
1823
+ doRestartBridge();
1824
+ }
1825
+
1826
+ // ── Node Info ─────────────────────────────────────────────
1827
+ let nodeInfoTimer = null;
1828
+ function refreshNodeInfo() {
1829
+ fetch("/runtime/node-info").then(function(r) { return r.json(); }).then(function(d) {
1830
+ if (!d.ok) return;
1831
+ document.getElementById("valNodeHostname").textContent = d.hostname;
1832
+ document.getElementById("valNodePlatform").textContent = d.platform + " " + d.arch;
1833
+ document.getElementById("valNodeUptime").textContent = fmtDuration(d.uptime);
1834
+ document.getElementById("valNodeVersion").textContent = d.bridgeVersion;
1835
+ document.getElementById("valNodeNodeVer").textContent = d.nodeVersion;
1836
+ document.getElementById("valNodeConfigPath").textContent = d.configPath;
1837
+ document.getElementById("valNodeHomeDir").textContent = d.homeDir;
1838
+ document.getElementById("valNodeScheduler").textContent = d.schedulerBaseUrl || "-";
1839
+ document.getElementById("valNodeBinding").textContent = d.paired ? t("nodePaired") : t("nodeUnpaired");
1840
+ document.getElementById("valNodeRegistered").textContent = d.registered ? t("nodeRegYes") : t("nodeRegNo");
1841
+ document.getElementById("valNodeInstanceId").textContent = d.instanceId || "-";
1842
+ document.getElementById("valNodeActiveSessions").textContent = d.activeSessions;
1843
+ }).catch(function() {});
1844
+ }
1845
+
1846
+ // ── Logs ──────────────────────────────────────────────────
1847
+ let logTimer = null;
1848
+ function refreshLogs() {
1849
+ var level = document.getElementById("logLevelFilter").value;
1850
+ var url = "/runtime/logs?count=300";
1851
+ if (level) url += "&level=" + level;
1852
+ document.getElementById("btnRefreshLogs").textContent = t("logRefreshing");
1853
+ fetch(url).then(function(r) { return r.json(); }).then(function(d) {
1854
+ if (!d.ok) return;
1855
+ var list = document.getElementById("logList");
1856
+ if (!d.logs || d.logs.length === 0) {
1857
+ list.innerHTML = '<div class="s-log-empty">' + t("logEmpty") + '</div>';
1858
+ return;
1859
+ }
1860
+ var html = "";
1861
+ for (var i = 0; i < d.logs.length; i++) {
1862
+ var e = d.logs[i];
1863
+ html += '<div class="s-log-entry"><span class="ts">' + escHtml(e.timestamp.slice(0, 19).replace("T", " ")) + '</span> <span class="ll ' + e.level.toUpperCase() + '">[' + e.level.toUpperCase() + ']</span>';
1864
+ if (e.prefix) html += ' [' + escHtml(e.prefix) + ']';
1865
+ html += ' ' + escHtml(e.message) + '</div>';
1866
+ }
1867
+ list.innerHTML = html;
1868
+ list.scrollTop = 0;
1869
+ }).catch(function() {}).then(function() {
1870
+ document.getElementById("btnRefreshLogs").textContent = "\u21bb";
1871
+ });
1872
+ }
1873
+ function startLogAutoRefresh() {
1874
+ stopLogAutoRefresh();
1875
+ if (document.getElementById("logAutoRefresh").checked) {
1876
+ logTimer = setInterval(refreshLogs, 5000);
1877
+ }
1878
+ }
1879
+ function stopLogAutoRefresh() {
1880
+ if (logTimer) { clearInterval(logTimer); logTimer = null; }
1881
+ }
1882
+ // wire log auto-refresh toggle
1883
+ document.addEventListener("change", function(e) {
1884
+ if (e.target && e.target.id === "logAutoRefresh") {
1885
+ if (e.target.checked) { refreshLogs(); startLogAutoRefresh(); }
1886
+ else { stopLogAutoRefresh(); }
1887
+ }
1888
+ });
1889
+
1890
+ // ── Scheduler ─────────────────────────────────────────────
1891
+ function doReRegister() {
1892
+ var btn = document.getElementById("btnReRegister");
1893
+ var msg = document.getElementById("schedMsg");
1894
+ msg.className = "s-msg";
1895
+ msg.style.display = "none";
1896
+ btn.disabled = true;
1897
+ btn.textContent = t("schedReRegistering");
1898
+ fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1899
+ .then(function(r) { return r.json(); })
1900
+ .then(function(d) {
1901
+ if (d.ok) {
1902
+ msg.textContent = t("schedReRegistered");
1903
+ msg.className = "s-msg success";
1904
+ refreshNodeInfo();
1905
+ } else {
1906
+ msg.textContent = t("schedReRegisterFail") + (d.error ? ": " + d.error : "");
1907
+ msg.className = "s-msg error";
1908
+ }
1909
+ })
1910
+ .catch(function() {
1911
+ msg.textContent = t("schedReRegisterFail");
1912
+ msg.className = "s-msg error";
1913
+ })
1914
+ .then(function() {
1915
+ btn.disabled = false;
1916
+ btn.textContent = t("schedReRegister");
1917
+ });
1918
+ }
1919
+
1920
+ // ── Kill process (unknown processes table) ────────────────
1921
+ function killProcess(pid) {
1922
+ if (!confirm(t("btnKill") + " PID " + pid + "?")) return;
1923
+ var btn = document.querySelector('[data-pid="' + pid + '"]');
1924
+ if (btn) btn.disabled = true;
1925
+ fetch("/runtime/kill-process", {
1926
+ method: "POST",
1927
+ headers: { "Content-Type": "application/json" },
1928
+ body: JSON.stringify({ pid: pid }),
1929
+ })
1930
+ .then(function(r) { return r.json(); })
1931
+ .then(function(d) {
1932
+ if (d.ok) {
1933
+ var row = btn ? btn.closest("tr") : null;
1934
+ if (row) row.style.display = "none";
1935
+ } else {
1936
+ if (btn) btn.disabled = false;
1937
+ alert(t("btnKill") + " " + t("setSaveFailed") + (d.error ? ": " + d.error : ""));
1938
+ }
1939
+ })
1940
+ .catch(function() {
1941
+ if (btn) btn.disabled = false;
1942
+ alert(t("btnKill") + " " + t("setNetworkError"));
1943
+ });
1944
+ }
1945
+ // Event delegation for kill buttons
1946
+ document.addEventListener("click", function(e) {
1947
+ var btn = e.target.closest ? e.target.closest(".btn-kill") : null;
1948
+ if (btn && btn.getAttribute("data-pid")) {
1949
+ killProcess(parseInt(btn.getAttribute("data-pid"), 10));
1950
+ }
1951
+ });
1952
+
1953
+ // ── Switch settings page with data refresh ────────────────
1954
+ var __origSwitch = switchSettingsPage;
1955
+ switchSettingsPage = function(page) {
1956
+ __origSwitch(page);
1957
+ stopLogAutoRefresh();
1958
+ if (nodeInfoTimer) { clearTimeout(nodeInfoTimer); nodeInfoTimer = null; }
1959
+ if (page === "node") { refreshNodeInfo(); }
1960
+ else if (page === "logs") { refreshLogs(); startLogAutoRefresh(); }
1961
+ else if (page === "scheduler") { refreshNodeInfo(); }
1962
+ };
1175
1963
  </script>
1176
1964
  </body>
1177
1965
  </html>`;
@@ -1322,7 +2110,12 @@ async function doLogin(e) {
1322
2110
  });
1323
2111
 
1324
2112
  if (resp.ok) {
1325
- window.location.href = "/";
2113
+ const data = await resp.json();
2114
+ if (data.needSetup) {
2115
+ window.location.href = "/setup";
2116
+ } else {
2117
+ window.location.href = "/";
2118
+ }
1326
2119
  return;
1327
2120
  }
1328
2121
 
@@ -1345,3 +2138,294 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
1345
2138
  </script>
1346
2139
  </body>
1347
2140
  </html>`;
2141
+ // ======================================================================
2142
+ // SETUP HTML — 首次登录设置页面
2143
+ // ======================================================================
2144
+ const SETUP_HTML = `<!DOCTYPE html>
2145
+ <html lang="zh-CN">
2146
+ <head>
2147
+ <meta charset="UTF-8">
2148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2149
+ <title>Bridge Dashboard - Setup</title>
2150
+ <style>
2151
+ :root {
2152
+ --bg: #0f1117;
2153
+ --surface: #1a1d2e;
2154
+ --surface2: #232738;
2155
+ --border: #2d3248;
2156
+ --text: #e1e4f0;
2157
+ --text2: #8b90a8;
2158
+ --accent: #5b8def;
2159
+ --accent-hover: #4a7de0;
2160
+ --green: #43c6a8;
2161
+ --red: #f05454;
2162
+ --radius: 10px;
2163
+ }
2164
+ * { margin:0; padding:0; box-sizing:border-box; }
2165
+ body {
2166
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
2167
+ background: var(--bg);
2168
+ color: var(--text);
2169
+ min-height: 100vh;
2170
+ display: flex;
2171
+ align-items: center;
2172
+ justify-content: center;
2173
+ padding: 20px;
2174
+ }
2175
+ .setup-card {
2176
+ background: var(--surface);
2177
+ border: 1px solid var(--border);
2178
+ border-radius: var(--radius);
2179
+ padding: 40px 36px;
2180
+ width: 400px;
2181
+ max-width: 100%;
2182
+ }
2183
+ .setup-card h1 {
2184
+ font-size: 20px;
2185
+ font-weight: 600;
2186
+ margin-bottom: 4px;
2187
+ text-align: center;
2188
+ }
2189
+ .setup-card p.subtitle {
2190
+ font-size: 13px;
2191
+ color: var(--text2);
2192
+ text-align: center;
2193
+ margin-bottom: 24px;
2194
+ }
2195
+ .badge {
2196
+ display: inline-block;
2197
+ background: rgba(240,160,48,0.12);
2198
+ color: #f0a030;
2199
+ font-size: 12px;
2200
+ padding: 2px 10px;
2201
+ border-radius: 4px;
2202
+ margin-bottom: 20px;
2203
+ text-align: center;
2204
+ width: 100%;
2205
+ }
2206
+ .form-group {
2207
+ margin-bottom: 18px;
2208
+ }
2209
+ .form-group label {
2210
+ display: block;
2211
+ font-size: 13px;
2212
+ font-weight: 500;
2213
+ margin-bottom: 6px;
2214
+ color: var(--text2);
2215
+ }
2216
+ .form-group input {
2217
+ width: 100%;
2218
+ padding: 10px 12px;
2219
+ background: var(--surface2);
2220
+ border: 1px solid var(--border);
2221
+ border-radius: 6px;
2222
+ color: var(--text);
2223
+ font-size: 14px;
2224
+ outline: none;
2225
+ transition: border-color .15s;
2226
+ }
2227
+ .form-group input:focus {
2228
+ border-color: var(--accent);
2229
+ }
2230
+ .form-group input::placeholder {
2231
+ color: var(--text2);
2232
+ opacity: 0.6;
2233
+ }
2234
+ .btn-row {
2235
+ display: flex;
2236
+ gap: 12px;
2237
+ margin-top: 24px;
2238
+ }
2239
+ .btn-row button {
2240
+ flex: 1;
2241
+ padding: 10px;
2242
+ border-radius: 6px;
2243
+ font-size: 14px;
2244
+ font-weight: 600;
2245
+ cursor: pointer;
2246
+ transition: all .15s;
2247
+ border: none;
2248
+ }
2249
+ .btn-primary {
2250
+ background: var(--accent);
2251
+ color: #fff;
2252
+ }
2253
+ .btn-primary:hover { background: var(--accent-hover); }
2254
+ .btn-primary:disabled {
2255
+ opacity: 0.5;
2256
+ cursor: not-allowed;
2257
+ }
2258
+ .btn-skip {
2259
+ background: var(--surface2);
2260
+ color: var(--text);
2261
+ border: 1px solid var(--border) !important;
2262
+ }
2263
+ .btn-skip:hover { background: var(--border); }
2264
+ .error-msg {
2265
+ background: rgba(240,84,84,0.12);
2266
+ color: var(--red);
2267
+ padding: 8px 12px;
2268
+ border-radius: 6px;
2269
+ font-size: 13px;
2270
+ margin-bottom: 16px;
2271
+ display: none;
2272
+ }
2273
+ .error-msg.visible { display: block; }
2274
+ .success-msg {
2275
+ background: rgba(67,198,168,0.12);
2276
+ color: var(--green);
2277
+ padding: 8px 12px;
2278
+ border-radius: 6px;
2279
+ font-size: 13px;
2280
+ margin-bottom: 16px;
2281
+ display: none;
2282
+ text-align: center;
2283
+ }
2284
+ .success-msg.visible { display: block; }
2285
+ .hint {
2286
+ font-size: 12px;
2287
+ color: var(--text2);
2288
+ margin-top: 16px;
2289
+ text-align: center;
2290
+ line-height: 1.6;
2291
+ }
2292
+ .spinner {
2293
+ display: inline-block;
2294
+ width: 14px;
2295
+ height: 14px;
2296
+ border: 2px solid var(--text2);
2297
+ border-top-color: var(--accent);
2298
+ border-radius: 50%;
2299
+ animation: spin .6s linear infinite;
2300
+ vertical-align: middle;
2301
+ margin-right: 6px;
2302
+ }
2303
+ @keyframes spin { to { transform: rotate(360deg); } }
2304
+ </style>
2305
+ </head>
2306
+ <body>
2307
+ <div class="setup-card" id="setupCard">
2308
+ <h1>Bridge Dashboard</h1>
2309
+ <p class="subtitle">First-time setup — 首次登录设置</p>
2310
+ <div class="badge">Default credentials detected. Please set a new username and password.<br>检测到默认凭据,请设置新的用户名和密码</div>
2311
+ <div class="error-msg" id="errorMsg"></div>
2312
+ <div class="success-msg" id="successMsg">Credentials saved! Redirecting... / 凭据已保存,正在跳转...</div>
2313
+ <form id="setupForm">
2314
+ <div class="form-group">
2315
+ <label for="username">Username / 用户名</label>
2316
+ <input type="text" id="username" name="username" value="root" autocomplete="username" autofocus>
2317
+ </div>
2318
+ <div class="form-group">
2319
+ <label for="password">New Password / 新密码</label>
2320
+ <input type="password" id="password" name="password" placeholder="Enter new password" autocomplete="new-password">
2321
+ </div>
2322
+ <div class="form-group">
2323
+ <label for="passwordConfirm">Confirm Password / 确认密码</label>
2324
+ <input type="password" id="passwordConfirm" name="passwordConfirm" placeholder="Enter password again" autocomplete="new-password">
2325
+ </div>
2326
+ <div class="btn-row">
2327
+ <button type="button" class="btn-skip" id="skipBtn" onclick="doSkip()">Skip / 跳过</button>
2328
+ <button type="submit" class="btn-primary" id="saveBtn">Save / 保存</button>
2329
+ </div>
2330
+ </form>
2331
+ <div class="hint">Skip to keep default credentials <b>root/root</b>. You can change them later via <code>~/.aws-bridge/config.json</code>.<br>跳过以保留默认凭据 <b>root/root</b>。</div>
2332
+ </div>
2333
+ <script>
2334
+ async function doSave(e) {
2335
+ e.preventDefault();
2336
+ const errorEl = document.getElementById("errorMsg");
2337
+ const saveBtn = document.getElementById("saveBtn");
2338
+ errorEl.classList.remove("visible");
2339
+
2340
+ const username = document.getElementById("username").value.trim();
2341
+ const password = document.getElementById("password").value;
2342
+ const passwordConfirm = document.getElementById("passwordConfirm").value;
2343
+
2344
+ if (!username) {
2345
+ errorEl.textContent = "Username is required / 用户名不能为空";
2346
+ errorEl.classList.add("visible");
2347
+ return;
2348
+ }
2349
+ if (!password) {
2350
+ errorEl.textContent = "Password is required / 密码不能为空";
2351
+ errorEl.classList.add("visible");
2352
+ return;
2353
+ }
2354
+ if (password !== passwordConfirm) {
2355
+ errorEl.textContent = "Passwords do not match / 两次输入的密码不一致";
2356
+ errorEl.classList.add("visible");
2357
+ return;
2358
+ }
2359
+ if (password.length < 4) {
2360
+ errorEl.textContent = "Password must be at least 4 characters / 密码至少4位";
2361
+ errorEl.classList.add("visible");
2362
+ return;
2363
+ }
2364
+
2365
+ saveBtn.disabled = true;
2366
+ saveBtn.innerHTML = '<span class="spinner"></span>Saving...';
2367
+
2368
+ try {
2369
+ const resp = await fetch("/runtime/change-credentials", {
2370
+ method: "POST",
2371
+ headers: { "Content-Type": "application/json" },
2372
+ body: JSON.stringify({ username, password, passwordConfirm }),
2373
+ });
2374
+ const data = await resp.json();
2375
+ if (data.ok) {
2376
+ document.getElementById("successMsg").classList.add("visible");
2377
+ document.getElementById("setupForm").style.display = "none";
2378
+ document.querySelector(".hint").style.display = "none";
2379
+ document.querySelector(".badge").style.display = "none";
2380
+ setTimeout(() => { window.location.href = "/"; }, 1500);
2381
+ } else {
2382
+ errorEl.textContent = data.error || "Save failed";
2383
+ errorEl.classList.add("visible");
2384
+ saveBtn.disabled = false;
2385
+ saveBtn.textContent = "Save / 保存";
2386
+ }
2387
+ } catch (err) {
2388
+ errorEl.textContent = "Network error, please try again / 网络错误,请重试";
2389
+ errorEl.classList.add("visible");
2390
+ saveBtn.disabled = false;
2391
+ saveBtn.textContent = "Save / 保存";
2392
+ }
2393
+ }
2394
+
2395
+ async function doSkip() {
2396
+ const skipBtn = document.getElementById("skipBtn");
2397
+ const errorEl = document.getElementById("errorMsg");
2398
+ skipBtn.disabled = true;
2399
+ skipBtn.textContent = "Skipping... / 跳过中...";
2400
+
2401
+ try {
2402
+ const resp = await fetch("/runtime/change-credentials", {
2403
+ method: "POST",
2404
+ headers: { "Content-Type": "application/json" },
2405
+ body: JSON.stringify({ username: "root", password: "root", passwordConfirm: "root" }),
2406
+ });
2407
+ const data = await resp.json();
2408
+ if (data.ok) {
2409
+ document.getElementById("successMsg").classList.add("visible");
2410
+ document.getElementById("setupForm").style.display = "none";
2411
+ document.querySelector(".hint").style.display = "none";
2412
+ document.querySelector(".badge").style.display = "none";
2413
+ setTimeout(() => { window.location.href = "/"; }, 1500);
2414
+ } else {
2415
+ errorEl.textContent = data.error || "Skip failed";
2416
+ errorEl.classList.add("visible");
2417
+ skipBtn.disabled = false;
2418
+ skipBtn.textContent = "Skip / 跳过";
2419
+ }
2420
+ } catch (err) {
2421
+ errorEl.textContent = "Network error, please try again / 网络错误,请重试";
2422
+ errorEl.classList.add("visible");
2423
+ skipBtn.disabled = false;
2424
+ skipBtn.textContent = "Skip / 跳过";
2425
+ }
2426
+ }
2427
+
2428
+ document.getElementById("setupForm").addEventListener("submit", doSave);
2429
+ </script>
2430
+ </body>
2431
+ </html>`;