aws-runtime-bridge 1.9.10 → 1.9.21

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 (132) 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 +376 -10
  4. package/dist/adapter/AcodeSdkAdapter.test.js +155 -8
  5. package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
  6. package/dist/adapter/ClaudeSdkAdapter.js +17 -1
  7. package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
  8. package/dist/adapter/CodexSdkAdapter.js +15 -1
  9. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  10. package/dist/adapter/OpencodeSdkAdapter.js +56 -4
  11. package/dist/adapter/instance-ai-config-resolver.d.ts +49 -0
  12. package/dist/adapter/instance-ai-config-resolver.d.ts.map +1 -0
  13. package/dist/adapter/instance-ai-config-resolver.js +46 -0
  14. package/dist/adapter/types.d.ts +29 -2
  15. package/dist/adapter/types.d.ts.map +1 -1
  16. package/dist/index.js +5 -1
  17. package/dist/routes/dashboard.d.ts.map +1 -1
  18. package/dist/routes/dashboard.js +749 -15
  19. package/dist/routes/events.d.ts +7 -0
  20. package/dist/routes/events.d.ts.map +1 -0
  21. package/dist/routes/events.js +22 -0
  22. package/dist/routes/file-browser.d.ts +1 -1
  23. package/dist/routes/file-browser.d.ts.map +1 -1
  24. package/dist/routes/file-browser.js +25 -9
  25. package/dist/routes/file-snapshot.d.ts +10 -0
  26. package/dist/routes/file-snapshot.d.ts.map +1 -0
  27. package/dist/routes/file-snapshot.js +156 -0
  28. package/dist/routes/git.d.ts +10 -0
  29. package/dist/routes/git.d.ts.map +1 -1
  30. package/dist/routes/git.js +405 -2
  31. package/dist/routes/git.test.js +39 -2
  32. package/dist/routes/instance.js +3 -1
  33. package/dist/routes/memory.d.ts +3 -0
  34. package/dist/routes/memory.d.ts.map +1 -1
  35. package/dist/routes/memory.js +3 -0
  36. package/dist/routes/terminal.d.ts +17 -3
  37. package/dist/routes/terminal.d.ts.map +1 -1
  38. package/dist/routes/terminal.js +464 -47
  39. package/dist/routes/terminal.test.js +37 -9
  40. package/dist/services/agent-event-store.d.ts +60 -0
  41. package/dist/services/agent-event-store.d.ts.map +1 -0
  42. package/dist/services/agent-event-store.js +68 -0
  43. package/dist/services/event-delta-pusher.d.ts +40 -0
  44. package/dist/services/event-delta-pusher.d.ts.map +1 -0
  45. package/dist/services/event-delta-pusher.js +95 -0
  46. package/dist/services/fileSnapshotStore.d.ts +96 -0
  47. package/dist/services/fileSnapshotStore.d.ts.map +1 -0
  48. package/dist/services/fileSnapshotStore.js +500 -0
  49. package/dist/services/instance-ai-config-service.d.ts +49 -0
  50. package/dist/services/instance-ai-config-service.d.ts.map +1 -0
  51. package/dist/services/instance-ai-config-service.js +174 -0
  52. package/dist/services/panel-auth.d.ts +8 -0
  53. package/dist/services/panel-auth.d.ts.map +1 -1
  54. package/dist/services/panel-auth.js +8 -0
  55. package/dist/services/ring-buffer.d.ts +67 -0
  56. package/dist/services/ring-buffer.d.ts.map +1 -0
  57. package/dist/services/ring-buffer.js +103 -0
  58. package/dist/services/session-output.d.ts +49 -3
  59. package/dist/services/session-output.d.ts.map +1 -1
  60. package/dist/services/session-output.js +115 -2
  61. package/dist/utils/instance-ai-config-debug-logger.d.ts +37 -0
  62. package/dist/utils/instance-ai-config-debug-logger.d.ts.map +1 -0
  63. package/dist/utils/instance-ai-config-debug-logger.js +63 -0
  64. package/dist/utils/logger.d.ts +18 -0
  65. package/dist/utils/logger.d.ts.map +1 -1
  66. package/dist/utils/logger.js +48 -4
  67. package/package/acode/dist/built-in-file-tools.d.ts +1 -1
  68. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  69. package/package/acode/dist/built-in-file-tools.js +5 -3
  70. package/package/acode/dist/builtins/commandArtifacts.d.ts +8 -1
  71. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -1
  72. package/package/acode/dist/builtins/commandArtifacts.js +24 -4
  73. package/package/acode/dist/builtins/shellCommandResult.d.ts +1 -0
  74. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -1
  75. package/package/acode/dist/builtins/shellCommandResult.js +3 -3
  76. package/package/acode/dist/builtins/shellCommandRunner.d.ts +1 -0
  77. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -1
  78. package/package/acode/dist/builtins/shellCommandRunner.js +1 -0
  79. package/package/acode/dist/builtins/shellOutputDecoder.d.ts +25 -2
  80. package/package/acode/dist/builtins/shellOutputDecoder.d.ts.map +1 -1
  81. package/package/acode/dist/builtins/shellOutputDecoder.js +69 -5
  82. package/package/acode/dist/cli.js +2 -0
  83. package/package/acode/dist/runtime.d.ts.map +1 -1
  84. package/package/acode/dist/runtime.js +156 -5
  85. package/package/acode/dist/types.d.ts +1 -0
  86. package/package/acode/dist/types.d.ts.map +1 -1
  87. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +10 -3
  88. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
  89. package/package/aws-client-agent-mcp/dist/agent-client.js +33 -11
  90. package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
  91. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +136 -0
  92. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +1 -0
  93. package/package/aws-client-agent-mcp/dist/context-manager.js +473 -0
  94. package/package/aws-client-agent-mcp/dist/context-manager.js.map +1 -0
  95. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +6 -0
  96. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +1 -0
  97. package/package/aws-client-agent-mcp/dist/context-manager.test.js +307 -0
  98. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +1 -0
  99. package/package/aws-client-agent-mcp/dist/http-client.d.ts +11 -0
  100. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  101. package/package/aws-client-agent-mcp/dist/http-client.js +67 -0
  102. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  103. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +166 -10
  104. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  105. package/package/aws-client-agent-mcp/dist/mcp-server.js +933 -41
  106. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  107. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +150 -130
  108. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  109. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -1
  110. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  111. package/package/aws-client-agent-mcp/dist/mcp-tools.js +62 -2
  112. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  113. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +58 -1
  114. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +1 -1
  115. package/package/aws-client-agent-mcp/dist/memory-store.js +246 -15
  116. package/package/aws-client-agent-mcp/dist/memory-store.js.map +1 -1
  117. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +8 -0
  118. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -1
  119. package/package/aws-client-agent-mcp/dist/memory-tools.js +106 -2
  120. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -1
  121. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +7 -1
  122. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +1 -1
  123. package/package/aws-client-agent-mcp/dist/server-memory-store.js +18 -0
  124. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +1 -1
  125. package/package/aws-client-agent-mcp/dist/types.d.ts +103 -2
  126. package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
  127. package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
  128. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +27 -2
  129. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  130. package/package/aws-client-agent-mcp/dist/websocket-client.js +123 -5
  131. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  132. 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, hasConfiguredCredentials, isDefaultCredentials, setPanelCredentials, } 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
@@ -141,32 +143,162 @@ dashboardRouter.post("/runtime/logout", (req, res) => {
141
143
  dashboardRouter.get("/runtime/credentials-status", (req, res) => {
142
144
  if (!requirePanelAuth(req, res))
143
145
  return;
144
- res.json({ ok: true, isDefault: isDefaultCredentials(), configured: hasConfiguredCredentials() });
146
+ const creds = getPanelCredentials();
147
+ res.json({ ok: true, isDefault: isDefaultCredentials(), configured: hasConfiguredCredentials(), username: creds.username });
145
148
  });
146
149
  /**
147
150
  * POST /runtime/change-credentials
148
151
  * 修改面板登录凭据,写入 ~/.aws-bridge/config.json。
149
- * 需要面板登录会话。密码需两次一致且非空。
152
+ * 需要面板登录会话。修改密码需校验原密码;用户名可同时修改。
150
153
  */
151
154
  dashboardRouter.post("/runtime/change-credentials", (req, res) => {
152
155
  if (!requirePanelAuth(req, res))
153
156
  return;
154
- const { username, password, passwordConfirm } = req.body || {};
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
+ // 至少需要用户名或密码的变更
155
179
  if (!username || !String(username).trim()) {
156
180
  res.status(400).json({ ok: false, error: "username required" });
157
181
  return;
158
182
  }
159
- if (!password || !String(password).trim()) {
160
- res.status(400).json({ ok: false, error: "password required" });
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))
161
194
  return;
162
- }
163
- if (String(password) !== String(passwordConfirm)) {
164
- res.status(400).json({ ok: false, error: "passwords do not match" });
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))
165
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 });
166
248
  }
167
- setPanelCredentials(String(username).trim(), String(password));
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);
168
275
  res.json({ ok: true });
169
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
+ });
170
302
  /**
171
303
  * GET /runtime/dashboard
172
304
  * 聚合 bridge 所有状态数据。使用 2 秒缓存避免频繁 I/O,需要面板登录会话。
@@ -219,6 +351,9 @@ dashboardRouter.get("/runtime/dashboard", async (req, res) => {
219
351
  managementStatus: p.managementStatus,
220
352
  sessionId: record?.sessionId,
221
353
  state: record?.state,
354
+ command: p.command,
355
+ cwd: p.cwd,
356
+ startedAt: p.startedAt,
222
357
  };
223
358
  });
224
359
  const sortOrder = { managed: 0, orphan: 1, unknown: 2 };
@@ -619,6 +754,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
619
754
  <span id="updateIndicator" style="display:none"></span>
620
755
  </span>
621
756
  <span class="lang-btn" id="langBtn" onclick="toggleLang()">中文</span>
757
+ <button onclick="openSettings()" id="settingsBtn" title="Settings / 设置">&#x2699;</button>
622
758
  <button onclick="refresh()" id="refreshBtn">&#x21bb;</button>
623
759
  </div>
624
760
  </div>
@@ -662,6 +798,200 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
662
798
  <div class="section-title" id="sectionPersisted">Persisted Sessions <span class="count" id="persistedCount"></span></div>
663
799
  <div class="table-wrap" id="persistedTable"><div class="empty" id="loadingPersisted">Loading...</div></div>
664
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
+
665
995
  <script>
666
996
  // ── 多语言翻译表 ──────────────────────────────────────────
667
997
  const T = {
@@ -705,6 +1035,7 @@ const T = {
705
1035
  secUnknown: "\u5176\u4ed6\u8fdb\u7a0b",
706
1036
  orphanBanner: "\u4ee5\u4e0b\u8fdb\u7a0b\u4ecd\u5728\u8fd0\u884c\u4f46\u8131\u79bb\u4e86 bridge \u7ba1\u7406",
707
1037
  btnTerminate: "\u7ec8\u6b62",
1038
+ btnKill: "\u6740\u6b7b",
708
1039
  showUnknown: "\u663e\u793a",
709
1040
  hideUnknown: "\u9690\u85cf",
710
1041
  noOrphans: "\u65e0\u5b64\u513f\u8fdb\u7a0b",
@@ -728,8 +1059,62 @@ const T = {
728
1059
  thWorkspace: "\u5de5\u4f5c\u533a",
729
1060
  thProvider: "\u63d0\u4f9b\u65b9",
730
1061
  thCommand: "\u547d\u4ee4",
1062
+ thDirectory: "\u76ee\u5f55",
731
1063
  thStarted: "\u5f00\u59cb\u65f6\u95f4",
732
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",
733
1118
  // 状态文本
734
1119
  stHealthy: "\u5065\u5eb7",
735
1120
  stError: "\u5f02\u5e38",
@@ -780,6 +1165,7 @@ const T = {
780
1165
  secUnknown: "Other Processes",
781
1166
  orphanBanner: "These processes are running but no longer managed by bridge",
782
1167
  btnTerminate: "Terminate",
1168
+ btnKill: "Kill",
783
1169
  showUnknown: "Show",
784
1170
  hideUnknown: "Hide",
785
1171
  noOrphans: "No orphan processes",
@@ -802,8 +1188,63 @@ const T = {
802
1188
  thWorkspace: "Workspace",
803
1189
  thProvider: "Provider",
804
1190
  thCommand: "Command",
1191
+ thDirectory: "Directory",
805
1192
  thStarted: "Started",
806
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
+ // 状态文本
807
1248
  stHealthy: "Healthy",
808
1249
  stError: "Error",
809
1250
  stRunning: "Running",
@@ -837,6 +1278,10 @@ function toggleLang() {
837
1278
  localStorage.setItem("dashboard-lang", lang);
838
1279
  document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
839
1280
  document.getElementById("langBtn").textContent = t("langName");
1281
+ if (document.getElementById("settingsModal").style.display === "flex") {
1282
+ renderSettingsUI();
1283
+ settingsLang = lang;
1284
+ }
840
1285
  if (lastData) render(lastData);
841
1286
  }
842
1287
 
@@ -869,6 +1314,20 @@ function fmtDuration(seconds) {
869
1314
  parts.push(s + "s");
870
1315
  return parts.join(" ");
871
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
+ }
872
1331
 
873
1332
  function statusClass(v) {
874
1333
  if (v === true || v === "healthy" || v === "paired" || v === "running" || v === "managed" || v === "registered") return "status-green";
@@ -1030,7 +1489,7 @@ function render(data) {
1030
1489
  return '<tr><td>' + s.agentId + '</td><td>' + (pid ? '<code>' + pid + '</code>' : '<span class="status-muted">-</span>') + '</td>'
1031
1490
  + '<td><span class="' + statusClass(st) + '">' + (t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
1032
1491
  + '<td>' + (s.runtimeActionLabel || '-') + '</td>'
1033
- + '<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>'
1034
1493
  + '<td>' + (s.providerId || "-") + '</td></tr>';
1035
1494
  }).join("") +
1036
1495
  '</tbody></table>';
@@ -1058,9 +1517,9 @@ function render(data) {
1058
1517
  unknownTable.innerHTML = '<div class="empty">' + t("noProcesses") + '</div>';
1059
1518
  } else {
1060
1519
  unknownTable.innerHTML =
1061
- '<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>' +
1062
1521
  unknownProcs.map(p =>
1063
- '<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>'
1064
1523
  ).join("") +
1065
1524
  '</tbody></table>';
1066
1525
  }
@@ -1075,7 +1534,7 @@ function render(data) {
1075
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>' +
1076
1535
  persisted.map(s =>
1077
1536
  '<tr><td>' + s.agentId + '</td><td>' + (s.status || "stopped") + '</td>'
1078
- + '<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>'
1079
1538
  + '<td>' + (s.command || "-") + '</td>'
1080
1539
  + '<td>' + (s.startedAt ? new Date(s.startedAt).toLocaleString() : "-") + '</td></tr>'
1081
1540
  ).join("") +
@@ -1226,6 +1685,281 @@ render = function(data) {
1226
1685
  checkUpdate();
1227
1686
  // Also check periodically (every 5 minutes)
1228
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
+ };
1229
1963
  </script>
1230
1964
  </body>
1231
1965
  </html>`;