aws-runtime-bridge 1.9.175 → 1.9.180

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 (357) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/README.md +61 -2
  3. package/dist/adapter/AcodeSdkAdapter.d.ts +382 -0
  4. package/dist/adapter/AcodeSdkAdapter.js +3 -3
  5. package/dist/adapter/AdapterRegistry.d.ts +28 -0
  6. package/dist/adapter/ClaudeSdkAdapter.d.ts +71 -0
  7. package/dist/adapter/CodexSdkAdapter.d.ts +14 -0
  8. package/dist/adapter/OpencodeSdkAdapter.d.ts +44 -0
  9. package/dist/adapter/acode-tool-names.d.ts +9 -0
  10. package/dist/adapter/idle-poll-loop.d.ts +20 -0
  11. package/dist/adapter/index.d.ts +3 -0
  12. package/dist/adapter/instance-ai-config-resolver.d.ts +20 -0
  13. package/dist/adapter/prompts.d.ts +12 -0
  14. package/dist/adapter/types.d.ts +201 -0
  15. package/dist/browser/browser-bootstrap.d.ts +15 -0
  16. package/dist/browser/browser-connect-token.d.ts +11 -0
  17. package/dist/browser/browser-frame-encode.d.ts +32 -0
  18. package/dist/browser/browser-pool.d.ts +129 -0
  19. package/dist/browser/browser-profile-store.d.ts +41 -0
  20. package/dist/browser/browser-ws-bridge.d.ts +20 -0
  21. package/dist/browser/built-in-browser-tools.d.ts +44 -0
  22. package/dist/browser/element-locator.d.ts +25 -0
  23. package/dist/browser/element-picker.d.ts +30 -0
  24. package/dist/browser/frame-encode.d.ts +19 -0
  25. package/dist/browser/input-dispatcher.d.ts +35 -0
  26. package/dist/browser/listener-store.d.ts +44 -0
  27. package/dist/browser/page-listener-manager.d.ts +88 -0
  28. package/dist/browser/page-listener-types.d.ts +38 -0
  29. package/dist/browser/screencast-differ.d.ts +10 -0
  30. package/dist/browser/screencast-manager.d.ts +94 -0
  31. package/dist/browser/snapshot-serializer.d.ts +56 -0
  32. package/dist/browser/ssrf-guard.d.ts +44 -0
  33. package/dist/browser/stealth.d.ts +21 -0
  34. package/dist/browser/step-archive.d.ts +13 -0
  35. package/dist/browser/types.d.ts +100 -0
  36. package/dist/browser/ws-server.d.ts +54 -0
  37. package/dist/config.d.ts +138 -2
  38. package/dist/config.js +2 -2
  39. package/dist/desktop/built-in-desktop-tools.d.ts +68 -0
  40. package/dist/desktop/built-in-desktop-tools.js +2 -1
  41. package/dist/desktop/desktop-control.d.ts +142 -0
  42. package/dist/desktop/desktop-control.js +2 -0
  43. package/dist/desktop/desktop-mcp-client.d.ts +28 -0
  44. package/dist/desktop/desktop-staleness.d.ts +66 -0
  45. package/dist/desktop/desktop-staleness.js +2 -0
  46. package/dist/index.d.ts +4 -0
  47. package/dist/index.js +1 -1
  48. package/dist/middleware/auth.d.ts +26 -0
  49. package/dist/middleware/auth.js +1 -1
  50. package/dist/remote-desktop/computer-control.d.ts +3 -0
  51. package/dist/remote-desktop/coords.d.ts +6 -0
  52. package/dist/remote-desktop/frame-backpressure.d.ts +10 -0
  53. package/dist/remote-desktop/frame-encode.d.ts +3 -0
  54. package/dist/remote-desktop/index.js +2 -2
  55. package/dist/remote-desktop/input-dispatcher.d.ts +4 -0
  56. package/dist/remote-desktop/permission-manager.d.ts +3 -0
  57. package/dist/remote-desktop/screen-capture.d.ts +91 -0
  58. package/dist/remote-desktop/session-manager.d.ts +47 -0
  59. package/dist/remote-desktop/session-manager.js +1 -1
  60. package/dist/remote-desktop/system-backend.d.ts +64 -0
  61. package/dist/remote-desktop/types.d.ts +25 -0
  62. package/dist/remote-desktop/ui-automation.d.ts +11 -0
  63. package/dist/remote-desktop/win32-api.d.ts +10 -0
  64. package/dist/routes/acode-todos.d.ts +13 -0
  65. package/dist/routes/agent-state.d.ts +15 -0
  66. package/dist/routes/ai-sources.js +1 -1
  67. package/dist/routes/background-tasks.d.ts +18 -0
  68. package/dist/routes/background-tasks.js +2 -2
  69. package/dist/routes/browser-listeners.d.ts +17 -0
  70. package/dist/routes/browser-sessions.d.ts +10 -0
  71. package/dist/routes/capabilities.js +1 -1
  72. package/dist/routes/dashboard-icon.d.ts +9 -0
  73. package/dist/routes/dashboard-icon.js +2 -0
  74. package/dist/routes/dashboard-theme.d.ts +24 -0
  75. package/dist/routes/dashboard-theme.js +157 -0
  76. package/dist/routes/dashboard.d.ts +9 -0
  77. package/dist/routes/dashboard.js +1988 -625
  78. package/dist/routes/debug-mount.d.ts +16 -0
  79. package/dist/routes/desktop-control.d.ts +15 -0
  80. package/dist/routes/desktop-control.js +2 -0
  81. package/dist/routes/events.d.ts +4 -0
  82. package/dist/routes/file-browser.d.ts +11 -0
  83. package/dist/routes/file-change-diff.d.ts +25 -0
  84. package/dist/routes/file-diff-rollback.d.ts +28 -0
  85. package/dist/routes/file-snapshot.d.ts +7 -0
  86. package/dist/routes/git.d.ts +143 -0
  87. package/dist/routes/instance-tool-status.d.ts +5 -0
  88. package/dist/routes/instance.d.ts +8 -0
  89. package/dist/routes/instance.js +1 -1
  90. package/dist/routes/jdks.d.ts +4 -0
  91. package/dist/routes/launch.d.ts +5 -0
  92. package/dist/routes/lsp-providers.d.ts +11 -0
  93. package/dist/routes/lsp-providers.js +2 -0
  94. package/dist/routes/lsp.d.ts +41 -0
  95. package/dist/routes/lsp.js +4 -2
  96. package/dist/routes/mcp-extra.d.ts +15 -0
  97. package/dist/routes/mcp.d.ts +16 -0
  98. package/dist/routes/mcp.js +1 -1
  99. package/dist/routes/plugins.d.ts +19 -0
  100. package/dist/routes/plugins.js +1 -1
  101. package/dist/routes/processes.d.ts +4 -0
  102. package/dist/routes/properties.d.ts +4 -0
  103. package/dist/routes/pty.d.ts +29 -0
  104. package/dist/routes/runtime-binding.d.ts +3 -0
  105. package/dist/routes/sessions.d.ts +22 -0
  106. package/dist/routes/skills.d.ts +15 -0
  107. package/dist/routes/skills.js +1 -1
  108. package/dist/routes/sub-agents.d.ts +11 -0
  109. package/dist/routes/sub-agents.js +1 -1
  110. package/dist/routes/task-context.d.ts +5 -0
  111. package/dist/routes/tech-stack.d.ts +4 -0
  112. package/dist/routes/terminal.d.ts +131 -0
  113. package/dist/routes/terminal.js +25 -25
  114. package/dist/routes/workspace-index.d.ts +12 -0
  115. package/dist/routes/workspace-index.js +2 -0
  116. package/dist/routes/yml.d.ts +4 -0
  117. package/dist/scripts/migrate-user-scopes.d.ts +26 -0
  118. package/dist/scripts/migrate-user-scopes.js +2 -0
  119. package/dist/services/access-control.d.ts +21 -0
  120. package/dist/services/acode-bg-manager.d.ts +21 -0
  121. package/dist/services/acode-package.d.ts +10 -0
  122. package/dist/services/acode-package.js +1 -1
  123. package/dist/services/agent-capability-sync.d.ts +26 -0
  124. package/dist/services/agent-capability-sync.js +1 -1
  125. package/dist/services/agent-event-store.d.ts +20 -0
  126. package/dist/services/agent-ownership.d.ts +58 -0
  127. package/dist/services/agent-ownership.js +3 -0
  128. package/dist/services/agent-process-manager.d.ts +154 -0
  129. package/dist/services/agent-pty-manager.d.ts +37 -0
  130. package/dist/services/agent-startup-memory.d.ts +34 -0
  131. package/dist/services/agent-startup-memory.js +1 -1
  132. package/dist/services/ai-config-service.d.ts +26 -52
  133. package/dist/services/ai-config-service.js +1 -3
  134. package/dist/services/ai-source-apply.d.ts +7 -15
  135. package/dist/services/ai-source-apply.js +1 -1
  136. package/dist/services/auto-register.d.ts +165 -1
  137. package/dist/services/auto-register.js +1 -1
  138. package/dist/services/aws-client-agent-mcp.d.ts +14 -0
  139. package/dist/services/aws-client-agent-mcp.js +2 -2
  140. package/dist/services/bridge-package-publish-hooks.d.ts +14 -0
  141. package/dist/services/bridge-pairing.d.ts +82 -0
  142. package/dist/services/bridge-pairing.js +2 -0
  143. package/dist/services/bridge-temp-files.d.ts +29 -0
  144. package/dist/services/browser-session-manager.d.ts +71 -0
  145. package/dist/services/capability-config.d.ts +60 -15
  146. package/dist/services/capability-config.js +2 -2
  147. package/dist/services/change-cache-retention.d.ts +9 -0
  148. package/dist/services/changelog.d.ts +9 -0
  149. package/dist/services/cli-commands.d.ts +82 -0
  150. package/dist/services/cli-commands.js +6 -6
  151. package/dist/services/connection-key-rotation.d.ts +27 -0
  152. package/dist/services/connection-key-rotation.js +2 -0
  153. package/dist/services/debug-mount.d.ts +49 -0
  154. package/dist/services/desktop-mcp-manager.d.ts +103 -1
  155. package/dist/services/desktop-mcp-manager.js +1 -1
  156. package/dist/services/event-delta-pusher.d.ts +15 -0
  157. package/dist/services/fileSnapshotStore.d.ts +33 -0
  158. package/dist/services/gitignore.d.ts +9 -0
  159. package/dist/services/instance-ai-config-service.d.ts +9 -0
  160. package/dist/services/instance-ai-config-service.js +1 -1
  161. package/dist/services/instance-features.d.ts +127 -0
  162. package/dist/services/instance-features.js +1 -1
  163. package/dist/services/instance-init-service.d.ts +3 -0
  164. package/dist/services/instance-init-service.js +1 -1
  165. package/dist/services/instance-state.d.ts +23 -0
  166. package/dist/services/jdk-scanner.d.ts +15 -0
  167. package/dist/services/launch-manager.d.ts +138 -0
  168. package/dist/services/launch-metrics.d.ts +42 -0
  169. package/dist/services/launch-streams.d.ts +19 -0
  170. package/dist/services/lifecycle-state.d.ts +5 -0
  171. package/dist/services/log-retention.d.ts +38 -0
  172. package/dist/services/lsp/install-manager.d.ts +55 -0
  173. package/dist/services/lsp/install-manager.js +2 -0
  174. package/dist/services/lsp/provider-registry.d.ts +108 -0
  175. package/dist/services/lsp/provider-registry.js +2 -0
  176. package/dist/services/lsp/uri-rewrite.d.ts +44 -0
  177. package/dist/services/lsp/uri-rewrite.js +2 -0
  178. package/dist/services/lsp-stdio-bridge.d.ts +41 -4
  179. package/dist/services/lsp-stdio-bridge.js +3 -3
  180. package/dist/services/mcp-deploy.d.ts +45 -4
  181. package/dist/services/mcp-deploy.js +4 -4
  182. package/dist/services/mcp-env-injector.d.ts +52 -0
  183. package/dist/services/mcp-launch-binding-queue.d.ts +12 -0
  184. package/dist/services/mcp-package.d.ts +22 -0
  185. package/dist/services/mcp-package.js +1 -1
  186. package/dist/services/mcp-probe-client.d.ts +19 -0
  187. package/dist/services/mcp-probe-client.js +1 -1
  188. package/dist/services/open-doc-events.d.ts +22 -0
  189. package/dist/services/open-doc-registry.d.ts +35 -0
  190. package/dist/services/orphan-monitor.d.ts +43 -0
  191. package/dist/services/panel-auth.d.ts +24 -0
  192. package/dist/services/panel-env.d.ts +28 -0
  193. package/dist/services/plugin-deployment.d.ts +34 -1
  194. package/dist/services/plugin-deployment.js +2 -2
  195. package/dist/services/plugin-event-buffer.d.ts +21 -0
  196. package/dist/services/plugin-fs.d.ts +11 -0
  197. package/dist/services/plugin-handler-adapter.d.ts +13 -0
  198. package/dist/services/plugin-listener-runner.d.ts +10 -0
  199. package/dist/services/plugin-loader.d.ts +15 -0
  200. package/dist/services/plugin-manager.d.ts +95 -9
  201. package/dist/services/plugin-manager.js +4 -4
  202. package/dist/services/plugin-manifest.d.ts +14 -0
  203. package/dist/services/plugin-package.d.ts +13 -0
  204. package/dist/services/plugin-runtime.d.ts +15 -0
  205. package/dist/services/port-detector.d.ts +15 -0
  206. package/dist/services/process-detail.d.ts +3 -0
  207. package/dist/services/process-detector.d.ts +132 -0
  208. package/dist/services/process-registry.d.ts +120 -0
  209. package/dist/services/process-top.d.ts +6 -0
  210. package/dist/services/request-identity.d.ts +77 -0
  211. package/dist/services/request-identity.js +2 -0
  212. package/dist/services/request-invalid-dump.d.ts +4 -0
  213. package/dist/services/ring-buffer.d.ts +33 -0
  214. package/dist/services/runtime-binding.d.ts +56 -4
  215. package/dist/services/runtime-binding.js +4 -4
  216. package/dist/services/runtime-lifecycle-policy.d.ts +17 -0
  217. package/dist/services/runtime-resources-snapshot.d.ts +26 -0
  218. package/dist/services/sdk-tool-file-change-recorder.d.ts +9 -0
  219. package/dist/services/session-output.d.ts +63 -0
  220. package/dist/services/session-output.js +3 -3
  221. package/dist/services/skill-manager.d.ts +42 -5
  222. package/dist/services/skill-manager.js +3 -3
  223. package/dist/services/snapshot-retention.d.ts +23 -0
  224. package/dist/services/startup-config-wizard.d.ts +7 -1
  225. package/dist/services/startup-config-wizard.js +10 -10
  226. package/dist/services/system-metrics.d.ts +3 -0
  227. package/dist/services/tech-stack-scanner.d.ts +12 -0
  228. package/dist/services/terminal-persistence.d.ts +43 -0
  229. package/dist/services/tool-installer.d.ts +12 -0
  230. package/dist/services/tunnel-client.d.ts +57 -2
  231. package/dist/services/tunnel-client.js +1 -1
  232. package/dist/services/update-log.d.ts +6 -0
  233. package/dist/services/update-notifier.d.ts +17 -0
  234. package/dist/services/update-notifier.js +2 -2
  235. package/dist/services/user-scope.d.ts +115 -0
  236. package/dist/services/user-scope.js +2 -0
  237. package/dist/services/windows-service-manager.d.ts +12 -0
  238. package/dist/services/workspace-files.d.ts +64 -0
  239. package/dist/services/workspace-index/index-manager.d.ts +46 -0
  240. package/dist/services/workspace-index/index-manager.js +2 -0
  241. package/dist/services/workspace-index/indexer.d.ts +15 -0
  242. package/dist/services/workspace-index/indexer.js +2 -0
  243. package/dist/services/workspace-index/query.d.ts +74 -0
  244. package/dist/services/workspace-index/query.js +5 -0
  245. package/dist/services/workspace-index/resolver.d.ts +26 -0
  246. package/dist/services/workspace-index/resolver.js +2 -0
  247. package/dist/services/workspace-index/types.d.ts +89 -0
  248. package/dist/services/workspace-index/types.js +2 -0
  249. package/dist/services/workspace-watch.d.ts +4 -0
  250. package/dist/services/workspaceTaskChangeWatcher.d.ts +49 -0
  251. package/dist/services/workspaceTaskChangeWatcher.js +1 -1
  252. package/dist/types.d.ts +9 -0
  253. package/dist/utils/config-write-queue.d.ts +11 -0
  254. package/dist/utils/env-safety.d.ts +21 -0
  255. package/dist/utils/file-utils.d.ts +15 -0
  256. package/dist/utils/instance-ai-config-debug-logger.d.ts +15 -0
  257. package/dist/utils/lifecycle-log.d.ts +5 -0
  258. package/dist/utils/logger.d.ts +13 -0
  259. package/dist/utils/logger.js +2 -1
  260. package/dist/utils/npm-version.d.ts +10 -0
  261. package/dist/utils/path-utils.d.ts +12 -0
  262. package/dist/utils/scheduler-url.d.ts +18 -0
  263. package/dist/utils/scheduler-url.js +2 -0
  264. package/dist/utils/sdk-package-loader.d.ts +13 -0
  265. package/dist/utils/sdk-package-loader.js +1 -1
  266. package/dist/utils/target-identity.d.ts +31 -0
  267. package/dist/utils/target-identity.js +2 -0
  268. package/dist/utils/validation.d.ts +17 -0
  269. package/dist/utils/yaml-utils.d.ts +48 -0
  270. package/package/acode/dist/__tests__/summary-request-helper.d.ts +10 -0
  271. package/package/acode/dist/ask-user-question-tool.d.ts +17 -0
  272. package/package/acode/dist/await-complete-tool.d.ts +12 -0
  273. package/package/acode/dist/background-command-manager.d.ts +105 -0
  274. package/package/acode/dist/background-command-store.d.ts +27 -0
  275. package/package/acode/dist/background-command-tools.d.ts +8 -0
  276. package/package/acode/dist/background-output-ringbuffer.d.ts +15 -0
  277. package/package/acode/dist/built-in-file-tools.d.ts +65 -0
  278. package/package/acode/dist/built-in-symbols-tool.d.ts +3 -0
  279. package/package/acode/dist/builtins/commandArtifacts.d.ts +3 -0
  280. package/package/acode/dist/builtins/shellCommandPreparation.d.ts +17 -0
  281. package/package/acode/dist/builtins/shellCommandRunner.d.ts +4 -0
  282. package/package/acode/dist/builtins/shellOutputDecoder.d.ts +15 -0
  283. package/package/acode/dist/cache-control.d.ts +45 -0
  284. package/package/acode/dist/commands/commandLoader.d.ts +3 -0
  285. package/package/acode/dist/commands/commandParser.d.ts +3 -0
  286. package/package/acode/dist/commands/commandRegistry.d.ts +3 -0
  287. package/package/acode/dist/compaction.d.ts +354 -0
  288. package/package/acode/dist/config.d.ts +3 -0
  289. package/package/acode/dist/context-overflow.d.ts +32 -0
  290. package/package/acode/dist/fs-utils.d.ts +18 -0
  291. package/package/acode/dist/hooks/hookBus.d.ts +3 -0
  292. package/package/acode/dist/image-utils.d.ts +35 -0
  293. package/package/acode/dist/index.d.ts +1 -0
  294. package/package/acode/dist/index.js +1 -1
  295. package/package/acode/dist/mcpToolNames.d.ts +32 -0
  296. package/package/acode/dist/microcompact.d.ts +53 -0
  297. package/package/acode/dist/parsers/ast-cache.d.ts +41 -0
  298. package/package/acode/dist/parsers/file-index.d.ts +83 -0
  299. package/package/acode/dist/parsers/file-index.js +4 -0
  300. package/package/acode/dist/parsers/language-detector.d.ts +4 -0
  301. package/package/acode/dist/parsers/tree-sitter-loader.d.ts +12 -0
  302. package/package/acode/dist/parsers/types.d.ts +11 -0
  303. package/package/acode/dist/paths.d.ts +14 -0
  304. package/package/acode/dist/plugin-types.d.ts +34 -0
  305. package/package/acode/dist/read-file-state.d.ts +15 -0
  306. package/package/acode/dist/runtime-environment.d.ts +16 -0
  307. package/package/acode/dist/runtime.d.ts +824 -9
  308. package/package/acode/dist/runtime.js +22 -22
  309. package/package/acode/dist/skill-tool.d.ts +20 -0
  310. package/package/acode/dist/sub-agent-manager.d.ts +169 -0
  311. package/package/acode/dist/sub-agent-manager.js +1 -1
  312. package/package/acode/dist/sub-agent-store.d.ts +50 -0
  313. package/package/acode/dist/sub-agent-tools.d.ts +15 -0
  314. package/package/acode/dist/sub-agent-types.d.ts +13 -0
  315. package/package/acode/dist/terminal-tool.d.ts +15 -0
  316. package/package/acode/dist/terminal-types.d.ts +10 -0
  317. package/package/acode/dist/todo-store.d.ts +15 -0
  318. package/package/acode/dist/token-estimator.d.ts +17 -0
  319. package/package/acode/dist/tool-allocation.d.ts +113 -0
  320. package/package/acode/dist/tool-concurrency/index.d.ts +8 -0
  321. package/package/acode/dist/tool-concurrency/observability.d.ts +32 -0
  322. package/package/acode/dist/tool-concurrency/resource-lock.d.ts +30 -0
  323. package/package/acode/dist/tool-concurrency/scheduler.d.ts +19 -0
  324. package/package/acode/dist/tool-concurrency/types.d.ts +57 -0
  325. package/package/acode/dist/tool-group-manager.d.ts +195 -0
  326. package/package/acode/dist/tool-group-manager.js +4 -4
  327. package/package/acode/dist/types.d.ts +194 -0
  328. package/package/aws-client-agent-mcp/README.md +130 -215
  329. package/package/aws-client-agent-mcp/dist/ability-market-tools.d.ts +57 -0
  330. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +277 -3
  331. package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
  332. package/package/aws-client-agent-mcp/dist/config.d.ts +18 -0
  333. package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
  334. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +70 -0
  335. package/package/aws-client-agent-mcp/dist/http-client.d.ts +61 -0
  336. package/package/aws-client-agent-mcp/dist/index.d.ts +10 -0
  337. package/package/aws-client-agent-mcp/dist/logger.d.ts +24 -0
  338. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +606 -1
  339. package/package/aws-client-agent-mcp/dist/mcp-server.js +4 -4
  340. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +158 -0
  341. package/package/aws-client-agent-mcp/dist/mcp-tools.js +38 -22
  342. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +81 -0
  343. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +18 -0
  344. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +86 -0
  345. package/package/aws-client-agent-mcp/dist/messageContent.d.ts +24 -0
  346. package/package/aws-client-agent-mcp/dist/office/office-equation.d.ts +19 -0
  347. package/package/aws-client-agent-mcp/dist/office/office-image.d.ts +18 -0
  348. package/package/aws-client-agent-mcp/dist/office/office-workspace.d.ts +29 -0
  349. package/package/aws-client-agent-mcp/dist/parent-lifecycle.d.ts +18 -0
  350. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts +6 -0
  351. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +4 -0
  352. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +41 -0
  353. package/package/aws-client-agent-mcp/dist/tool-groups.d.ts +136 -0
  354. package/package/aws-client-agent-mcp/dist/types.d.ts +123 -0
  355. package/package/aws-client-agent-mcp/dist/watchdog.d.ts +7 -0
  356. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +108 -0
  357. package/package.json +1 -1
@@ -1,146 +1,480 @@
1
- import{execFile as X}from"node:child_process";import v from"node:fs";import k from"node:os";import O from"node:path";import{Router as Q}from"express";import{getBridgeVersion as Z,getRuntimeHomeDir as ee,getUserHomeDir as te}from"../config.js";import{getAccessControlEntries as ne,isValidIpEntry as se,parseAllowIps as re,refreshAccessControlCache as ae}from"../services/access-control.js";import{getAgentProcessManager as E}from"../services/agent-process-manager.js";import{autoRegister as oe,getRegistrationState as z,getAutoRegisterConfigFilePath as T}from"../services/auto-register.js";import{createSession as ie,getPanelCredentials as L,validateSession as D,destroySession as F,validateCredentials as _,hasConfiguredCredentials as P,isDefaultCredentials as de,setPanelCredentials as le,getAllSessions as ce,getActiveSessionCount as ue,checkLoginAllowed as ge,recordLoginFailure as pe,recordLoginSuccess as fe}from"../services/panel-auth.js";import{getRuntimeBindingPublicState as V,getRuntimePairingCode as me,normalizeSchedulerBaseUrl as be}from"../services/runtime-binding.js";import{collectSystemMetrics as he}from"../services/system-metrics.js";import{loadPersistedSessions as ve}from"../services/terminal-persistence.js";import{runExclusiveConfigWrite as J}from"../utils/config-write-queue.js";import{createLogger as ye,getRecentLogs as xe}from"../utils/logger.js";import{sdkSessions as we}from"./terminal.js";const x=ye("dashboard");let A=null;const Se=1e4;let M=null,R=null;const Ie=3e4;async function j(){he().then(t=>{M={data:t,ts:Date.now()}}).catch(()=>{}),E().discoverAllProcesses().then(t=>{R={data:t,ts:Date.now()}}).catch(()=>{}),E().cleanupStaleRegistry().catch(()=>{})}const ke=12e4;let C=null,w=null;function Ce(){w&&(clearTimeout(w),w=null),w=setTimeout(Be,ke),w.unref?.(),C===null&&(C=setInterval(j,Ie),C.unref?.(),j())}function Be(){C&&(clearInterval(C),C=null),w&&(clearTimeout(w),w=null)}const W=Z();async function Ee(){const t=await import("node:fs/promises"),n=(await import("node:path")).join(ee(),".aws-bridge","lifecycle-state.json");try{const s=await t.readFile(n,"utf8"),i=JSON.parse(s),r=Array.isArray(i.operations)?i.operations:[];return r.length===0?null:(r.sort((a,o)=>new Date(o.updatedAt).getTime()-new Date(a.updatedAt).getTime()),r[0]||null)}catch{return null}}const c=Q(),Te=["/runtime/login","/runtime/logout","/runtime/credentials-status","/runtime/change-credentials","/runtime/server-config","/runtime/access-control","/runtime/node-info","/runtime/logs","/runtime/re-register","/runtime/admin","/runtime/kill-process","/runtime/dashboard","/login","/setup"];function Pe(t){return t==="/"?!0:Te.some(e=>t===e||t.startsWith(`${e}/`))}c.use((t,e,n)=>{Pe(t.path)&&Ce(),n()});const q="panel_sid";function Ae(t){const e={};if(!t)return e;for(const n of t.split(";")){const s=n.indexOf("=");if(s===-1)continue;const i=n.slice(0,s).trim(),r=n.slice(s+1).trim();i&&(e[i]=r)}return e}function N(t){return Ae(t.headers.cookie)[q]}function G(t,e,n){const s=t.secure||t.headers["x-forwarded-proto"]==="https";return`${q}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${n}`}function m(t,e){const n=N(t);return n&&D(n)?!0:t.xhr||t.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}c.post("/runtime/login",(t,e)=>{const n=t.socket.remoteAddress||"unknown",s=ge(n);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:i,password:r}=t.body||{};if(!i||!r){e.status(400).json({ok:!1,error:"username and password required"});return}if(!_(String(i),String(r))){pe(n),e.status(401).json({ok:!1,error:"invalid credentials"});return}fe(n);const a=ie(),o=!P();e.setHeader("Set-Cookie",G(t,a,86400)),e.json({ok:!0,needSetup:o})}),c.post("/runtime/logout",(t,e)=>{const n=N(t);n&&F(n),e.setHeader("Set-Cookie",G(t,"",0)),e.json({ok:!0})}),c.get("/runtime/credentials-status",(t,e)=>{if(!m(t,e))return;const n=L();e.json({ok:!0,isDefault:de(),configured:P(),username:n.username})}),c.post("/runtime/change-credentials",async(t,e)=>{if(!m(t,e))return;const{username:n,password:s,passwordConfirm:i,oldPassword:r}=t.body||{};if(s&&String(s).trim()){if(String(s)!==String(i)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(P()){if(!r){e.status(400).json({ok:!1,error:"old password required"});return}const o=L();if(!_(o.username,String(r))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!n||!String(n).trim()){e.status(400).json({ok:!1,error:"username required"});return}const a=s&&String(s).trim()?String(s).trim():L().password;try{await le(String(n).trim(),a),e.json({ok:!0})}catch(o){const l=o;x.error("Failed to change panel credentials:",l.message),e.status(500).json({ok:!1,error:l.message})}});function $(t){const e=typeof t=="string"?t.trim():"";if(e)return e.length<8?"\u2022\u2022\u2022\u2022":"\u2022\u2022\u2022\u2022"+e.slice(-4)}function Y(t){const e=Array.isArray(t.autoRegisterTargets)?t.autoRegisterTargets.filter(s=>!!(s&&typeof s=="object"&&!Array.isArray(s))):[];return e.length>0?e:["serverUrl","schedulerBaseUrl","userKey"].some(s=>!!String(t[s]??"").trim())?[t]:[]}const Re=["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","projectId","roleName","workspacePath","registerAddr","registerIp","virtualIp","tunnel","preferSameLanIp","maxRetries","retryInterval"];function Ne(t){const e={};for(const n of Re)t[n]!==void 0&&(e[n]=t[n]);return e}function Ue(t,e){const n=String(t.connectionKey??"").trim()||String(e??"").trim();return{serverUrl:String(t.serverUrl??t.schedulerBaseUrl??"").trim(),tenantId:String(t.tenantId??"").trim(),instanceName:String(t.instanceName??"").trim(),instanceId:String(t.instanceId??"").trim()||void 0,registerAddr:String(t.registerAddr??"").trim(),tunnel:typeof t.tunnel=="boolean"?t.tunnel:void 0,userKeyMasked:$(t.userKey),userKeySet:!!String(t.userKey??"").trim(),connectionKeyMasked:$(n),connectionKeySet:!!n}}function U(t){return be(t)??String(t||"").trim()}c.get("/runtime/server-config",(t,e)=>{if(!m(t,e))return;const n=T();let s={};if(v.existsSync(n))try{s=JSON.parse(v.readFileSync(n,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON; fix it manually first"});return}const i=String(s.connectionKey??"").trim()||void 0,r=Y(s).map(a=>Ue(a,i));e.json({ok:!0,targets:r})});function H(t,e){const n=new Error(e);return n.status=t,n}c.post("/runtime/server-config",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).targets;if(!Array.isArray(n)){e.status(400).json({ok:!1,error:"targets array is required"});return}const s=[];for(let r=0;r<n.length;r++){const a=n[r];if(!a||typeof a!="object"||Array.isArray(a)){e.status(400).json({ok:!1,error:`targets[${r}] must be an object`});return}const o=a,l=String(o.serverUrl??o.schedulerBaseUrl??"").trim();if(!l){e.status(400).json({ok:!1,error:`targets[${r}].serverUrl is required`});return}try{const d=new URL(l);if(!["http:","https:"].includes(d.protocol))throw new Error("protocol must be http or https")}catch{e.status(400).json({ok:!1,error:`targets[${r}].serverUrl must be a valid http(s) URL`});return}const p=o.tunnel;let f;if(p==="auto"||p===""||p===null||p===void 0)f=void 0;else if(p===!0||p==="true")f=!0;else if(p===!1||p==="false")f=!1;else{e.status(400).json({ok:!1,error:`targets[${r}].tunnel must be auto, true or false`});return}const h=String(o.originalServerUrl??"").trim();s.push({serverUrl:l,originalServerUrl:h&&h!==l?h:"",userKey:String(o.userKey??"").trim(),tenantId:String(o.tenantId??"").trim(),instanceName:String(o.instanceName??"").trim(),connectionKey:String(o.connectionKey??"").trim(),registerAddr:String(o.registerAddr??"").trim(),tunnel:f})}const i=new Set;for(const r of s){const a=U(r.serverUrl);if(i.has(a)){e.status(400).json({ok:!1,error:"duplicate serverUrl in targets"});return}i.add(a)}try{const r=T();await J(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw H(500,"config.json is not valid JSON; fix it manually first")}const o=String(a.connectionKey??"").trim(),l=Y(a),p=l.length>0&&l[0]===a,f=new Map(l.map(d=>[U(String(d.serverUrl??d.schedulerBaseUrl??"").trim()),p?Ne(d):d])),h=[];for(const d of s){let b;if(d.originalServerUrl){const S=U(d.originalServerUrl);b=f.get(S),b&&f.delete(S)}if(!b){const S=U(d.serverUrl);b=f.get(S),b&&f.delete(S)}const u=b?{...b}:{};if(u.serverUrl=d.serverUrl,u.tenantId=d.tenantId,u.instanceName=d.instanceName,u.registerAddr=d.registerAddr,d.tunnel===void 0?delete u.tunnel:u.tunnel=d.tunnel,d.userKey)u.userKey=d.userKey;else if(!String(u.userKey??"").trim())throw H(400,`userKey is required for ${d.serverUrl} (new target without inheritable userKey)`);d.connectionKey?u.connectionKey=d.connectionKey:!String(u.connectionKey??"").trim()&&o&&(u.connectionKey=o),h.push(u)}a.autoRegisterTargets=h;for(const d of["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","registerAddr","registerIp","virtualIp","tunnel"])delete a[d];v.mkdirSync(O.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8")}),x.info("Server connection config updated via panel",{configPath:r,targetCount:s.length}),e.json({ok:!0})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save server connection config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),c.get("/runtime/access-control",(t,e)=>{m(t,e)&&e.json({ok:!0,allowIps:ne(),defaultAllowIps:["0.0.0.0"]})}),c.post("/runtime/access-control",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).allowIps;if(typeof n!="string"&&!Array.isArray(n)){e.status(400).json({ok:!1,error:"allowIps must be a string or an array"});return}const s=re(n),i=s.filter(r=>!se(r));if(i.length>0){e.status(400).json({ok:!1,error:`invalid IP/CIDR entries: ${i.join(", ")}`});return}try{const r=T();await J(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw H(500,"config.json is not valid JSON; fix it manually first")}const o=a.accessControl&&typeof a.accessControl=="object"&&!Array.isArray(a.accessControl)?{...a.accessControl}:{};o.allowIps=s,a.accessControl=o,v.mkdirSync(O.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8"),ae()}),x.info("Access control config updated via panel",{configPath:r,entryCount:s.length}),e.json({ok:!0,allowIps:s})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save access control config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),c.get("/runtime/node-info",(t,e)=>{if(!m(t,e))return;const n=V(),s=z();e.json({ok:!0,hostname:k.hostname(),platform:k.platform(),arch:k.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:W,configPath:T(),homeDir:te(),bindingStatus:n.status,paired:n.paired,instanceId:n.instanceId||s.instanceId,schedulerBaseUrl:n.schedulerBaseUrl,registered:s.registered,pairingCode:n.pairingCode,activeSessions:ue()})}),c.get("/runtime/logs",(t,e)=>{if(!m(t,e))return;const n=t.query.level||void 0,s=Math.min(Number(t.query.count)||200,500),r=n&&["debug","info","warn","error"].includes(n)?n:void 0,a=xe(s,r);e.json({ok:!0,logs:a})}),c.post("/runtime/re-register",async(t,e)=>{if(m(t,e))try{const n=await oe();e.json({ok:n})}catch(n){const s=n;e.status(500).json({ok:!1,error:s.message})}}),c.get("/runtime/admin/sessions",(t,e)=>{if(!m(t,e))return;const n=ce().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:n})}),c.post("/runtime/admin/sessions/:token/destroy",(t,e)=>{m(t,e)&&(F(t.params.token),e.json({ok:!0}))});function Le(t){return new Promise((e,n)=>{const s=k.platform()==="win32",i=s?"taskkill":"kill",r=s?["/PID",String(t),"/F"]:["-9",String(t)];X(i,r,{timeout:5e3},a=>a?n(a):e())})}c.post("/runtime/kill-process",async(t,e)=>{if(!m(t,e))return;const n=Number(t.body.pid);if(!n||!Number.isInteger(n)||n<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=E(),i=()=>{const a=new Set,o=R?.data??[];for(const l of o)typeof l.pid=="number"&&a.add(l.pid);for(const l of s.getAllManagedProcesses())typeof l.pid=="number"&&a.add(l.pid);return a};let r=i();if(r.size===0)try{R={data:await s.discoverAllProcesses(),ts:Date.now()},r=i()}catch{}if(!r.has(n)){x.warn("Rejected kill-process for unrecognized PID",{pid:n}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await Le(n),x.info("Killed process",{pid:n}),e.json({ok:!0})}catch(a){const o=a;x.warn("Failed to kill process",{pid:n,error:o.message}),e.status(500).json({ok:!1,error:o.message})}}),c.get("/runtime/dashboard",async(t,e)=>{if(m(t,e)){if(A&&Date.now()-A.ts<Se)return void e.json(A.data);try{const n=M?.data??null,s=R?.data??null;M||j();const[i,r,a]=await Promise.all([Promise.resolve(V()),Promise.resolve(z()),ve().catch(()=>[])]),l=E().getAllManagedProcesses(),p=Array.from(we.entries()).map(([I,y])=>({sessionId:I,agentId:y.agentId,workspacePath:y.workspacePath,runtimeStatus:y.currentRuntimeStatus||"running",runtimeActionType:y.currentRuntimeActionType,runtimeActionLabel:y.currentRuntimeActionLabel,providerId:y.providerId}));let f={managed:0,orphan:0,unknown:0},h=[];if(s){f={managed:s.filter(g=>g.managementStatus==="managed").length,orphan:s.filter(g=>g.managementStatus==="orphan").length,unknown:s.filter(g=>g.managementStatus==="unknown").length};const I=new Map(l.map(g=>[g.pid,g]));h=s.map(g=>{const B=I.get(g.pid);return{agentId:g.agentId||B?.agentId,pid:g.pid,managementStatus:g.managementStatus,sessionId:B?.sessionId,state:B?.state,command:g.command,cwd:g.cwd,startedAt:g.startedAt}});const y={managed:0,orphan:1,unknown:2};h.sort((g,B)=>(y[g.managementStatus]??9)-(y[B.managementStatus]??9))}const d=new Set(p.map(I=>I.sessionId)),b=a.filter(I=>!d.has(I.sessionId)),u=i,S=await Ee(),K={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:W,hostname:k.hostname(),platform:k.platform(),arch:k.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:u.status,paired:u.paired,instanceId:u.instanceId,userId:u.userId,schedulerBaseUrl:u.schedulerBaseUrl,pairingCode:me()},registration:{registered:r.registered,instanceId:r.instanceId,lastAttempt:r.lastAttempt?.toISOString(),error:r.error},metrics:n,sessions:{active:p,activeCount:p.length,persisted:b,persistedCount:b.length,total:p.length+b.length},processes:{...f,total:f.managed+f.orphan+f.unknown,list:h,registryCount:l.length},lifecycleOperation:S};A={data:K,ts:Date.now()},e.json(K)}catch(n){const s=n;x.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),c.get("/login",(t,e)=>{e.type("text/html; charset=utf-8"),e.send(Me)}),c.get("/",(t,e)=>{const n=N(t);if(!n||!D(n)){e.redirect("/login");return}if(!P()){e.redirect("/setup");return}const s=t.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(De.replace("{{LANG}}",s))}),c.get("/setup",(t,e)=>{const n=N(t);if(!n||!D(n)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(je)});const De=`<!DOCTYPE html>
1
+ import{execFile as ne}from"node:child_process";import y from"node:fs";import B from"node:os";import V from"node:path";import{Router as ae}from"express";import{getBridgeVersion as re,getRuntimeHomeDir as se,getUserHomeDir as oe}from"../config.js";import{getAccessControlEntries as ie,isValidIpEntry as de,parseAllowIps as le,refreshAccessControlCache as ce}from"../services/access-control.js";import{getAgentProcessManager as P}from"../services/agent-process-manager.js";import{autoRegister as ue,getRegistrationState as $,getAutoRegisterConfigFilePath as A}from"../services/auto-register.js";import{createSession as ge,getPanelCredentials as D,validateSession as H,destroySession as W,validateCredentials as J,hasConfiguredCredentials as R,isDefaultCredentials as pe,setPanelCredentials as fe,getAllSessions as me,getActiveSessionCount as he,checkLoginAllowed as be,recordLoginFailure as ve,recordLoginSuccess as ye}from"../services/panel-auth.js";import{getRuntimeBindingPublicState as q,getRuntimePairingCode as xe,normalizeSchedulerBaseUrl as we}from"../services/runtime-binding.js";import{collectSystemMetrics as ke}from"../services/system-metrics.js";import{loadPersistedSessions as Se}from"../services/terminal-persistence.js";import{runExclusiveConfigWrite as G}from"../utils/config-write-queue.js";import{createLogger as Ie,getRecentLogs as Be}from"../utils/logger.js";import{buildTargetKey as T}from"../utils/target-identity.js";import{DASHBOARD_FAVICON_DATA_URI as Y,DASHBOARD_FAVICON_LINK as O}from"./dashboard-icon.js";import{THEME_CSS as j,THEME_BOOT_SCRIPT as z,THEME_FAB_SCRIPT as X}from"./dashboard-theme.js";import{sdkSessions as Ce}from"./terminal.js";const x=Ie("dashboard");let N=null;const Ee=1e4;let M=null,U=null;const Ae=3e4;async function F(){ke().then(t=>{M={data:t,ts:Date.now()}}).catch(()=>{}),P().discoverAllProcesses().then(t=>{U={data:t,ts:Date.now()}}).catch(()=>{}),P().cleanupStaleRegistry().catch(()=>{})}const Te=12e4;let C=null,k=null;function Pe(){k&&(clearTimeout(k),k=null),k=setTimeout(Re,Te),k.unref?.(),C===null&&(C=setInterval(F,Ae),C.unref?.(),F())}function Re(){C&&(clearInterval(C),C=null),k&&(clearTimeout(k),k=null)}const Q=re();async function Ne(){const t=await import("node:fs/promises"),n=(await import("node:path")).join(se(),".aws-bridge","lifecycle-state.json");try{const a=await t.readFile(n,"utf8"),o=JSON.parse(a),r=Array.isArray(o.operations)?o.operations:[];return r.length===0?null:(r.sort((s,d)=>new Date(d.updatedAt).getTime()-new Date(s.updatedAt).getTime()),r[0]||null)}catch{return null}}const f=ae(),Ue=["/runtime/login","/runtime/logout","/runtime/credentials-status","/runtime/change-credentials","/runtime/server-config","/runtime/access-control","/runtime/node-info","/runtime/logs","/runtime/re-register","/runtime/admin","/runtime/kill-process","/runtime/dashboard","/login","/setup"];function Le(t){return t==="/"?!0:Ue.some(e=>t===e||t.startsWith(`${e}/`))}f.use((t,e,n)=>{Le(t.path)&&Pe(),n()});const Z="panel_sid";function Ke(t){const e={};if(!t)return e;for(const n of t.split(";")){const a=n.indexOf("=");if(a===-1)continue;const o=n.slice(0,a).trim(),r=n.slice(a+1).trim();o&&(e[o]=r)}return e}function L(t){return Ke(t.headers.cookie)[Z]}function ee(t,e,n){const a=t.secure||t.headers["x-forwarded-proto"]==="https";return`${Z}=${e}; HttpOnly; SameSite=Lax; Path=/${a?"; Secure":""}; Max-Age=${n}`}function v(t,e){const n=L(t);return n&&H(n)?!0:t.xhr||t.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}f.post("/runtime/login",(t,e)=>{const n=t.socket.remoteAddress||"unknown",a=be(n);if(!a.allowed){e.status(429).set("Retry-After",String(a.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:a.retryAfterSec});return}const{username:o,password:r}=t.body||{};if(!o||!r){e.status(400).json({ok:!1,error:"username and password required"});return}if(!J(String(o),String(r))){ve(n),e.status(401).json({ok:!1,error:"invalid credentials"});return}ye(n);const s=ge(),d=!R();e.setHeader("Set-Cookie",ee(t,s,86400)),e.json({ok:!0,needSetup:d})}),f.post("/runtime/logout",(t,e)=>{const n=L(t);n&&W(n),e.setHeader("Set-Cookie",ee(t,"",0)),e.json({ok:!0})}),f.get("/runtime/credentials-status",(t,e)=>{if(!v(t,e))return;const n=D();e.json({ok:!0,isDefault:pe(),configured:R(),username:n.username})}),f.post("/runtime/change-credentials",async(t,e)=>{if(!v(t,e))return;const{username:n,password:a,passwordConfirm:o,oldPassword:r}=t.body||{};if(a&&String(a).trim()){if(String(a)!==String(o)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(R()){if(!r){e.status(400).json({ok:!1,error:"old password required"});return}const d=D();if(!J(d.username,String(r))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!n||!String(n).trim()){e.status(400).json({ok:!1,error:"username required"});return}const s=a&&String(a).trim()?String(a).trim():D().password;try{await fe(String(n).trim(),s),e.json({ok:!0})}catch(d){const l=d;x.error("Failed to change panel credentials:",l.message),e.status(500).json({ok:!1,error:l.message})}});function te(t){const e=typeof t=="string"?t.trim():"";if(e)return e.length<8?"\u2022\u2022\u2022\u2022":"\u2022\u2022\u2022\u2022"+e.slice(-4)}function _(t){const e=Array.isArray(t.autoRegisterTargets)?t.autoRegisterTargets.filter(a=>!!(a&&typeof a=="object"&&!Array.isArray(a))):[];return e.length>0?e:["serverUrl","schedulerBaseUrl","userKey"].some(a=>!!String(t[a]??"").trim())?[t]:[]}const De=["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","projectId","roleName","workspacePath","registerAddr","registerIp","virtualIp","tunnel","preferSameLanIp","maxRetries","retryInterval"];function He(t){const e={};for(const n of De)t[n]!==void 0&&(e[n]=t[n]);return e}function Oe(t,e){const n=String(t.connectionKey??"").trim()||String(e??"").trim();return{serverUrl:String(t.serverUrl??t.schedulerBaseUrl??"").trim(),tenantId:String(t.tenantId??"").trim(),instanceName:String(t.instanceName??"").trim(),instanceId:String(t.instanceId??"").trim()||void 0,registerAddr:String(t.registerAddr??"").trim(),tunnel:typeof t.tunnel=="boolean"?t.tunnel:void 0,userKeyMasked:te(t.userKey),userKeySet:!!String(t.userKey??"").trim(),connectionKeyMasked:te(n),connectionKeySet:!!n,targetKey:T(String(t.serverUrl??t.schedulerBaseUrl??"").trim(),t.userKey)}}function je(t){return we(t)??String(t||"").trim()}f.get("/runtime/server-config",(t,e)=>{if(!v(t,e))return;const n=A();let a={};if(y.existsSync(n))try{a=JSON.parse(y.readFileSync(n,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON; fix it manually first"});return}const o=String(a.connectionKey??"").trim()||void 0,r=_(a).map(s=>Oe(s,o));e.json({ok:!0,targets:r})}),f.get("/runtime/server-config/connection-key",(t,e)=>{if(!v(t,e))return;const n=String((t.query||{}).targetKey??"").trim();if(!n){e.status(400).json({ok:!1,error:"targetKey is required"});return}const a=A();if(!y.existsSync(a)){e.status(404).json({ok:!1,error:"config.json not found"});return}let o;try{o=JSON.parse(y.readFileSync(a,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON"});return}const r=String(o.connectionKey??"").trim()||void 0,s=_(o).find(l=>T(String(l.serverUrl??l.schedulerBaseUrl??"").trim(),l.userKey)===n),d=String(s?.connectionKey??"").trim()||r||"";if(!d){e.status(404).json({ok:!1,error:"\u8BE5\u76EE\u6807\u5C1A\u672A\u914D\u7F6E\u8FDE\u63A5\u5BC6\u94A5\uFF1B\u542F\u52A8\u4E00\u6B21 Bridge \u4F1A\u81EA\u52A8\u751F\u6210\u5E76\u5199\u5165 config.json"});return}e.setHeader("Cache-Control","no-store"),e.json({ok:!0,connectionKey:d})});function K(t,e){const n=new Error(e);return n.status=t,n}f.post("/runtime/server-config",async(t,e)=>{if(!v(t,e))return;const n=(t.body||{}).targets;if(!Array.isArray(n)){e.status(400).json({ok:!1,error:"targets array is required"});return}const a=[];for(let o=0;o<n.length;o++){const r=n[o];if(!r||typeof r!="object"||Array.isArray(r)){e.status(400).json({ok:!1,error:`targets[${o}] must be an object`});return}const s=r,d=String(s.serverUrl??s.schedulerBaseUrl??"").trim();if(!d){e.status(400).json({ok:!1,error:`targets[${o}].serverUrl is required`});return}try{const w=new URL(d);if(!["http:","https:"].includes(w.protocol))throw new Error("protocol must be http or https")}catch{e.status(400).json({ok:!1,error:`targets[${o}].serverUrl must be a valid http(s) URL`});return}const l=s.tunnel;let m;if(l==="auto"||l===""||l===null||l===void 0)m=void 0;else if(l===!0||l==="true")m=!0;else if(l===!1||l==="false")m=!1;else{e.status(400).json({ok:!1,error:`targets[${o}].tunnel must be auto, true or false`});return}const h=String(s.originalServerUrl??"").trim();a.push({serverUrl:d,originalServerUrl:h&&h!==d?h:"",originalTargetKey:String(s.originalTargetKey??"").trim(),userKey:String(s.userKey??"").trim(),tenantId:String(s.tenantId??"").trim(),instanceName:String(s.instanceName??"").trim(),connectionKey:String(s.connectionKey??"").trim(),registerAddr:String(s.registerAddr??"").trim(),tunnel:m})}try{const o=A();await G(o,()=>{let r={};if(y.existsSync(o))try{r=JSON.parse(y.readFileSync(o,"utf-8"))}catch{throw K(500,"config.json is not valid JSON; fix it manually first")}const s=String(r.connectionKey??"").trim(),d=_(r),l=d.length>0&&d[0]===r,m=new Map;for(const i of d){const u=T(String(i.serverUrl??i.schedulerBaseUrl??"").trim(),i.userKey);m.set(u,l?He(i):i)}const h=[];for(const i of a){let u;const b=g=>{const c=m.get(g);return c&&m.delete(g),c},S=(g,c)=>T(g,c),E=g=>{const c=`${je(g)}#`;return[...m.keys()].filter(I=>I.startsWith(c))};if(i.originalTargetKey&&(u=b(i.originalTargetKey)),!u&&i.originalServerUrl&&(u=b(S(i.originalServerUrl,i.userKey)),!u)){const g=E(i.originalServerUrl);g.length===1&&(u=b(g[0]))}if(u||(u=b(S(i.serverUrl,i.userKey))),!u){const g=E(i.serverUrl);g.length>=1&&(u=b(g[0]))}const p=u?{...u}:{};if(p.serverUrl=i.serverUrl,p.tenantId=i.tenantId,p.instanceName=i.instanceName,p.registerAddr=i.registerAddr,i.tunnel===void 0?delete p.tunnel:p.tunnel=i.tunnel,i.userKey)p.userKey=i.userKey;else if(!String(p.userKey??"").trim())throw K(400,`userKey is required for ${i.serverUrl} (new target without inheritable userKey)`);i.connectionKey?p.connectionKey=i.connectionKey:!String(p.connectionKey??"").trim()&&s&&(p.connectionKey=s),h.push(p)}const w=new Map;for(let i=0;i<h.length;i++){const u=h[i],b=T(String(u.serverUrl??u.schedulerBaseUrl??"").trim(),u.userKey),S=w.get(b);if(S!==void 0)throw K(400,`duplicate target: ${String(u.serverUrl??"")} appears in both targets[${S}] and targets[${i}] with the same API key; the same scheduler address can be added multiple times only with different API keys`);w.set(b,i)}r.autoRegisterTargets=h;for(const i of["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","registerAddr","registerIp","virtualIp","tunnel"])delete r[i];y.mkdirSync(V.dirname(o),{recursive:!0}),y.writeFileSync(o,JSON.stringify(r,null,2),"utf-8")}),x.info("Server connection config updated via panel",{configPath:o,targetCount:a.length}),e.json({ok:!0})}catch(o){const r=o,s=Number(r.status)||500;s>=500&&x.error("Failed to save server connection config:",r.message),e.status(s).json({ok:!1,error:r.message})}}),f.get("/runtime/access-control",(t,e)=>{v(t,e)&&e.json({ok:!0,allowIps:ie(),defaultAllowIps:["0.0.0.0"]})}),f.post("/runtime/access-control",async(t,e)=>{if(!v(t,e))return;const n=(t.body||{}).allowIps;if(typeof n!="string"&&!Array.isArray(n)){e.status(400).json({ok:!1,error:"allowIps must be a string or an array"});return}const a=le(n),o=a.filter(r=>!de(r));if(o.length>0){e.status(400).json({ok:!1,error:`invalid IP/CIDR entries: ${o.join(", ")}`});return}try{const r=A();await G(r,()=>{let s={};if(y.existsSync(r))try{s=JSON.parse(y.readFileSync(r,"utf-8"))}catch{throw K(500,"config.json is not valid JSON; fix it manually first")}const d=s.accessControl&&typeof s.accessControl=="object"&&!Array.isArray(s.accessControl)?{...s.accessControl}:{};d.allowIps=a,s.accessControl=d,y.mkdirSync(V.dirname(r),{recursive:!0}),y.writeFileSync(r,JSON.stringify(s,null,2),"utf-8"),ce()}),x.info("Access control config updated via panel",{configPath:r,entryCount:a.length}),e.json({ok:!0,allowIps:a})}catch(r){const s=r,d=Number(s.status)||500;d>=500&&x.error("Failed to save access control config:",s.message),e.status(d).json({ok:!1,error:s.message})}}),f.get("/runtime/node-info",(t,e)=>{if(!v(t,e))return;const n=q(),a=$();e.json({ok:!0,hostname:B.hostname(),platform:B.platform(),arch:B.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:Q,configPath:A(),homeDir:oe(),bindingStatus:n.status,paired:n.paired,instanceId:n.instanceId||a.instanceId,schedulerBaseUrl:n.schedulerBaseUrl,registered:a.registered,pairingCode:n.pairingCode,activeSessions:he()})}),f.get("/runtime/logs",(t,e)=>{if(!v(t,e))return;const n=t.query.level||void 0,a=Math.min(Number(t.query.count)||200,500),r=n&&["debug","info","warn","error"].includes(n)?n:void 0,s=Be(a,r);e.json({ok:!0,logs:s})}),f.post("/runtime/re-register",async(t,e)=>{if(v(t,e))try{const n=await ue();e.json({ok:n})}catch(n){const a=n;e.status(500).json({ok:!1,error:a.message})}}),f.get("/runtime/admin/sessions",(t,e)=>{if(!v(t,e))return;const n=me().map(a=>({token:a.token.slice(0,8)+"...",createdAt:new Date(a.createdAt).toISOString(),lastUsedAt:new Date(a.lastUsedAt).toISOString(),age:Math.floor((Date.now()-a.createdAt)/1e3)}));e.json({ok:!0,sessions:n})}),f.post("/runtime/admin/sessions/:token/destroy",(t,e)=>{v(t,e)&&(W(t.params.token),e.json({ok:!0}))});function ze(t){return new Promise((e,n)=>{const a=B.platform()==="win32",o=a?"taskkill":"kill",r=a?["/PID",String(t),"/F"]:["-9",String(t)];ne(o,r,{timeout:5e3},s=>s?n(s):e())})}f.post("/runtime/kill-process",async(t,e)=>{if(!v(t,e))return;const n=Number(t.body.pid);if(!n||!Number.isInteger(n)||n<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const a=P(),o=()=>{const s=new Set,d=U?.data??[];for(const l of d)typeof l.pid=="number"&&s.add(l.pid);for(const l of a.getAllManagedProcesses())typeof l.pid=="number"&&s.add(l.pid);return s};let r=o();if(r.size===0)try{U={data:await a.discoverAllProcesses(),ts:Date.now()},r=o()}catch{}if(!r.has(n)){x.warn("Rejected kill-process for unrecognized PID",{pid:n}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await ze(n),x.info("Killed process",{pid:n}),e.json({ok:!0})}catch(s){const d=s;x.warn("Failed to kill process",{pid:n,error:d.message}),e.status(500).json({ok:!1,error:d.message})}}),f.get("/runtime/dashboard",async(t,e)=>{if(v(t,e)){if(N&&Date.now()-N.ts<Ee)return void e.json(N.data);try{const n=M?.data??null,a=U?.data??null;M||F();const[o,r,s]=await Promise.all([Promise.resolve(q()),Promise.resolve($()),Se().catch(()=>[])]),l=P().getAllManagedProcesses(),m=Array.from(Ce.entries()).map(([p,g])=>({sessionId:p,agentId:g.agentId,workspacePath:g.workspacePath,runtimeStatus:g.currentRuntimeStatus||"running",runtimeActionType:g.currentRuntimeActionType,runtimeActionLabel:g.currentRuntimeActionLabel,providerId:g.providerId}));let h={managed:0,orphan:0,unknown:0},w=[];if(a){h={managed:a.filter(c=>c.managementStatus==="managed").length,orphan:a.filter(c=>c.managementStatus==="orphan").length,unknown:a.filter(c=>c.managementStatus==="unknown").length};const p=new Map(l.map(c=>[c.pid,c]));w=a.map(c=>{const I=p.get(c.pid);return{agentId:c.agentId||I?.agentId,pid:c.pid,managementStatus:c.managementStatus,sessionId:I?.sessionId,state:I?.state,command:c.command,cwd:c.cwd,startedAt:c.startedAt}});const g={managed:0,orphan:1,unknown:2};w.sort((c,I)=>(g[c.managementStatus]??9)-(g[I.managementStatus]??9))}const i=new Set(m.map(p=>p.sessionId)),u=s.filter(p=>!i.has(p.sessionId)),b=o,S=await Ne(),E={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:Q,hostname:B.hostname(),platform:B.platform(),arch:B.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:b.status,paired:b.paired,instanceId:b.instanceId,userId:b.userId,schedulerBaseUrl:b.schedulerBaseUrl,pairingCode:xe()},registration:{registered:r.registered,instanceId:r.instanceId,lastAttempt:r.lastAttempt?.toISOString(),error:r.error},metrics:n,sessions:{active:m,activeCount:m.length,persisted:u,persistedCount:u.length,total:m.length+u.length},processes:{...h,total:h.managed+h.orphan+h.unknown,list:w,registryCount:l.length},lifecycleOperation:S};N={data:E,ts:Date.now()},e.json(E)}catch(n){const a=n;x.error("Dashboard aggregation failed:",a.message),e.status(500).json({ok:!1,error:a.message})}}}),f.get("/login",(t,e)=>{e.type("text/html; charset=utf-8"),e.send(Fe)}),f.get("/",(t,e)=>{const n=L(t);if(!n||!H(n)){e.redirect("/login");return}if(!R()){e.redirect("/setup");return}const a=t.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(Me.replace("{{LANG}}",a))}),f.get("/setup",(t,e)=>{const n=L(t);if(!n||!H(n)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(_e)});const Me=`<!DOCTYPE html>
2
2
  <html lang="{{LANG}}">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Bridge Dashboard</title>
7
+ ${O}
8
+ ${z}
7
9
  <style>
8
- :root {
9
- --bg: #0f1117;
10
- --surface: #1a1d2e;
11
- --surface2: #232738;
12
- --border: #2d3248;
13
- --text: #e1e4f0;
14
- --text2: #8b90a8;
15
- --accent: #5b8def;
16
- --accent2: #43c6a8;
17
- --red: #f05454;
18
- --orange: #f0a030;
19
- --green: #43c6a8;
20
- --green-bg: rgba(67,198,168,0.12);
21
- --red-bg: rgba(240,84,84,0.12);
22
- --orange-bg: rgba(240,160,48,0.12);
23
- --radius: 10px;
10
+ /* \u8BBE\u8BA1\u4EE4\u724C\u89C1 dashboard-theme.ts\uFF1A\u9ED8\u8BA4\u6DF1\u8272\uFF0Chtml.light-theme \u5207\u6D45\u8272 */
11
+ ${j}
12
+
13
+ :root { --topbar-h: 56px; }
14
+
15
+ * { margin: 0; padding: 0; box-sizing: border-box; }
16
+
17
+ html {
18
+ scrollbar-width: thin;
19
+ scrollbar-color: var(--border-strong) transparent;
24
20
  }
25
- * { margin:0; padding:0; box-sizing:border-box; }
21
+ html::-webkit-scrollbar { width: 10px; height: 10px; }
22
+ html::-webkit-scrollbar-track { background: transparent; }
23
+ html::-webkit-scrollbar-thumb {
24
+ background: var(--border-strong);
25
+ border: 2px solid var(--bg);
26
+ border-radius: 5px;
27
+ background-clip: content-box;
28
+ }
29
+ html::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: content-box; }
30
+
26
31
  body {
27
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
32
+ font-family: var(--font-sans);
28
33
  background: var(--bg);
29
34
  color: var(--text);
30
- padding: 24px;
31
35
  min-height: 100vh;
32
- }
33
- .header {
36
+ font-size: 14px;
37
+ line-height: 1.5;
38
+ -webkit-font-smoothing: antialiased;
39
+ }
40
+
41
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
42
+ \u9876\u680F
43
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
44
+ .topbar {
45
+ position: sticky;
46
+ top: 0;
47
+ z-index: 50;
48
+ height: var(--topbar-h);
34
49
  display: flex;
35
50
  align-items: center;
36
- justify-content: space-between;
37
- margin-bottom: 24px;
38
- flex-wrap: wrap;
39
- gap: 12px;
51
+ gap: 16px;
52
+ padding: 0 24px;
53
+ background: color-mix(in srgb, var(--bg) 82%, transparent);
54
+ backdrop-filter: blur(12px);
55
+ border-bottom: 1px solid var(--border);
40
56
  }
41
- .header h1 {
42
- font-size: 22px;
43
- font-weight: 600;
57
+
58
+ .brand {
44
59
  display: flex;
45
60
  align-items: center;
46
61
  gap: 10px;
62
+ font-size: 15px;
63
+ font-weight: 650;
64
+ letter-spacing: -0.01em;
65
+ white-space: nowrap;
47
66
  }
48
- .header h1 .dot {
49
- display: inline-block;
50
- width: 10px; height: 10px;
51
- border-radius: 50%;
52
- background: var(--green);
53
- animation: pulse 2s ease-in-out infinite;
67
+ .brand-mark {
68
+ width: 22px;
69
+ height: 22px;
70
+ border-radius: 6px;
71
+ object-fit: contain;
72
+ flex-shrink: 0;
54
73
  }
55
- @keyframes pulse {
56
- 0%, 100% { opacity: 1; }
57
- 50% { opacity: 0.4; }
74
+
75
+ .topbar-meta {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 8px;
79
+ font-size: 12px;
80
+ color: var(--text-3);
81
+ min-width: 0;
58
82
  }
59
- .header-right {
83
+ .meta-sep { opacity: 0.4; }
84
+ .meta-host {
85
+ font-family: var(--font-mono);
86
+ font-size: 11.5px;
87
+ overflow: hidden;
88
+ text-overflow: ellipsis;
89
+ white-space: nowrap;
90
+ max-width: 180px;
91
+ }
92
+
93
+ .topbar-spacer { flex: 1; }
94
+
95
+ .topbar-actions {
60
96
  display: flex;
61
97
  align-items: center;
62
- gap: 12px;
98
+ gap: 6px;
99
+ }
100
+
101
+ .version-area {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 6px;
105
+ font-size: 12px;
106
+ color: var(--text-2);
107
+ font-variant-numeric: tabular-nums;
108
+ }
109
+
110
+ .icon-btn {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ gap: 6px;
115
+ min-width: 32px;
116
+ height: 32px;
117
+ padding: 0 10px;
118
+ background: transparent;
119
+ border: 1px solid transparent;
120
+ border-radius: var(--radius-sm);
121
+ color: var(--text-2);
63
122
  font-size: 13px;
64
- color: var(--text2);
123
+ font-family: inherit;
124
+ cursor: pointer;
125
+ transition: background .15s, color .15s, border-color .15s;
65
126
  }
66
- .header-right .refresh-time { color: var(--text2); }
67
- .header-right button, .header-right .lang-btn {
68
- background: var(--surface2);
69
- border: 1px solid var(--border);
127
+ .icon-btn:hover {
128
+ background: var(--surface-2);
129
+ border-color: var(--border);
70
130
  color: var(--text);
71
- padding: 6px 14px;
72
- border-radius: 6px;
73
- cursor: pointer;
74
- font-size: 13px;
75
- transition: background .15s;
76
131
  }
77
- .header-right button:hover, .header-right .lang-btn:hover {
78
- background: var(--border);
132
+ .icon-btn:focus-visible {
133
+ outline: 2px solid var(--accent);
134
+ outline-offset: 1px;
79
135
  }
80
- .lang-btn.active {
81
- background: var(--accent);
82
- border-color: var(--accent);
136
+ .icon-btn.solid {
137
+ background: var(--surface-2);
138
+ border-color: var(--border);
139
+ color: var(--text);
83
140
  }
141
+ .icon-btn.solid:hover { background: var(--surface-3); }
84
142
 
85
- .grid {
86
- display: grid;
87
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
143
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
144
+ \u4E3B\u5185\u5BB9\u533A
145
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
146
+ .page {
147
+ max-width: 1320px;
148
+ margin: 0 auto;
149
+ padding: 28px 24px 64px;
150
+ }
151
+
152
+ .page-head {
153
+ display: flex;
154
+ align-items: flex-end;
155
+ justify-content: space-between;
88
156
  gap: 16px;
89
157
  margin-bottom: 20px;
158
+ flex-wrap: wrap;
90
159
  }
91
- .card {
160
+ .page-title {
161
+ font-size: 24px;
162
+ font-weight: 680;
163
+ letter-spacing: -0.02em;
164
+ line-height: 1.2;
165
+ }
166
+ .page-sub {
167
+ font-size: 13px;
168
+ color: var(--text-2);
169
+ margin-top: 4px;
170
+ }
171
+
172
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
173
+ \u5065\u5EB7\u72B6\u6001\u603B\u89C8\uFF08hero\uFF09
174
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
175
+ .hero {
92
176
  background: var(--surface);
93
177
  border: 1px solid var(--border);
94
- border-radius: var(--radius);
95
- padding: 18px;
178
+ border-radius: var(--radius-lg);
179
+ box-shadow: var(--shadow-1);
180
+ overflow: hidden;
181
+ margin-bottom: 20px;
96
182
  }
97
- .card-title {
98
- font-size: 12px;
183
+
184
+ .hero-body {
185
+ display: grid;
186
+ grid-template-columns: repeat(3, minmax(0, 1fr));
187
+ gap: 1px;
188
+ background: var(--border);
189
+ }
190
+
191
+ .hero-cell {
192
+ background: var(--surface);
193
+ padding: 18px 20px;
194
+ min-width: 0;
195
+ }
196
+ .hero-cell-label {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 6px;
200
+ font-size: 11px;
99
201
  font-weight: 600;
202
+ letter-spacing: 0.06em;
100
203
  text-transform: uppercase;
101
- letter-spacing: 0.5px;
102
- color: var(--text2);
103
- margin-bottom: 12px;
204
+ color: var(--text-3);
205
+ margin-bottom: 8px;
206
+ }
207
+ .hero-cell-value {
104
208
  display: flex;
105
209
  align-items: center;
106
210
  gap: 8px;
211
+ font-size: 17px;
212
+ font-weight: 620;
213
+ letter-spacing: -0.01em;
214
+ line-height: 1.3;
215
+ min-width: 0;
216
+ }
217
+ .hero-cell-value > span:last-child {
218
+ overflow: hidden;
219
+ text-overflow: ellipsis;
220
+ white-space: nowrap;
221
+ }
222
+ .hero-cell-detail {
223
+ margin-top: 6px;
224
+ font-size: 12px;
225
+ color: var(--text-3);
226
+ font-family: var(--font-mono);
227
+ overflow: hidden;
228
+ text-overflow: ellipsis;
229
+ white-space: nowrap;
230
+ }
231
+
232
+ /* \u72B6\u6001\u70B9 */
233
+ .dot {
234
+ width: 8px;
235
+ height: 8px;
236
+ border-radius: 50%;
237
+ flex-shrink: 0;
238
+ background: var(--text-3);
239
+ }
240
+ .dot.ok { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
241
+ .dot.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
242
+ .dot.danger { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }
243
+ .dot.pulse { animation: pulse 2.4s ease-in-out infinite; }
244
+ @keyframes pulse {
245
+ 0%, 100% { opacity: 1; }
246
+ 50% { opacity: 0.35; }
107
247
  }
108
248
 
109
- .stat-row {
249
+ /* \u9700\u8981\u5904\u7406\u7684\u4E8B\u9879 */
250
+ .attention {
110
251
  display: flex;
111
- justify-content: space-between;
112
252
  align-items: center;
113
- padding: 6px 0;
253
+ gap: 12px;
254
+ padding: 14px 20px;
255
+ background: var(--warn-soft);
256
+ border-top: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
114
257
  font-size: 13px;
115
- border-bottom: 1px solid var(--border);
116
258
  }
117
- .stat-row:last-child { border-bottom: none; }
118
- .stat-label { color: var(--text2); }
119
- .stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }
259
+ .attention[hidden] { display: none; }
260
+ .attention-icon {
261
+ width: 20px;
262
+ height: 20px;
263
+ border-radius: 50%;
264
+ background: var(--warn);
265
+ color: #0b0d13;
266
+ display: grid;
267
+ place-items: center;
268
+ font-size: 12px;
269
+ font-weight: 800;
270
+ flex-shrink: 0;
271
+ }
272
+ .attention-text { flex: 1; min-width: 0; color: var(--text); }
273
+ .attention-text b { font-weight: 650; }
120
274
 
121
- .status-green { color: var(--green); }
122
- .status-orange { color: var(--orange); }
123
- .status-red { color: var(--red); }
124
- .status-muted { color: var(--text2); }
275
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
276
+ KPI \u78C1\u8D34
277
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
278
+ .kpi-grid {
279
+ display: grid;
280
+ grid-template-columns: repeat(4, minmax(0, 1fr));
281
+ gap: 12px;
282
+ margin-bottom: 28px;
283
+ }
284
+ .kpi {
285
+ background: var(--surface);
286
+ border: 1px solid var(--border);
287
+ border-radius: var(--radius);
288
+ padding: 16px 18px;
289
+ min-width: 0;
290
+ transition: border-color .15s;
291
+ }
292
+ .kpi:hover { border-color: var(--border-strong); }
293
+ .kpi-label {
294
+ font-size: 12px;
295
+ color: var(--text-2);
296
+ margin-bottom: 6px;
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 6px;
300
+ }
301
+ .kpi-value {
302
+ font-size: 26px;
303
+ font-weight: 680;
304
+ letter-spacing: -0.03em;
305
+ font-variant-numeric: tabular-nums;
306
+ line-height: 1.1;
307
+ }
308
+ .kpi-value.muted { color: var(--text-3); }
309
+ .kpi-value.ok { color: var(--ok); }
310
+ .kpi-value.warn { color: var(--warn); }
311
+ .kpi-foot {
312
+ font-size: 11.5px;
313
+ color: var(--text-3);
314
+ margin-top: 4px;
315
+ overflow: hidden;
316
+ text-overflow: ellipsis;
317
+ white-space: nowrap;
318
+ }
125
319
 
320
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
321
+ \u533A\u5757\u6807\u9898
322
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
323
+ .section {
324
+ margin-bottom: 28px;
325
+ }
326
+ .section-head {
327
+ display: flex;
328
+ align-items: center;
329
+ gap: 8px;
330
+ margin-bottom: 12px;
331
+ min-height: 24px;
332
+ }
126
333
  .section-title {
127
334
  font-size: 15px;
128
- font-weight: 600;
129
- margin: 24px 0 12px;
335
+ font-weight: 640;
336
+ letter-spacing: -0.01em;
337
+ }
338
+ .count {
339
+ font-size: 12px;
340
+ font-weight: 500;
341
+ color: var(--text-3);
342
+ font-variant-numeric: tabular-nums;
343
+ }
344
+ .section-head .spacer { flex: 1; }
345
+
346
+ .toggle-header {
347
+ cursor: pointer;
348
+ user-select: none;
349
+ display: inline-flex;
350
+ align-items: center;
351
+ gap: 6px;
352
+ border-radius: var(--radius-sm);
353
+ padding: 2px 4px;
354
+ margin-left: -4px;
355
+ transition: background .15s;
356
+ }
357
+ .toggle-header:hover { background: var(--surface-2); }
358
+ .toggle-icon {
359
+ display: inline-block;
360
+ font-size: 9px;
361
+ color: var(--text-3);
362
+ transition: transform .2s;
363
+ }
364
+ .toggle-icon.open { transform: rotate(90deg); }
365
+
366
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
367
+ \u5361\u7247
368
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
369
+ .card {
370
+ background: var(--surface);
371
+ border: 1px solid var(--border);
372
+ border-radius: var(--radius);
373
+ box-shadow: var(--shadow-1);
374
+ overflow: hidden;
375
+ }
376
+ .card-head {
130
377
  display: flex;
131
378
  align-items: center;
132
379
  gap: 8px;
380
+ padding: 14px 18px;
381
+ border-bottom: 1px solid var(--border);
382
+ }
383
+ .card-title {
384
+ font-size: 13px;
385
+ font-weight: 620;
386
+ letter-spacing: -0.01em;
387
+ }
388
+ .card-title .unit {
389
+ font-size: 11.5px;
390
+ font-weight: 500;
391
+ color: var(--text-3);
392
+ margin-left: 6px;
393
+ font-family: var(--font-mono);
394
+ }
395
+
396
+ /* \u8D44\u6E90\u4EEA\u8868 */
397
+ .gauges {
398
+ display: grid;
399
+ grid-template-columns: repeat(4, minmax(0, 1fr));
400
+ gap: 1px;
401
+ background: var(--border);
402
+ }
403
+ .gauge {
404
+ background: var(--surface);
405
+ padding: 16px 18px;
406
+ min-width: 0;
407
+ }
408
+ .gauge-head {
409
+ display: flex;
410
+ align-items: baseline;
411
+ justify-content: space-between;
412
+ gap: 8px;
413
+ margin-bottom: 10px;
414
+ }
415
+ .gauge-name {
416
+ font-size: 11px;
417
+ font-weight: 620;
418
+ letter-spacing: 0.06em;
419
+ text-transform: uppercase;
420
+ color: var(--text-3);
421
+ }
422
+ .gauge-reading {
423
+ font-size: 19px;
424
+ font-weight: 680;
425
+ letter-spacing: -0.02em;
426
+ font-variant-numeric: tabular-nums;
427
+ line-height: 1;
428
+ }
429
+ .gauge-reading.muted { font-size: 15px; color: var(--text-2); font-weight: 600; }
430
+
431
+ .bar {
432
+ height: 5px;
433
+ background: var(--surface-3);
434
+ border-radius: 3px;
435
+ overflow: hidden;
436
+ margin-bottom: 12px;
437
+ }
438
+ .bar-placeholder { background: transparent; }
439
+ .bar-fill {
440
+ height: 100%;
441
+ border-radius: 3px;
442
+ transition: width .45s cubic-bezier(.4, 0, .2, 1);
133
443
  }
134
- .section-title .count {
444
+ .bar-fill.green { background: var(--ok); }
445
+ .bar-fill.orange { background: var(--warn); }
446
+ .bar-fill.red { background: var(--danger); }
447
+
448
+ .gauge-lines {
449
+ display: flex;
450
+ flex-direction: column;
451
+ gap: 5px;
452
+ }
453
+ .gauge-line {
454
+ display: flex;
455
+ justify-content: space-between;
456
+ gap: 10px;
135
457
  font-size: 12px;
136
- color: var(--text2);
137
- font-weight: 400;
458
+ min-width: 0;
459
+ }
460
+ .gauge-line dt { color: var(--text-3); flex-shrink: 0; }
461
+ .gauge-line dd {
462
+ color: var(--text-2);
463
+ font-variant-numeric: tabular-nums;
464
+ text-align: right;
465
+ overflow: hidden;
466
+ text-overflow: ellipsis;
467
+ white-space: nowrap;
138
468
  }
139
469
 
470
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
471
+ \u8868\u683C
472
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
140
473
  .table-wrap {
141
474
  background: var(--surface);
142
475
  border: 1px solid var(--border);
143
476
  border-radius: var(--radius);
477
+ box-shadow: var(--shadow-1);
144
478
  overflow-x: auto;
145
479
  }
146
480
  table {
@@ -148,251 +482,368 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
148
482
  border-collapse: collapse;
149
483
  font-size: 13px;
150
484
  }
151
- th {
485
+ thead th {
152
486
  text-align: left;
153
- padding: 10px 14px;
487
+ padding: 10px 16px;
154
488
  font-size: 11px;
155
- font-weight: 600;
489
+ font-weight: 620;
490
+ letter-spacing: 0.05em;
156
491
  text-transform: uppercase;
157
- letter-spacing: 0.5px;
158
- color: var(--text2);
159
- background: var(--surface2);
492
+ color: var(--text-3);
493
+ background: var(--bg-elevated);
160
494
  border-bottom: 1px solid var(--border);
161
495
  white-space: nowrap;
496
+ position: sticky;
497
+ top: 0;
498
+ z-index: 1;
162
499
  }
163
- td {
164
- padding: 9px 14px;
500
+ tbody td {
501
+ padding: 11px 16px;
165
502
  border-bottom: 1px solid var(--border);
166
503
  white-space: nowrap;
504
+ color: var(--text-2);
505
+ }
506
+ tbody tr:last-child td { border-bottom: none; }
507
+ tbody tr { transition: background .12s; }
508
+ tbody tr:hover { background: var(--surface-2); }
509
+ tbody td.strong { color: var(--text); font-weight: 550; }
510
+ code, .mono {
511
+ font-family: var(--font-mono);
512
+ font-size: 12px;
513
+ color: var(--text);
167
514
  }
168
- tr:last-child td { border-bottom: none; }
169
- tr:hover { background: rgba(255,255,255,0.03); }
170
515
 
171
- .bar-wrap {
516
+ /* \u7A7A\u72B6\u6001 */
517
+ .empty {
172
518
  display: flex;
519
+ flex-direction: column;
173
520
  align-items: center;
174
- gap: 8px;
175
- }
176
- .bar {
177
- flex: 1;
178
- height: 6px;
179
- background: var(--surface2);
180
- border-radius: 3px;
181
- overflow: hidden;
182
- }
183
- .bar-fill {
184
- height: 100%;
185
- border-radius: 3px;
186
- transition: width 0.5s ease;
521
+ gap: 6px;
522
+ padding: 40px 24px;
523
+ text-align: center;
524
+ color: var(--text-3);
525
+ font-size: 13px;
187
526
  }
188
- .bar-fill.green { background: var(--green); }
189
- .bar-fill.orange { background: var(--orange); }
190
- .bar-fill.red { background: var(--red); }
191
- .bar-label { font-size: 12px; min-width: 48px; text-align: right; font-variant-numeric: tabular-nums; }
527
+ .empty-icon { font-size: 22px; opacity: 0.5; line-height: 1; }
528
+ .empty-hint { font-size: 12px; color: var(--text-3); opacity: 0.8; }
192
529
 
530
+ /* \u6807\u7B7E */
193
531
  .tag {
194
- display: inline-block;
195
- padding: 1px 8px;
196
- border-radius: 4px;
197
- font-size: 11px;
532
+ display: inline-flex;
533
+ align-items: center;
534
+ gap: 5px;
535
+ padding: 2px 8px;
536
+ border-radius: 20px;
537
+ font-size: 11.5px;
198
538
  font-weight: 600;
539
+ white-space: nowrap;
199
540
  }
200
- .tag.managed { background: var(--green-bg); color: var(--green); }
201
- .tag.orphan { background: var(--orange-bg); color: var(--orange); }
202
- .tag.unknown { background: rgba(139,144,168,0.15); color: var(--text2); }
203
-
204
- .empty { color: var(--text2); text-align: center; padding: 24px; font-size: 13px; }
205
-
206
- .toggle-header { cursor: pointer; user-select: none; }
207
- .toggle-header:hover { opacity: 0.8; }
208
- .toggle-icon { display: inline-block; transition: transform .2s; font-size: 10px; margin-right: 4px; }
209
- .toggle-icon.open { transform: rotate(90deg); }
210
-
541
+ .tag::before {
542
+ content: "";
543
+ width: 5px;
544
+ height: 5px;
545
+ border-radius: 50%;
546
+ background: currentColor;
547
+ flex-shrink: 0;
548
+ }
549
+ .tag.managed, .tag.green { background: var(--ok-soft); color: var(--ok); }
550
+ .tag.orphan, .tag.orange { background: var(--warn-soft); color: var(--warn); }
551
+ .tag.unknown, .tag.gray { background: var(--surface-3); color: var(--text-2); }
552
+ .tag.red { background: var(--danger-soft); color: var(--danger); }
553
+ .tag.blue { background: var(--info-soft); color: var(--info); }
554
+ .tag.plain::before { display: none; }
555
+
556
+ /* \u72B6\u6001\u6587\u5B57 */
557
+ .status-green { color: var(--ok); font-weight: 550; }
558
+ .status-orange { color: var(--warn); font-weight: 550; }
559
+ .status-red { color: var(--danger); font-weight: 550; }
560
+ .status-muted { color: var(--text-3); }
561
+
562
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
563
+ \u5B64\u513F\u8FDB\u7A0B\u544A\u8B66
564
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
211
565
  .orphan-banner {
212
- background: var(--orange-bg);
213
- border: 1px solid var(--orange);
214
- border-radius: var(--radius);
215
- padding: 12px 16px;
216
- margin-bottom: 12px;
217
- font-size: 13px;
218
566
  display: flex;
219
567
  align-items: center;
220
568
  gap: 10px;
569
+ padding: 12px 16px;
570
+ margin-bottom: 10px;
571
+ background: var(--warn-soft);
572
+ border: 1px solid color-mix(in srgb, var(--warn) 32%, transparent);
573
+ border-radius: var(--radius);
574
+ font-size: 13px;
575
+ }
576
+ .orphan-banner .icon { font-size: 15px; flex-shrink: 0; }
577
+ .orphan-banner .text { flex: 1; min-width: 0; }
578
+ .orphan-banner .action-hint {
579
+ color: var(--warn);
580
+ font-weight: 620;
581
+ font-size: 12px;
582
+ white-space: nowrap;
221
583
  }
222
- .orphan-banner .icon { font-size: 16px; }
223
- .orphan-banner .text { flex: 1; }
224
- .orphan-banner .action-hint { color: var(--orange); font-weight: 600; }
225
584
 
226
- .action-btn {
227
- background: var(--surface2);
585
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
586
+ \u6309\u94AE
587
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
588
+ .btn {
589
+ display: inline-flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ gap: 6px;
593
+ padding: 7px 14px;
594
+ border-radius: var(--radius-sm);
228
595
  border: 1px solid var(--border);
596
+ background: var(--surface-2);
229
597
  color: var(--text);
230
- padding: 2px 10px;
231
- border-radius: 4px;
598
+ font-family: inherit;
599
+ font-size: 12.5px;
600
+ font-weight: 550;
232
601
  cursor: pointer;
233
- font-size: 11px;
234
- transition: background .15s;
602
+ transition: background .15s, border-color .15s, color .15s, opacity .15s;
603
+ white-space: nowrap;
235
604
  }
236
- .action-btn:hover { background: var(--border); }
237
- .action-btn.terminate { border-color: var(--red); color: var(--red); }
238
- .action-btn.terminate:hover { background: var(--red-bg); }
239
-
240
- .summary-line {
241
- display: flex;
242
- gap: 16px;
243
- align-items: center;
244
- margin-bottom: 12px;
245
- font-size: 13px;
246
- flex-wrap: wrap;
605
+ .btn:hover { background: var(--surface-3); border-color: var(--border-strong); }
606
+ .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
607
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; }
608
+ .btn-primary {
609
+ background: var(--accent);
610
+ border-color: var(--accent);
611
+ color: #fff;
247
612
  }
248
- .summary-line .item { display: flex; align-items: center; gap: 4px; }
249
- .summary-line .dot {
250
- display: inline-block;
251
- width: 8px; height: 8px;
252
- border-radius: 50%;
613
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
614
+ .btn-ghost { background: transparent; border-color: transparent; color: var(--text-2); }
615
+ .btn-ghost:hover { background: var(--surface-2); color: var(--text); border-color: transparent; }
616
+ .btn-danger { border-color: color-mix(in srgb, var(--danger) 45%, transparent); color: var(--danger); background: transparent; }
617
+ .btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }
618
+ .btn-sm { padding: 3px 10px; font-size: 11.5px; }
619
+
620
+ .btn-kill {
621
+ padding: 3px 10px;
622
+ border-radius: var(--radius-sm);
623
+ font-size: 11.5px;
624
+ font-weight: 600;
625
+ cursor: pointer;
626
+ border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
627
+ background: transparent;
628
+ color: var(--danger);
629
+ transition: background .15s, border-color .15s;
253
630
  }
254
- .summary-line .dot.green { background: var(--green); }
255
- .summary-line .dot.orange { background: var(--orange); }
256
- .summary-line .dot.gray { background: var(--text2); }
631
+ .btn-kill:hover { background: var(--danger-soft); border-color: var(--danger); }
632
+ .btn-kill:disabled { opacity: 0.4; cursor: not-allowed; }
257
633
 
258
- .version-area {
259
- display: inline-flex;
260
- align-items: center;
261
- gap: 6px;
262
- font-size: 13px;
263
- color: var(--text2);
264
- }
634
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
635
+ \u66F4\u65B0\u6307\u793A\u5668
636
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
265
637
  .update-indicator {
266
638
  font-size: 12px;
267
639
  cursor: pointer;
268
640
  white-space: nowrap;
269
641
  transition: color .15s;
270
- }
271
- .update-indicator:hover { opacity: 0.8; }
272
- .update-indicator.available { color: var(--accent); }
273
- .update-indicator.installing { color: var(--orange); cursor: default; }
274
- .update-indicator.success { color: var(--green); }
275
- .update-indicator.failed { color: var(--red); }
276
-
277
- @media (max-width: 640px) {
278
- body { padding: 12px; }
279
- .grid { grid-template-columns: 1fr; }
280
- th, td { padding: 7px 10px; }
281
- .header h1 { font-size: 18px; }
282
- }
283
-
284
- /* ===== Scrollbar (\u4E3B\u9898\u9002\u914D) ===== */
285
- /* \u9875\u9762\u7EA7\u6EDA\u52A8\u6761\uFF1Apill \u5F62\u6001\uFF08\u8FB9\u6846 + background-clip\uFF09\uFF0C\u4E0E\u6DF1\u8272\u80CC\u666F\u878D\u4E3A\u4E00\u4F53 */
286
- html {
287
- scrollbar-width: thin;
288
- scrollbar-color: color-mix(in srgb, var(--accent) 30%, transparent) var(--bg);
289
- }
290
- html::-webkit-scrollbar {
291
- width: 10px;
292
- background: var(--bg);
293
- }
294
- html::-webkit-scrollbar-thumb {
295
- background: color-mix(in srgb, var(--accent) 30%, transparent);
296
- border: 2px solid var(--bg);
297
- border-radius: 5px;
298
- background-clip: content-box;
299
- }
300
- html::-webkit-scrollbar-thumb:hover {
301
- background: color-mix(in srgb, var(--accent) 50%, transparent);
302
- background-clip: content-box;
303
- }
304
- /* \u5185\u90E8\u6EDA\u52A8\u5BB9\u5668\uFF08\u8868\u683C\u3001\u8BBE\u7F6E\u9762\u677F\u3001\u65E5\u5FD7\u5217\u8868\uFF09\uFF1A6px \u900F\u660E\u8F68\u9053 + accent \u80F6\u56CA\u6ED1\u5757 */
305
- .table-wrap,
306
- .settings-content,
307
- .s-log-list,
308
- .s-table-wrap {
309
- scrollbar-width: thin;
310
- scrollbar-color: color-mix(in srgb, var(--accent) 30%, transparent) transparent;
311
- }
312
- .table-wrap::-webkit-scrollbar,
313
- .settings-content::-webkit-scrollbar,
314
- .s-log-list::-webkit-scrollbar,
315
- .s-table-wrap::-webkit-scrollbar {
316
- width: 6px;
317
- height: 6px;
318
- }
319
- .table-wrap::-webkit-scrollbar-track,
320
- .settings-content::-webkit-scrollbar-track,
321
- .s-log-list::-webkit-scrollbar-track,
322
- .s-table-wrap::-webkit-scrollbar-track {
323
- background: transparent;
324
- }
325
- .table-wrap::-webkit-scrollbar-thumb,
326
- .settings-content::-webkit-scrollbar-thumb,
327
- .s-log-list::-webkit-scrollbar-thumb,
328
- .s-table-wrap::-webkit-scrollbar-thumb {
329
- background: color-mix(in srgb, var(--accent) 30%, transparent);
330
- border-radius: 3px;
331
- }
332
- .table-wrap::-webkit-scrollbar-thumb:hover,
333
- .settings-content::-webkit-scrollbar-thumb:hover,
334
- .s-log-list::-webkit-scrollbar-thumb:hover,
335
- .s-table-wrap::-webkit-scrollbar-thumb:hover {
336
- background: color-mix(in srgb, var(--accent) 50%, transparent);
642
+ border-radius: var(--radius-sm);
643
+ padding: 2px 6px;
644
+ }
645
+ .update-indicator:hover { background: var(--surface-2); }
646
+ .update-indicator.available { color: var(--accent); font-weight: 600; }
647
+ .update-indicator.installing { color: var(--warn); cursor: default; }
648
+ .update-indicator.success { color: var(--ok); }
649
+ .update-indicator.failed { color: var(--danger); }
650
+
651
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
652
+ \u54CD\u5E94\u5F0F
653
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
654
+ @media (max-width: 1080px) {
655
+ .gauges { grid-template-columns: repeat(2, minmax(0, 1fr)); }
656
+ }
657
+
658
+ @media (max-width: 860px) {
659
+ .hero-body { grid-template-columns: 1fr; }
660
+ .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
661
+ .topbar { padding: 0 16px; gap: 10px; }
662
+ .topbar-meta { display: none; }
663
+ .page { padding: 20px 16px 48px; }
664
+ }
665
+
666
+ @media (max-width: 560px) {
667
+ :root { --topbar-h: 52px; }
668
+ body { font-size: 13.5px; }
669
+ .page { padding: 16px 12px 40px; }
670
+ .page-title { font-size: 19px; }
671
+ .kpi-grid { gap: 8px; }
672
+ .kpi { padding: 13px 14px; }
673
+ .kpi-value { font-size: 21px; }
674
+ .gauges { grid-template-columns: 1fr; }
675
+ .hero-cell { padding: 14px 16px; }
676
+ thead th, tbody td { padding: 9px 12px; }
677
+ .brand { font-size: 14px; }
678
+ .icon-btn { min-width: 30px; height: 30px; padding: 0 8px; }
337
679
  }
338
680
  </style>
339
681
  </head>
340
682
  <body>
341
683
 
342
- <div class="header">
343
- <h1><span class="dot"></span>Bridge Dashboard</h1>
344
- <div class="header-right">
345
- <span class="refresh-time" id="refreshTime">-</span>
346
- <span id="bridgeHostname">-</span>
684
+ <!-- \u2550\u2550 \u9876\u680F\uFF1A\u54C1\u724C + \u8FD0\u884C\u5143\u4FE1\u606F + \u5168\u5C40\u64CD\u4F5C \u2550\u2550 -->
685
+ <header class="topbar">
686
+ <div class="brand">
687
+ <img class="brand-mark" src="${Y}" alt="">
688
+ <span>AgentsWorkStudio</span>
689
+ </div>
690
+ <div class="topbar-meta">
691
+ <span class="meta-sep">/</span>
692
+ <span>Bridge</span>
693
+ <span class="meta-sep">\xB7</span>
694
+ <span class="meta-host" id="bridgeHostname">-</span>
695
+ </div>
696
+ <div class="topbar-spacer"></div>
697
+ <div class="topbar-actions">
347
698
  <span class="version-area">
348
699
  <span id="bridgeVersion">v-</span>
349
700
  <span id="updateIndicator" style="display:none"></span>
350
701
  </span>
351
- <span class="lang-btn" id="langBtn" onclick="toggleLang()">\u4E2D\u6587</span>
352
- <button onclick="openSettings()" id="settingsBtn" title="Settings / \u8BBE\u7F6E">&#x2699;</button>
353
- <button onclick="doLogout()" id="logoutBtn" title="Logout / \u9000\u51FA\u767B\u5F55">&#x23FB;</button>
354
- <button onclick="refresh()" id="refreshBtn">&#x21bb;</button>
702
+ <span class="icon-btn" id="themeBtn" onclick="toggleTheme()" title="\u5207\u6362\u4E3B\u9898">&#x263D;</span>
703
+ <span class="icon-btn" id="langBtn" onclick="toggleLang()" title="Switch language">\u4E2D\u6587</span>
704
+ <button class="icon-btn solid" onclick="openSettings()" id="settingsBtn" title="Settings / \u8BBE\u7F6E">&#x2699;</button>
705
+ <button class="icon-btn" onclick="refresh()" id="refreshBtn" title="Refresh / \u5237\u65B0">&#x21bb;</button>
706
+ <button class="icon-btn" onclick="doLogout()" id="logoutBtn" title="Logout / \u9000\u51FA\u767B\u5F55">&#x23FB;</button>
355
707
  </div>
356
- </div>
357
-
358
- <div class="grid" id="statusCards"></div>
708
+ </header>
359
709
 
360
- <div class="section-title" id="sectionSysRes">System Resources <span class="count" id="sysMetricsCount"></span></div>
361
- <div class="grid" id="metricsGrid"></div>
710
+ <main class="page">
362
711
 
363
- <!-- Summary badges for process categories -->
364
- <div class="summary-line" id="summaryLine">
365
- <span class="item"><span class="dot green"></span> <span id="summaryManaged">0 managed</span></span>
366
- <span class="item"><span class="dot orange"></span> <span id="summaryOrphan">0 orphan</span></span>
367
- <span class="item"><span class="dot gray"></span> <span id="summaryUnknown">0 unknown</span></span>
368
- </div>
369
-
370
- <!-- Active Sessions (primary table, includes PID from process list) -->
371
- <div class="section-title" id="sectionSessions">Active Sessions <span class="count" id="sessionCount"></span></div>
372
- <div class="table-wrap" id="sessionTable"><div class="empty" id="loadingSessions">Loading...</div></div>
373
-
374
- <!-- Orphan Processes (shown only when orphans exist) -->
375
- <div id="orphanContainer" style="display:none">
376
- <div class="section-title" id="sectionOrphans">Orphan Processes <span class="count" id="orphanCount"></span></div>
377
- <div class="orphan-banner">
378
- <span class="icon">&#x26A0;</span>
379
- <span class="text" id="orphanBannerText">These processes are running but no longer managed by bridge</span>
380
- <span class="action-hint">Terminate to clean up</span>
712
+ <!-- \u2550\u2550 \u9875\u9762\u6807\u9898 + \u5237\u65B0\u65F6\u95F4 \u2550\u2550 -->
713
+ <div class="page-head">
714
+ <div>
715
+ <div class="page-title" id="pageTitle">\u8FD0\u884C\u603B\u89C8</div>
716
+ <div class="page-sub" id="pageSub">\u672C\u673A Bridge \u72B6\u6001\u4E0E Agent \u4F1A\u8BDD</div>
717
+ </div>
718
+ <div class="page-sub">
719
+ <span id="refreshTime">-</span>
720
+ </div>
381
721
  </div>
382
- <div class="table-wrap" id="orphanTable"><div class="empty">No orphans</div></div>
383
- </div>
384
722
 
385
- <!-- Unknown Processes (collapsible, hidden by default) -->
386
- <div id="unknownContainer">
387
- <div class="section-title toggle-header" id="sectionUnknown" onclick="toggleUnknown()">
388
- <span class="toggle-icon" id="unknownIcon">&#x25B6;</span><span id="unknownLabel"> Other processes</span> <span class="count" id="unknownCount"></span>
389
- </div>
390
- <div class="table-wrap" id="unknownTable" style="display:none"><div class="empty" id="loadingUnknown">Loading...</div></div>
391
- </div>
723
+ <!-- \u2550\u2550 \u5065\u5EB7\u603B\u89C8\uFF1A\u4E09\u4E2A\u6700\u5173\u952E\u7684\u300C\u662F\u5426\u53EF\u7528\u300D\u5224\u65AD \u2550\u2550 -->
724
+ <section class="hero" aria-label="Bridge health">
725
+ <div class="hero-body">
726
+ <div class="hero-cell">
727
+ <div class="hero-cell-label" id="heroServiceLabel">Bridge \u670D\u52A1</div>
728
+ <div class="hero-cell-value">
729
+ <span class="dot ok pulse" id="heroServiceDot"></span>
730
+ <span id="heroServiceValue">\u8FD0\u884C\u4E2D</span>
731
+ </div>
732
+ <div class="hero-cell-detail" id="heroServiceDetail">-</div>
733
+ </div>
734
+ <div class="hero-cell">
735
+ <div class="hero-cell-label" id="heroRegLabel">\u8C03\u5EA6\u4E2D\u5FC3\u6CE8\u518C</div>
736
+ <div class="hero-cell-value">
737
+ <span class="dot" id="heroRegDot"></span>
738
+ <span id="heroRegValue">-</span>
739
+ </div>
740
+ <div class="hero-cell-detail" id="heroRegDetail">-</div>
741
+ </div>
742
+ <div class="hero-cell">
743
+ <div class="hero-cell-label" id="heroBindLabel">\u9762\u677F\u7ED1\u5B9A</div>
744
+ <div class="hero-cell-value">
745
+ <span class="dot" id="heroBindDot"></span>
746
+ <span id="heroBindValue">-</span>
747
+ </div>
748
+ <div class="hero-cell-detail" id="heroBindDetail">-</div>
749
+ </div>
750
+ </div>
751
+ <!-- \u9700\u8981\u7528\u6237\u5904\u7406\u7684\u4E8B\u9879\uFF1A\u9ED8\u8BA4\u9690\u85CF\uFF0C\u6709\u5185\u5BB9\u65F6\u624D\u663E\u793A -->
752
+ <div class="attention" id="attentionBar" hidden>
753
+ <span class="attention-icon">!</span>
754
+ <span class="attention-text" id="attentionText"></span>
755
+ <button class="btn btn-sm" id="attentionAction" style="display:none" onclick="runAttentionAction()"></button>
756
+ </div>
757
+ </section>
758
+
759
+ <!-- \u2550\u2550 KPI \u78C1\u8D34\uFF1A\u56DB\u4E2A\u6570\u5B57\uFF0C\u4E00\u773C\u770B\u5B8C \u2550\u2550 -->
760
+ <section class="kpi-grid" id="kpiGrid" aria-label="Key metrics">
761
+ <div class="kpi">
762
+ <div class="kpi-label" id="kpiActiveLabel">\u6D3B\u8DC3\u4F1A\u8BDD</div>
763
+ <div class="kpi-value muted" id="kpiActive">0</div>
764
+ <div class="kpi-foot" id="kpiActiveFoot">-</div>
765
+ </div>
766
+ <div class="kpi">
767
+ <div class="kpi-label" id="kpiPersistedLabel">\u6301\u4E45\u5316\u4F1A\u8BDD</div>
768
+ <div class="kpi-value" id="kpiPersisted">0</div>
769
+ <div class="kpi-foot" id="kpiPersistedFoot">-</div>
770
+ </div>
771
+ <div class="kpi">
772
+ <div class="kpi-label" id="kpiOrphanLabel">\u5B64\u513F\u8FDB\u7A0B</div>
773
+ <div class="kpi-value muted" id="kpiOrphan">0</div>
774
+ <div class="kpi-foot" id="kpiOrphanFoot">-</div>
775
+ </div>
776
+ <div class="kpi">
777
+ <div class="kpi-label" id="kpiUptimeLabel">\u8FD0\u884C\u65F6\u957F</div>
778
+ <div class="kpi-value" id="kpiUptime">-</div>
779
+ <div class="kpi-foot" id="kpiUptimeFoot">-</div>
780
+ </div>
781
+ </section>
782
+
783
+ <!-- \u2550\u2550 \u7CFB\u7EDF\u8D44\u6E90\uFF1A\u56DB\u4E2A\u4EEA\u8868\uFF0C\u6A2A\u5411\u5BF9\u9F50 \u2550\u2550 -->
784
+ <section class="section">
785
+ <div class="section-head">
786
+ <span class="section-title" id="sectionSysRes">\u7CFB\u7EDF\u8D44\u6E90</span>
787
+ <span class="spacer"></span>
788
+ <span class="count" id="sysMetricsCount"></span>
789
+ </div>
790
+ <div class="card" id="metricsCard">
791
+ <div class="gauges" id="metricsGrid"></div>
792
+ </div>
793
+ </section>
794
+
795
+ <!-- \u2550\u2550 \u6D3B\u8DC3\u4F1A\u8BDD \u2550\u2550 -->
796
+ <section class="section">
797
+ <div class="section-head">
798
+ <span class="section-title" id="sectionSessions">\u6D3B\u8DC3\u4F1A\u8BDD</span>
799
+ <span class="count" id="sessionCount"></span>
800
+ <span class="spacer"></span>
801
+ <!-- \u8FDB\u7A0B\u5206\u7C7B\u5C0F\u8BA1\uFF08\u539F summary-line\uFF0C\u6536\u8FDB\u6807\u9898\u884C\u53F3\u4FA7\uFF09 -->
802
+ <span class="count" id="summaryLine">
803
+ <span id="summaryManaged">0 managed</span> \xB7
804
+ <span id="summaryOrphan">0 orphan</span> \xB7
805
+ <span id="summaryUnknown">0 unknown</span>
806
+ </span>
807
+ </div>
808
+ <div class="table-wrap" id="sessionTable"><div class="empty" id="loadingSessions">Loading...</div></div>
809
+ </section>
810
+
811
+ <!-- \u2550\u2550 \u5B64\u513F\u8FDB\u7A0B\uFF08\u6709\u624D\u663E\u793A\uFF09 \u2550\u2550 -->
812
+ <section class="section" id="orphanContainer" style="display:none">
813
+ <div class="section-head">
814
+ <span class="section-title" id="sectionOrphans">\u5B64\u513F\u8FDB\u7A0B</span>
815
+ <span class="count" id="orphanCount"></span>
816
+ </div>
817
+ <div class="orphan-banner">
818
+ <span class="icon">&#x26A0;</span>
819
+ <span class="text" id="orphanBannerText">These processes are running but no longer managed by bridge</span>
820
+ <span class="action-hint" id="orphanActionHint">Terminate to clean up</span>
821
+ </div>
822
+ <div class="table-wrap" id="orphanTable"><div class="empty">No orphans</div></div>
823
+ </section>
824
+
825
+ <!-- \u2550\u2550 \u5176\u4ED6\u8FDB\u7A0B\uFF08\u9ED8\u8BA4\u6298\u53E0\uFF09 \u2550\u2550 -->
826
+ <section class="section" id="unknownContainer">
827
+ <div class="section-head">
828
+ <span class="section-title toggle-header" id="sectionUnknown" onclick="toggleUnknown()">
829
+ <span class="toggle-icon" id="unknownIcon">&#x25B6;</span>
830
+ <span id="unknownLabel"> \u5176\u4ED6\u8FDB\u7A0B</span>
831
+ <span class="count" id="unknownCount"></span>
832
+ </span>
833
+ </div>
834
+ <div class="table-wrap" id="unknownTable" style="display:none"><div class="empty" id="loadingUnknown">Loading...</div></div>
835
+ </section>
836
+
837
+ <!-- \u2550\u2550 \u6301\u4E45\u5316\u4F1A\u8BDD \u2550\u2550 -->
838
+ <section class="section">
839
+ <div class="section-head">
840
+ <span class="section-title" id="sectionPersisted">\u6301\u4E45\u5316\u4F1A\u8BDD</span>
841
+ <span class="count" id="persistedCount"></span>
842
+ </div>
843
+ <div class="table-wrap" id="persistedTable"><div class="empty" id="loadingPersisted">Loading...</div></div>
844
+ </section>
392
845
 
393
- <!-- Persisted Sessions -->
394
- <div class="section-title" id="sectionPersisted">Persisted Sessions <span class="count" id="persistedCount"></span></div>
395
- <div class="table-wrap" id="persistedTable"><div class="empty" id="loadingPersisted">Loading...</div></div>
846
+ </main>
396
847
 
397
848
  <!-- Settings Modal -->
398
849
  <div class="modal-overlay" id="settingsModal" style="display:none" onclick="closeSettings(event)">
@@ -454,7 +905,7 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
454
905
  <div class="s-info-row"><span class="s-info-label" id="lblNodeActiveSessions"></span><span class="s-info-value" id="valNodeActiveSessions">-</span></div>
455
906
  </div>
456
907
  <div class="s-actions">
457
- <button class="s-btn s-btn-secondary" onclick="refreshNodeInfo()">Refresh / \u{1F504}</button>
908
+ <button class="s-btn s-btn-secondary s-btn-sm" id="btnRefreshNodeInfo" onclick="refreshNodeInfo()"></button>
458
909
  </div>
459
910
  </div>
460
911
  <!-- Logs page -->
@@ -473,6 +924,8 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
473
924
  </div>
474
925
  <!-- Scheduler page -->
475
926
  <div class="settings-page" id="pageScheduler">
927
+ <!-- \u4ECE Dashboard\u300C\u5F15\u5BFC\u914D\u7F6E\u300D\u8DF3\u8F6C\u8FC7\u6765\u65F6\u7684\u5E26\u5165\u63D0\u793A\uFF08\u53EA\u56DE\u586B\u975E\u5BC6\u94A5\u5B57\u6BB5\uFF09 -->
928
+ <div class="s-msg info" id="schedPrefillNotice" style="display:none"></div>
476
929
  <div class="s-field">
477
930
  <label id="lblSchedTargets"></label>
478
931
  <div class="s-target-list" id="schedTargetList"></div>
@@ -481,6 +934,13 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
481
934
  <button class="s-btn s-btn-secondary" id="btnAddTarget" onclick="startAddTarget()"></button>
482
935
  </div>
483
936
  <div class="s-target-form" id="targetForm">
937
+ <!-- \u5B9E\u4F8B\u540D\u79F0\uFF1A\u6700\u5E38\u6539\u7684\u53EF\u9009\u9879\uFF0C\u653E\u5230\u8868\u5355\u6700\u4E0A\u9762 -->
938
+ <div class="s-field">
939
+ <label id="lblSchedInstanceName"></label>
940
+ <input type="text" id="cfgInstanceName" autocomplete="off">
941
+ <span class="s-hint" id="hintSchedInstanceName"></span>
942
+ </div>
943
+ <!-- \u5FC5\u586B\uFF1A\u666E\u901A\u7528\u6237\u53EA\u9700\u8981\u586B\u8FD9\u4E24\u9879 -->
484
944
  <div class="s-field">
485
945
  <label id="lblSchedUrl"></label>
486
946
  <input type="text" id="cfgServerUrl" placeholder="http://192.168.1.100:8080" autocomplete="off">
@@ -491,38 +951,43 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
491
951
  <input type="password" id="cfgUserKey" autocomplete="new-password">
492
952
  <span class="s-hint" id="hintSchedUserKey"></span>
493
953
  </div>
494
- <div class="s-field">
495
- <label id="lblSchedTenantId"></label>
496
- <input type="text" id="cfgTenantId" autocomplete="off">
497
- <span class="s-hint" id="hintSchedTenantId"></span>
498
- </div>
499
- <div class="s-field">
500
- <label id="lblSchedInstanceName"></label>
501
- <input type="text" id="cfgInstanceName" autocomplete="off">
502
- <span class="s-hint" id="hintSchedInstanceName"></span>
503
- </div>
504
- <div class="s-field">
505
- <label id="lblSchedConnKey"></label>
506
- <input type="password" id="cfgConnectionKey" autocomplete="new-password">
507
- <span class="s-hint" id="hintSchedConnKey"></span>
508
- </div>
509
- <div class="s-field">
510
- <label id="lblSchedRegisterAddr"></label>
511
- <input type="text" id="cfgRegisterAddr" placeholder="192.168.1.25:18081" autocomplete="off">
512
- <span class="s-hint" id="hintSchedRegisterAddr"></span>
513
- </div>
514
- <div class="s-field">
515
- <label id="lblSchedTunnel"></label>
516
- <select id="cfgTunnel">
517
- <option value="auto" id="optTunnelAuto"></option>
518
- <option value="true" id="optTunnelOn"></option>
519
- <option value="false" id="optTunnelOff"></option>
520
- </select>
521
- <span class="s-hint" id="hintSchedTunnel"></span>
522
- </div>
523
- <div class="s-field">
524
- <div class="s-info-row"><span class="s-info-label" id="lblSchedInstanceId"></span><span class="s-info-value" id="valSchedInstanceId">-</span></div>
525
- <span class="s-hint" id="hintSchedInstanceId"></span>
954
+ <!-- \u9AD8\u7EA7\u8BBE\u7F6E\uFF1A\u5168\u90E8\u53EF\u9009\u9879\uFF0C\u9ED8\u8BA4\u6536\u7EB3 -->
955
+ <div class="s-advanced" id="schedAdvanced">
956
+ <button type="button" class="s-advanced-head" id="schedAdvancedHead" onclick="toggleSchedAdvanced()" aria-expanded="false" aria-controls="schedAdvancedBody">
957
+ <span class="toggle-icon" id="schedAdvancedIcon">&#x25B6;</span>
958
+ <span id="lblSchedAdvanced"></span>
959
+ <span class="s-advanced-count" id="schedAdvancedCount"></span>
960
+ </button>
961
+ <div class="s-advanced-body" id="schedAdvancedBody" hidden>
962
+ <div class="s-field">
963
+ <label id="lblSchedTenantId"></label>
964
+ <input type="text" id="cfgTenantId" autocomplete="off">
965
+ <span class="s-hint" id="hintSchedTenantId"></span>
966
+ </div>
967
+ <div class="s-field">
968
+ <label id="lblSchedConnKey"></label>
969
+ <input type="password" id="cfgConnectionKey" autocomplete="new-password">
970
+ <span class="s-hint" id="hintSchedConnKey"></span>
971
+ </div>
972
+ <div class="s-field">
973
+ <label id="lblSchedRegisterAddr"></label>
974
+ <input type="text" id="cfgRegisterAddr" placeholder="192.168.1.25:18081" autocomplete="off">
975
+ <span class="s-hint" id="hintSchedRegisterAddr"></span>
976
+ </div>
977
+ <div class="s-field">
978
+ <label id="lblSchedTunnel"></label>
979
+ <select id="cfgTunnel">
980
+ <option value="auto" id="optTunnelAuto"></option>
981
+ <option value="true" id="optTunnelOn"></option>
982
+ <option value="false" id="optTunnelOff"></option>
983
+ </select>
984
+ <span class="s-hint" id="hintSchedTunnel"></span>
985
+ </div>
986
+ <div class="s-field">
987
+ <div class="s-info-row"><span class="s-info-label" id="lblSchedInstanceId"></span><span class="s-info-value" id="valSchedInstanceId">-</span></div>
988
+ <span class="s-hint" id="hintSchedInstanceId"></span>
989
+ </div>
990
+ </div>
526
991
  </div>
527
992
  <div class="s-msg" id="schedMsg"></div>
528
993
  <div class="s-actions s-actions-row">
@@ -533,10 +998,6 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
533
998
  <div class="s-field">
534
999
  <p class="s-desc" id="descSched"></p>
535
1000
  </div>
536
- <div class="s-actions s-actions-row">
537
- <button class="s-btn s-btn-danger" id="btnSaveRestart" onclick="saveServerConfigAndRestart()"></button>
538
- <button class="s-btn s-btn-secondary" id="btnReRegister" onclick="doReRegister()"></button>
539
- </div>
540
1001
  </div>
541
1002
  <!-- Access control page -->
542
1003
  <div class="settings-page" id="pageAccess">
@@ -569,107 +1030,438 @@ import{execFile as X}from"node:child_process";import v from"node:fs";import k fr
569
1030
  </div>
570
1031
  </div>
571
1032
 
1033
+ <!-- \u4FDD\u5B58\u6210\u529F\u540E\u7684\u540E\u7EED\u64CD\u4F5C\u5F39\u6846\uFF1A\u91CD\u65B0\u6CE8\u518C\uFF08\u63A8\u8350\uFF09/ \u91CD\u542F Bridge / \u6682\u4E0D -->
1034
+ <div class="modal-overlay" id="afterSaveModal" style="display:none" onclick="closeAfterSave(event)">
1035
+ <div class="settings-panel aftersave-panel" onclick="event.stopPropagation()" role="dialog" aria-modal="true" aria-labelledby="aftersaveTitle">
1036
+ <div class="aftersave-icon" aria-hidden="true">
1037
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
1038
+ </div>
1039
+ <div class="aftersave-title" id="aftersaveTitle"></div>
1040
+ <p class="aftersave-desc" id="aftersaveDesc"></p>
1041
+ <div class="aftersave-actions">
1042
+ <button class="s-btn s-btn-primary aftersave-recommended" id="aftersaveBtnRegister" onclick="afterSaveAction('register')"></button>
1043
+ <button class="s-btn s-btn-danger" id="aftersaveBtnRestart" onclick="afterSaveAction('restart')"></button>
1044
+ <button type="button" class="aftersave-skip" id="aftersaveBtnSkip" onclick="afterSaveAction('skip')"></button>
1045
+ </div>
1046
+ </div>
1047
+ </div>
1048
+
572
1049
  <style>
1050
+ /* \u2550\u2550 \u8BBE\u7F6E\u5F39\u7A97\uFF08\u590D\u7528\u4E3B\u6837\u5F0F\u8868\u7684\u4EE4\u724C\uFF09 \u2550\u2550 */
573
1051
  .modal-overlay {
574
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
575
- background: rgba(0,0,0,0.55); z-index: 1000;
1052
+ position: fixed; inset: 0;
1053
+ background: rgba(0, 0, 0, 0.5);
1054
+ backdrop-filter: blur(3px);
1055
+ z-index: 1000;
576
1056
  display: flex; align-items: center; justify-content: center;
577
1057
  padding: 20px;
1058
+ animation: fadeIn .12s ease;
1059
+ }
1060
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
1061
+
1062
+ .settings-panel {
1063
+ background: var(--surface);
1064
+ border: 1px solid var(--border);
1065
+ border-radius: var(--radius-lg);
1066
+ box-shadow: var(--shadow-2);
1067
+ width: 720px; max-width: 100%;
1068
+ /* \u56FA\u5B9A\u9AD8\u5EA6\uFF1A\u5404\u8BBE\u7F6E\u9875\u5185\u5BB9\u591A\u5C11\u4E0D\u4E00\uFF0C\u82E5\u968F\u5185\u5BB9\u6491\u5F00\uFF0C\u5207\u6362\u9875\u7B7E\u65F6\u9762\u677F\u4F1A\u5FFD\u9AD8\u5FFD\u77EE\u3002
1069
+ \u5916\u6846\u6052\u5B9A 88vh\uFF0C\u957F\u5185\u5BB9\uFF08\u65E5\u5FD7\u3001\u8C03\u5EA6\u8868\u5355\uFF09\u7531 .settings-content \u5185\u90E8\u6EDA\u52A8\u6D88\u5316\u3002 */
1070
+ height: min(88vh, 720px);
1071
+ display: flex; flex-direction: column;
1072
+ overflow: hidden;
1073
+ animation: panelIn .16s cubic-bezier(.2, 0, .2, 1);
1074
+ }
1075
+ @keyframes panelIn {
1076
+ from { opacity: 0; transform: translateY(6px) scale(.99); }
1077
+ to { opacity: 1; transform: none; }
1078
+ }
1079
+
1080
+ .settings-head {
1081
+ display: flex; align-items: center; justify-content: space-between;
1082
+ padding: 15px 20px;
1083
+ border-bottom: 1px solid var(--border);
1084
+ flex-shrink: 0;
1085
+ }
1086
+ .settings-head span {
1087
+ font-size: 15px; font-weight: 650; letter-spacing: -0.01em;
1088
+ }
1089
+ .settings-close {
1090
+ background: none; border: none;
1091
+ color: var(--text-3);
1092
+ font-size: 20px; cursor: pointer;
1093
+ width: 28px; height: 28px;
1094
+ border-radius: var(--radius-sm);
1095
+ display: grid; place-items: center;
1096
+ line-height: 1;
1097
+ transition: background .15s, color .15s;
1098
+ }
1099
+ .settings-close:hover { background: var(--surface-2); color: var(--text); }
1100
+
1101
+ .settings-body { display: flex; flex: 1; min-height: 0; }
1102
+
1103
+ .settings-nav {
1104
+ width: 148px; flex-shrink: 0;
1105
+ border-right: 1px solid var(--border);
1106
+ padding: 10px 8px;
1107
+ display: flex; flex-direction: column; gap: 2px;
1108
+ overflow-y: auto;
1109
+ }
1110
+ .settings-nav-item {
1111
+ padding: 8px 12px;
1112
+ font-size: 13px;
1113
+ border-radius: var(--radius-sm);
1114
+ cursor: pointer;
1115
+ color: var(--text-2);
1116
+ transition: background .15s, color .15s;
1117
+ }
1118
+ .settings-nav-item:hover { color: var(--text); background: var(--surface-2); }
1119
+ .settings-nav-item.active {
1120
+ color: var(--accent);
1121
+ background: var(--accent-soft);
1122
+ font-weight: 600;
1123
+ }
1124
+
1125
+ .settings-content {
1126
+ flex: 1; padding: 22px 24px; overflow-y: auto; min-width: 0;
1127
+ }
1128
+ .settings-page { display: none; }
1129
+ .settings-page.active { display: block; }
1130
+
1131
+ /* \u2500\u2500 \u8868\u5355 \u2500\u2500 */
1132
+ .s-field { margin-bottom: 18px; }
1133
+ .s-field label {
1134
+ display: block; font-size: 12.5px; font-weight: 600;
1135
+ margin-bottom: 7px; color: var(--text-2);
1136
+ }
1137
+ .s-field input,
1138
+ .s-field select,
1139
+ .s-field textarea {
1140
+ width: 100%;
1141
+ padding: 9px 12px;
1142
+ background: var(--bg-elevated);
1143
+ border: 1px solid var(--border);
1144
+ border-radius: var(--radius-sm);
1145
+ color: var(--text);
1146
+ font-size: 13.5px;
1147
+ font-family: inherit;
1148
+ outline: none;
1149
+ transition: border-color .15s, box-shadow .15s;
1150
+ }
1151
+ .s-field input:focus,
1152
+ .s-field select:focus,
1153
+ .s-field textarea:focus {
1154
+ border-color: var(--accent);
1155
+ box-shadow: 0 0 0 3px var(--accent-soft);
1156
+ }
1157
+ .s-field input::placeholder { color: var(--text-3); }
1158
+ .s-field select { cursor: pointer; }
1159
+ .s-field select option { background: var(--bg-elevated); color: var(--text); }
1160
+ .s-field textarea {
1161
+ font-family: var(--font-mono);
1162
+ font-size: 12.5px;
1163
+ resize: vertical;
1164
+ }
1165
+
1166
+ .s-hint { display: block; font-size: 11.5px; color: var(--text-3); margin-top: 5px; line-height: 1.5; }
1167
+ .s-desc { font-size: 12.5px; color: var(--text-2); line-height: 1.65; margin: 0; }
1168
+
1169
+ .s-msg {
1170
+ padding: 9px 12px;
1171
+ border-radius: var(--radius-sm);
1172
+ font-size: 12.5px;
1173
+ margin-bottom: 14px;
1174
+ display: none;
1175
+ }
1176
+ .s-msg.error { display: block; background: var(--danger-soft); color: var(--danger); }
1177
+ .s-msg.success { display: block; background: var(--ok-soft); color: var(--ok); }
1178
+ .s-msg.info { display: block; background: var(--info-soft); color: var(--info); }
1179
+
1180
+ .s-actions { margin-top: 14px; }
1181
+ .s-actions-row { display: flex; gap: 8px; flex-wrap: wrap; }
1182
+ .s-actions-col { display: flex; flex-direction: column; gap: 8px; }
1183
+ .s-btn-ghost { background: transparent; border-color: transparent; color: var(--text-2); }
1184
+ .s-btn-ghost:hover { background: var(--surface-2); color: var(--text); border-color: transparent; }
1185
+
1186
+ /* \u2500\u2500 \u4FDD\u5B58\u6210\u529F\u540E\u7684\u540E\u7EED\u64CD\u4F5C\u5F39\u6846 \u2500\u2500 */
1187
+ .settings-panel.aftersave-panel {
1188
+ width: 380px;
1189
+ /* \u8986\u76D6 .settings-panel \u7684\u56FA\u5B9A\u9AD8\u5EA6\uFF1A\u8FD9\u662F\u5185\u5BB9\u5F88\u5C11\u7684\u5C0F\u5F39\u6846\uFF0C
1190
+ \u82E5\u6CBF\u7528 88vh \u4F1A\u88AB\u6491\u51FA\u5927\u7247\u7A7A\u767D\uFF08\u7A84\u5C4F\u5A92\u4F53\u67E5\u8BE2\u540C\u6837\u8986\u76D6\uFF09\u3002 */
1191
+ height: auto;
1192
+ max-height: min(88vh, 720px);
1193
+ padding: 24px 24px 20px;
1194
+ }
1195
+ .aftersave-icon {
1196
+ width: 40px; height: 40px;
1197
+ margin: 0 auto 14px;
1198
+ border-radius: 50%;
1199
+ background: var(--ok-soft);
1200
+ color: var(--ok);
1201
+ display: grid; place-items: center;
1202
+ }
1203
+ .aftersave-icon svg { width: 20px; height: 20px; display: block; }
1204
+ .aftersave-title {
1205
+ font-size: 15px; font-weight: 650; letter-spacing: -0.01em;
1206
+ margin-bottom: 5px;
1207
+ text-align: center;
1208
+ }
1209
+ .aftersave-desc {
1210
+ font-size: 12.5px; color: var(--text-2); line-height: 1.6;
1211
+ margin: 0 0 18px;
1212
+ text-align: center;
1213
+ }
1214
+ .aftersave-actions { display: flex; flex-direction: column; gap: 8px; }
1215
+ .aftersave-skip {
1216
+ margin-top: 2px;
1217
+ background: none;
1218
+ border: none;
1219
+ color: var(--text-3);
1220
+ font-family: inherit;
1221
+ font-size: 12.5px;
1222
+ cursor: pointer;
1223
+ padding: 6px;
1224
+ border-radius: var(--radius-sm);
1225
+ transition: color .15s, background .15s;
1226
+ }
1227
+ .aftersave-skip:hover { color: var(--text); background: var(--surface-2); }
1228
+ .aftersave-recommended {
1229
+ position: relative;
1230
+ }
1231
+ .aftersave-recommended::after {
1232
+ content: attr(data-recommended);
1233
+ position: absolute;
1234
+ top: -8px; right: 10px;
1235
+ padding: 1px 7px;
1236
+ border-radius: 20px;
1237
+ background: var(--accent);
1238
+ color: #fff;
1239
+ font-size: 10px;
1240
+ font-weight: 600;
1241
+ letter-spacing: 0.02em;
1242
+ }
1243
+
1244
+ .s-btn {
1245
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
1246
+ padding: 8px 18px;
1247
+ border-radius: var(--radius-sm);
1248
+ font-size: 13px; font-weight: 600;
1249
+ font-family: inherit;
1250
+ cursor: pointer;
1251
+ transition: background .15s, border-color .15s, opacity .15s;
1252
+ border: 1px solid transparent;
1253
+ white-space: nowrap;
1254
+ }
1255
+ .s-btn-sm { padding: 5px 12px; font-size: 12px; }
1256
+ .s-btn-primary { background: var(--accent); color: #fff; }
1257
+ .s-btn-primary:hover { background: var(--accent-hover); }
1258
+ .s-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
1259
+ .s-btn-secondary {
1260
+ background: var(--surface-2); color: var(--text); border-color: var(--border);
1261
+ }
1262
+ .s-btn-secondary:hover { background: var(--surface-3); border-color: var(--border-strong); }
1263
+ .s-btn-danger {
1264
+ background: transparent; color: var(--danger);
1265
+ border-color: color-mix(in srgb, var(--danger) 45%, transparent);
1266
+ }
1267
+ .s-btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }
1268
+ .s-btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
1269
+
1270
+ /* \u2500\u2500 \u8C03\u5EA6\u4E2D\u5FC3\u76EE\u6807\u5217\u8868 \u2500\u2500 */
1271
+ .s-target-list { display: flex; flex-direction: column; gap: 8px; }
1272
+ .s-target-item {
1273
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
1274
+ padding: 11px 13px;
1275
+ background: var(--bg-elevated);
1276
+ border: 1px solid var(--border);
1277
+ border-radius: var(--radius-sm);
1278
+ cursor: pointer;
1279
+ transition: border-color .15s, background .15s;
1280
+ }
1281
+ .s-target-item:hover { border-color: var(--border-strong); }
1282
+ .s-target-item.active { border-color: var(--accent); background: var(--accent-soft); }
1283
+ .s-target-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
1284
+ .s-target-url {
1285
+ font-size: 13.5px; font-weight: 600; color: var(--text);
1286
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1287
+ }
1288
+ .s-target-meta {
1289
+ font-size: 11.5px; color: var(--text-3);
1290
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1291
+ }
1292
+ .s-target-actions { display: flex; gap: 6px; flex-shrink: 0; }
1293
+ .s-target-form {
1294
+ border: 1px solid var(--border);
1295
+ border-radius: var(--radius);
1296
+ padding: 18px;
1297
+ margin-bottom: 18px;
1298
+ background: var(--bg-elevated);
1299
+ }
1300
+ .s-target-empty {
1301
+ padding: 20px 14px;
1302
+ text-align: center;
1303
+ font-size: 12.5px;
1304
+ color: var(--text-3);
1305
+ border: 1px dashed var(--border-strong);
1306
+ border-radius: var(--radius-sm);
1307
+ }
1308
+
1309
+ /* \u2500\u2500 \u9AD8\u7EA7\u8BBE\u7F6E\u6298\u53E0\u533A \u2500\u2500 */
1310
+ .s-advanced {
1311
+ border: 1px solid var(--border);
1312
+ border-radius: var(--radius-sm);
1313
+ background: var(--surface-2);
1314
+ margin: 4px 0 18px;
1315
+ overflow: hidden;
578
1316
  }
579
- .settings-panel {
580
- background: var(--surface); border: 1px solid var(--border);
581
- border-radius: var(--radius); width: 640px; max-width: 100%;
582
- max-height: 90vh; display: flex; flex-direction: column;
1317
+ .s-advanced-head {
1318
+ display: flex;
1319
+ align-items: center;
1320
+ gap: 7px;
1321
+ width: 100%;
1322
+ padding: 10px 13px;
1323
+ background: transparent;
1324
+ border: none;
1325
+ color: var(--text-2);
1326
+ font-family: inherit;
1327
+ font-size: 12.5px;
1328
+ font-weight: 600;
1329
+ cursor: pointer;
1330
+ text-align: left;
1331
+ transition: color .15s, background .15s;
1332
+ }
1333
+ .s-advanced-head:hover { color: var(--text); background: var(--surface-3); }
1334
+ .s-advanced-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
1335
+ .s-advanced-head .toggle-icon { font-size: 9px; transition: transform .2s; }
1336
+ .s-advanced-head[aria-expanded="true"] .toggle-icon { transform: rotate(90deg); }
1337
+ .s-advanced-count { margin-left: auto; font-weight: 500; color: var(--text-3); font-size: 11.5px; }
1338
+ .s-advanced-body {
1339
+ padding: 14px 13px 4px;
1340
+ background: var(--bg-elevated);
1341
+ border-top: 1px solid var(--border);
1342
+ }
1343
+ .s-advanced-body[hidden] { display: none; }
1344
+
1345
+ /* \u2500\u2500 \u8282\u70B9\u4FE1\u606F \u2500\u2500 */
1346
+ .s-info-grid { display: grid; gap: 0; margin-bottom: 16px; }
1347
+ .s-info-row {
1348
+ display: flex; gap: 16px;
1349
+ padding: 9px 0;
1350
+ border-bottom: 1px solid var(--border);
1351
+ font-size: 13px;
1352
+ min-width: 0;
583
1353
  }
584
- .settings-head {
585
- display: flex; align-items: center; justify-content: space-between;
586
- padding: 16px 20px; border-bottom: 1px solid var(--border);
1354
+ .s-info-row:last-child { border-bottom: none; }
1355
+ .s-info-label { color: var(--text-3); min-width: 118px; flex-shrink: 0; }
1356
+ .s-info-value {
1357
+ color: var(--text); word-break: break-all; min-width: 0;
1358
+ font-family: var(--font-mono); font-size: 12.5px;
1359
+ }
1360
+
1361
+ /* \u2500\u2500 \u65E5\u5FD7 \u2500\u2500 */
1362
+ .s-log-toolbar {
1363
+ display: flex; align-items: center; gap: 10px;
1364
+ margin-bottom: 12px; flex-wrap: wrap;
1365
+ }
1366
+ .s-log-toolbar select {
1367
+ background: var(--bg-elevated);
1368
+ border: 1px solid var(--border);
1369
+ color: var(--text);
1370
+ padding: 5px 9px;
1371
+ border-radius: var(--radius-sm);
1372
+ font-size: 12px; outline: none;
587
1373
  }
588
- .settings-head span { font-size: 16px; font-weight: 600; }
589
- .settings-close {
590
- background: none; border: none; color: var(--text2);
591
- font-size: 22px; cursor: pointer; padding: 0 4px; line-height: 1;
592
- }
593
- .settings-close:hover { color: var(--text); }
594
- .settings-body { display: flex; flex: 1; min-height: 320px; }
595
- .settings-nav { width: 100px; flex-shrink: 0; border-right: 1px solid var(--border); padding: 8px 0; }
596
- .settings-nav-item { padding: 7px 14px; font-size: 13px; cursor: pointer; color: var(--text2); transition: all .15s; }
597
- .settings-nav-item:hover { color: var(--text); background: rgba(255,255,255,0.03); }
598
- .settings-nav-item.active { color: var(--accent); background: rgba(91,141,239,0.1); }
599
- .settings-content { flex: 1; padding: 20px; overflow-y: auto; }
600
- .settings-page { display: none; }
601
- .settings-page.active { display: block; }
602
- .s-field { margin-bottom: 16px; }
603
- .s-field label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--text2); }
604
- .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; }
605
- .s-field input:focus { border-color: var(--accent); }
606
- .s-field select { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; outline: none; cursor: pointer; }
607
- .s-field select:focus { border-color: var(--accent); }
608
- .s-field select option { background: var(--surface2); color: var(--text); }
609
- .s-field textarea { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; font-family: "Cascadia Code", "Fira Code", Consolas, monospace; outline: none; resize: vertical; }
610
- .s-field textarea:focus { border-color: var(--accent); }
611
- .s-actions-row { display: flex; gap: 10px; flex-wrap: wrap; }
612
- .s-target-list { display: flex; flex-direction: column; gap: 8px; }
613
- .s-target-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
614
- .s-target-item.active { border-color: var(--accent); }
615
- .s-target-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
616
- .s-target-url { font-size: 14px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
617
- .s-target-meta { font-size: 12px; opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
618
- .s-target-actions { display: flex; gap: 6px; flex-shrink: 0; }
619
- .s-target-form { border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 18px; background: var(--surface); }
620
- .s-target-empty { padding: 14px; text-align: center; font-size: 13px; opacity: 0.6; border: 1px dashed var(--border); border-radius: 6px; }
621
- .s-hint { display: block; font-size: 11px; color: var(--text2); margin-top: 4px; }
622
- .s-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; display: none; }
623
- .s-msg.error { display: block; background: rgba(240,84,84,0.12); color: var(--red); }
624
- .s-msg.success { display: block; background: rgba(67,198,168,0.12); color: var(--green); }
625
- .s-msg.info { display: block; background: rgba(91,141,239,0.12); color: var(--accent); }
626
- .s-actions { margin-top: 12px; }
627
- .s-btn { padding: 9px 24px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all .15s; border: none; }
628
- .s-btn-sm { padding: 5px 12px; font-size: 13px; }
629
- .s-btn-primary { background: var(--accent); color: #fff; }
630
- .s-btn-primary:hover { background: var(--accent-hover); }
631
- .s-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
632
- .s-btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
633
- .s-btn-secondary:hover { background: var(--border); }
634
- .s-btn-danger { background: var(--red); color: #fff; }
635
- .s-btn-danger:hover { background: #d94a4a; }
636
- .s-btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
637
- .s-desc { font-size: 13px; color: var(--text2); line-height: 1.6; margin: 0; }
638
- .s-restart-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; display: none; }
639
- .s-restart-msg.info { display: block; background: rgba(91,141,239,0.12); color: var(--accent); }
640
- .s-restart-msg.error { display: block; background: rgba(240,84,84,0.12); color: var(--red); }
641
- /* Info grid */
642
- .s-info-grid { display: grid; gap: 2px; margin-bottom: 16px; }
643
- .s-info-row { display: flex; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
644
- .s-info-label { color: var(--text2); min-width: 110px; flex-shrink: 0; }
645
- .s-info-value { color: var(--text); word-break: break-all; }
646
- /* Log viewer */
647
- .s-log-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
648
- .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; }
649
1374
  .s-log-toolbar select:focus { border-color: var(--accent); }
650
- .s-log-toggle { font-size: 12px; color: var(--text2); cursor: pointer; display: flex; align-items: center; gap: 4px; }
1375
+ .s-log-toggle {
1376
+ font-size: 12px; color: var(--text-2);
1377
+ cursor: pointer; display: flex; align-items: center; gap: 5px;
1378
+ }
651
1379
  .s-log-toggle input { accent-color: var(--accent); }
652
- .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; }
1380
+ .s-log-list {
1381
+ max-height: 380px; overflow-y: auto;
1382
+ font-family: var(--font-mono);
1383
+ font-size: 11.5px; line-height: 1.75;
1384
+ background: var(--bg-elevated);
1385
+ border: 1px solid var(--border);
1386
+ border-radius: var(--radius-sm);
1387
+ padding: 10px;
1388
+ }
653
1389
  .s-log-entry { padding: 1px 4px; white-space: pre-wrap; word-break: break-all; }
654
- .s-log-entry .ll { font-weight: 600; }
1390
+ .s-log-entry .ll { font-weight: 700; }
655
1391
  .s-log-entry .ll.INFO { color: var(--accent); }
656
- .s-log-entry .ll.WARN { color: var(--orange); }
657
- .s-log-entry .ll.ERROR { color: var(--red); }
658
- .s-log-entry .ts { color: var(--text2); }
659
- .s-log-empty { color: var(--text2); font-size: 13px; text-align: center; padding: 24px; }
660
- /* Table */
1392
+ .s-log-entry .ll.WARN { color: var(--warn); }
1393
+ .s-log-entry .ll.ERROR { color: var(--danger); }
1394
+ .s-log-entry .ts { color: var(--text-3); }
1395
+ .s-log-empty { color: var(--text-3); font-size: 12.5px; text-align: center; padding: 28px; }
1396
+
1397
+ /* \u2500\u2500 \u8BBE\u7F6E\u5185\u8868\u683C \u2500\u2500 */
661
1398
  .s-table-wrap { max-height: 320px; overflow-y: auto; }
662
1399
  .s-table { width: 100%; border-collapse: collapse; font-size: 12px; }
663
- .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; }
664
- .s-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }
1400
+ .s-table th {
1401
+ text-align: left; padding: 8px 10px;
1402
+ font-size: 11px; font-weight: 620; color: var(--text-3);
1403
+ background: var(--surface-2);
1404
+ border-bottom: 1px solid var(--border);
1405
+ white-space: nowrap;
1406
+ }
1407
+ .s-table td { padding: 7px 10px; border-bottom: 1px solid var(--border); }
665
1408
  .s-table tr:last-child td { border-bottom: none; }
666
- .s-tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; }
667
- .s-tag.green { background: rgba(67,198,168,0.12); color: var(--green); }
668
- .s-tag.red { background: rgba(240,84,84,0.12); color: var(--red); }
669
- .s-tag.orange { background: rgba(240,160,48,0.12); color: var(--orange); }
670
- .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; }
671
- .btn-kill:hover { background: var(--red); color: #fff; }
672
- .btn-kill:disabled { opacity: 0.4; cursor: not-allowed; }
1409
+ .s-tag {
1410
+ display: inline-block; padding: 1px 7px;
1411
+ border-radius: 20px; font-size: 11px; font-weight: 600;
1412
+ }
1413
+ .s-tag.green { background: var(--ok-soft); color: var(--ok); }
1414
+ .s-tag.red { background: var(--danger-soft); color: var(--danger); }
1415
+ .s-tag.orange { background: var(--warn-soft); color: var(--warn); }
1416
+
1417
+ .s-restart-msg {
1418
+ padding: 9px 12px; border-radius: var(--radius-sm);
1419
+ font-size: 12.5px; margin-bottom: 14px; display: none;
1420
+ }
1421
+ .s-restart-msg.info { display: block; background: var(--info-soft); color: var(--info); }
1422
+ .s-restart-msg.error { display: block; background: var(--danger-soft); color: var(--danger); }
1423
+
1424
+ /* \u8BBE\u7F6E\u5F39\u7A97\u5185\u7684\u6EDA\u52A8\u5BB9\u5668\uFF1A\u7EC6\u6EDA\u52A8\u6761 */
1425
+ .settings-content,
1426
+ .settings-nav,
1427
+ .s-log-list,
1428
+ .s-table-wrap {
1429
+ scrollbar-width: thin;
1430
+ scrollbar-color: var(--border-strong) transparent;
1431
+ }
1432
+ .settings-content::-webkit-scrollbar,
1433
+ .settings-nav::-webkit-scrollbar,
1434
+ .s-log-list::-webkit-scrollbar,
1435
+ .s-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
1436
+ .settings-content::-webkit-scrollbar-track,
1437
+ .settings-nav::-webkit-scrollbar-track,
1438
+ .s-log-list::-webkit-scrollbar-track,
1439
+ .s-table-wrap::-webkit-scrollbar-track { background: transparent; }
1440
+ .settings-content::-webkit-scrollbar-thumb,
1441
+ .settings-nav::-webkit-scrollbar-thumb,
1442
+ .s-log-list::-webkit-scrollbar-thumb,
1443
+ .s-table-wrap::-webkit-scrollbar-thumb {
1444
+ background: var(--border-strong); border-radius: 3px;
1445
+ }
1446
+
1447
+ /* \u7A84\u5C4F\uFF1A\u5BFC\u822A\u6539\u4E3A\u9876\u90E8\u6A2A\u5411\u6EDA\u52A8\uFF08\u9762\u677F\u9AD8\u5EA6\u540C\u6837\u56FA\u5B9A\uFF09 */
1448
+ @media (max-width: 620px) {
1449
+ .settings-panel { height: min(92vh, 640px); }
1450
+ .settings-body { flex-direction: column; }
1451
+ .settings-nav {
1452
+ width: auto;
1453
+ flex-direction: row;
1454
+ border-right: none;
1455
+ border-bottom: 1px solid var(--border);
1456
+ padding: 8px;
1457
+ gap: 4px;
1458
+ overflow-x: auto;
1459
+ overflow-y: hidden;
1460
+ }
1461
+ .settings-nav-item { white-space: nowrap; padding: 6px 12px; }
1462
+ .settings-content { padding: 16px; }
1463
+ .s-info-label { min-width: 92px; }
1464
+ }
673
1465
  </style>
674
1466
 
675
1467
  <script>
@@ -686,6 +1478,32 @@ const T = {
686
1478
  managed: "\u7BA1\u7406",
687
1479
  orphan: "\u5B64\u513F",
688
1480
  managedLabel: "\u7BA1\u7406\u4E2D",
1481
+ // \u9875\u9762\u9AA8\u67B6
1482
+ pageTitle: "\u8FD0\u884C\u603B\u89C8",
1483
+ pageSub: "\u672C\u673A Bridge \u72B6\u6001\u4E0E Agent \u4F1A\u8BDD",
1484
+ heroServiceLabel: "Bridge \u670D\u52A1",
1485
+ heroRegLabel: "\u8C03\u5EA6\u4E2D\u5FC3\u6CE8\u518C",
1486
+ heroBindLabel: "\u9762\u677F\u7ED1\u5B9A",
1487
+ heroRunning: "\u8FD0\u884C\u6B63\u5E38",
1488
+ heroRegYes: "\u5DF2\u6CE8\u518C",
1489
+ heroRegNo: "\u672A\u6CE8\u518C",
1490
+ heroBindYes: "\u5DF2\u7ED1\u5B9A",
1491
+ heroBindNo: "\u672A\u7ED1\u5B9A",
1492
+ kpiActiveLabel: "\u6D3B\u8DC3\u4F1A\u8BDD",
1493
+ kpiPersistedLabel: "\u6301\u4E45\u5316\u4F1A\u8BDD",
1494
+ kpiOrphanLabel: "\u5B64\u513F\u8FDB\u7A0B",
1495
+ kpiUptimeLabel: "\u8FD0\u884C\u65F6\u957F",
1496
+ kpiActiveFoot: "\u6B63\u5728\u5DE5\u4F5C\u7684 Agent",
1497
+ kpiPersistedFoot: "\u53EF\u6062\u590D\u7684\u4F1A\u8BDD",
1498
+ kpiOrphanFoot: "\u9700\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B",
1499
+ kpiUptimeFoot: "\u81EA\u4E0A\u6B21\u542F\u52A8",
1500
+ // \u5F85\u5904\u7406\u4E8B\u9879
1501
+ attnNoScheduler: "\u8FD8\u672A\u8FDE\u63A5\u8C03\u5EA6\u4E2D\u5FC3",
1502
+ attnNoSchedulerDetail: "\u914D\u7F6E\u670D\u52A1\u5668\u5730\u5740\u540E\uFF0CAgent \u624D\u80FD\u63A5\u6536\u4EFB\u52A1\u3002",
1503
+ attnOrphans: "\u68C0\u51FA {n} \u4E2A\u5B64\u513F\u8FDB\u7A0B",
1504
+ attnOrphansDetail: "\u8FD9\u4E9B\u8FDB\u7A0B\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u5DF2\u4E0D\u53D7 Bridge \u7BA1\u7406\u3002",
1505
+ attnActionScheduler: "\u53BB\u914D\u7F6E",
1506
+ attnActionOrphans: "\u67E5\u770B",
689
1507
  // \u72B6\u6001\u5361\u7247
690
1508
  cardBridge: "Bridge",
691
1509
  cardRegistration: "\u6CE8\u518C",
@@ -765,6 +1583,7 @@ const T = {
765
1583
  setRestarting: "\u91CD\u542F\u4E2D...",
766
1584
  // \u8282\u70B9\u4FE1\u606F
767
1585
  nodeTitle: "\u8282\u70B9\u4FE1\u606F",
1586
+ nodeRefresh: "\u21BB \u5237\u65B0",
768
1587
  nodeHostname: "\u4E3B\u673A\u540D",
769
1588
  nodePlatform: "\u5E73\u53F0",
770
1589
  nodeUptime: "\u8FD0\u884C\u65F6\u957F",
@@ -794,12 +1613,13 @@ const T = {
794
1613
  schedAddTarget: "\uFF0B \u6DFB\u52A0\u8C03\u5EA6\u4E2D\u5FC3",
795
1614
  schedEditTarget: "\u7F16\u8F91",
796
1615
  schedDeleteTarget: "\u5220\u9664",
1616
+ schedCopyConnKey: "\u590D\u5236\u5BC6\u94A5",
797
1617
  schedConfirmDelete: "\u5220\u9664\u8BE5\u8C03\u5EA6\u4E2D\u5FC3\u914D\u7F6E\uFF1F",
798
1618
  schedFormNew: "\u65B0\u589E\u8C03\u5EA6\u4E2D\u5FC3",
799
1619
  schedUrl: "\u670D\u52A1\u5730\u5740 (serverUrl)",
800
1620
  schedUrlHint: "\u5FC5\u586B\uFF0C\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF1B\u5730\u5740\u53D8\u66F4\u65F6 API Key \u4F1A\u81EA\u52A8\u6CBF\u7528",
801
1621
  schedUserKey: "\u7528\u6237 API Key (userKey)",
802
- schedUserKeyHint: "\u5FC5\u586B\uFF1B\u5DF2\u914D\u7F6E\u65F6\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u4E0D\u53D8",
1622
+ schedUserKeyHint: "\u5FC5\u586B\uFF1B\u5DF2\u914D\u7F6E\u65F6\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u4E0D\u53D8\u3002\u540C\u4E00\u5730\u5740\u53EF\u6DFB\u52A0\u591A\u884C\u2014\u2014\u4E0D\u540C API Key \u4EE3\u8868\u4E0D\u540C\u8D26\u53F7/\u79DF\u6237\uFF0C\u4F1A\u5E76\u884C\u6CE8\u518C",
803
1623
  schedTenantId: "\u79DF\u6237 ID\uFF08\u53EF\u9009\uFF09",
804
1624
  schedTenantIdHint: "\u7559\u7A7A\u7531 server \u6839\u636E userKey \u81EA\u52A8\u63A8\u5BFC",
805
1625
  schedInstanceName: "\u5B9E\u4F8B\u540D\u79F0\uFF08\u53EF\u9009\uFF09",
@@ -815,18 +1635,37 @@ const T = {
815
1635
  schedTunnelOff: "\u7981\u7528",
816
1636
  schedInstanceId: "\u5B9E\u4F8B ID",
817
1637
  schedInstanceIdHint: "\u7F16\u8F91\u6539\u5740\u65F6\u81EA\u52A8\u7EE7\u627F\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF09\uFF0C\u65B0\u589E\u76EE\u6807\u6CE8\u518C\u6210\u529F\u540E\u81EA\u52A8\u56DE\u586B",
1638
+ schedAdvanced: "\u9AD8\u7EA7\u8BBE\u7F6E",
1639
+ schedAdvancedSummary: "\u9AD8\u7EA7\u8BBE\u7F6E \xB7 \u5DF2\u586B {n} \u9879\u53EF\u9009\u9879",
1640
+ schedAdvancedEmpty: "\u9AD8\u7EA7\u8BBE\u7F6E \xB7 \u53EF\u9009\u9879\u5747\u4E3A\u7A7A\uFF0C\u4FDD\u6301\u9ED8\u8BA4\u5373\u53EF",
1641
+ schedAdvancedN: "{n} \u9879\u5DF2\u586B",
818
1642
  schedSave: "\u4FDD\u5B58\u914D\u7F6E",
819
1643
  schedSaving: "\u4FDD\u5B58\u4E2D...",
820
1644
  schedSaved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58",
821
1645
  schedSavedRegistering: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u6B63\u5728\u91CD\u65B0\u6CE8\u518C...",
822
1646
  schedSaveFail: "\u4FDD\u5B58\u5931\u8D25",
1647
+ schedDupNoKey: "\u8BE5\u5730\u5740\u5DF2\u6709\u914D\u7F6E\u3002\u540C\u4E00\u5730\u5740\u8981\u65B0\u589E\u4E00\u6761\uFF0C\u8BF7\u586B\u5165\u53E6\u4E00\u4E2A API Key\uFF08\u4E0D\u540C\u8D26\u53F7/\u79DF\u6237\uFF09\uFF1B\u82E5\u8981\u4FEE\u6539\u5DF2\u6709\u6761\u76EE\uFF0C\u8BF7\u70B9\u51FB\u5217\u8868\u4E2D\u8BE5\u6761\u76EE\u7684\u300C\u7F16\u8F91\u300D\u3002",
1648
+ schedTenantShort: "\u79DF\u6237",
823
1649
  schedSaveAndRestart: "\u4FDD\u5B58\u5E76\u91CD\u542F",
824
1650
  schedSaveRestartConfirm: "\u4FDD\u5B58\u914D\u7F6E\u5E76\u91CD\u542F Bridge\uFF1F\u91CD\u542F\u671F\u95F4\u670D\u52A1\u4F1A\u77ED\u6682\u4E2D\u65AD\uFF0C\u8FD0\u884C\u4E2D\u7684 Agent \u4F1A\u88AB\u4FDD\u7559\u3002",
1651
+ aftersaveTitle: "\u914D\u7F6E\u5DF2\u4FDD\u5B58",
1652
+ aftersaveDesc: "\u65B0\u914D\u7F6E\u5DF2\u5199\u5165\u3002\u8981\u7ACB\u5373\u751F\u6548\u5417\uFF1F",
1653
+ aftersaveRegister: "\u7ACB\u5373\u91CD\u65B0\u6CE8\u518C",
1654
+ aftersaveRegisterRec: "\u63A8\u8350",
1655
+ aftersaveRestart: "\u91CD\u542F Bridge \u751F\u6548",
1656
+ aftersaveRestartHint: "\u96A7\u9053\u6A21\u5F0F\u7B49\u53D8\u66F4\u9700\u8981\u91CD\u542F\uFF1B\u91CD\u542F\u671F\u95F4\u670D\u52A1\u77ED\u6682\u4E2D\u65AD\uFF0C\u8FD0\u884C\u4E2D\u7684 Agent \u4F1A\u88AB\u4FDD\u7559",
1657
+ aftersaveSkip: "\u6682\u4E0D\uFF0C\u7A0D\u540E\u518D\u8BF4",
1658
+ aftersaveRegistering: "\u6B63\u5728\u91CD\u65B0\u6CE8\u518C...",
1659
+ aftersaveRestarting: "\u6B63\u5728\u91CD\u542F...",
1660
+ aftersaveRegisterOk: "\u91CD\u65B0\u6CE8\u518C\u6210\u529F\uFF0C\u65B0\u914D\u7F6E\u5DF2\u751F\u6548",
1661
+ aftersaveRegisterFail: "\u91CD\u65B0\u6CE8\u518C\u5931\u8D25",
1662
+ aftersaveSkipDone: "\u5DF2\u4FDD\u5B58\u3002\u914D\u7F6E\u4F1A\u5728\u4E0B\u6B21 Bridge \u542F\u52A8\u65F6\u751F\u6548\uFF1B\u4E5F\u53EF\u968F\u65F6\u624B\u52A8\u91CD\u542F\u3002",
825
1663
  schedReRegister: "\u91CD\u65B0\u6CE8\u518C",
826
1664
  schedReRegistering: "\u6CE8\u518C\u4E2D...",
827
1665
  schedReRegistered: "\u91CD\u65B0\u6CE8\u518C\u6210\u529F",
828
1666
  schedReRegisterFail: "\u91CD\u65B0\u6CE8\u518C\u5931\u8D25",
829
- schedDesc: "\u4FDD\u5B58\u540E\u53EF\u70B9\u51FB\u201C\u91CD\u65B0\u6CE8\u518C\u201D\u7ACB\u5373\u5E94\u7528\u65B0\u914D\u7F6E\uFF1B\u96A7\u9053\u6A21\u5F0F\u7B49\u90E8\u5206\u53D8\u66F4\u9700\u91CD\u542F Bridge \u751F\u6548\u3002",
1667
+ schedDesc: "\u4FDD\u5B58\u6210\u529F\u540E\u4F1A\u8BE2\u95EE\u5982\u4F55\u5E94\u7528\uFF1A\u63A8\u8350\u7ACB\u5373\u91CD\u65B0\u6CE8\u518C\uFF1B\u96A7\u9053\u6A21\u5F0F\u7B49\u90E8\u5206\u53D8\u66F4\u9700\u91CD\u542F Bridge \u751F\u6548\u3002",
1668
+ schedPrefillNotice: "\u5DF2\u4ECE\u9762\u677F\u5E26\u5165\u670D\u52A1\u5668\u5730\u5740 {0}\u3002\u8BF7\u5728\u4E0B\u65B9\u7C98\u8D34\u4F60\u7684 userKey \u540E\u4FDD\u5B58\uFF08\u5BC6\u94A5\u4E0D\u4F1A\u901A\u8FC7\u94FE\u63A5\u4F20\u5165\uFF09\u3002",
830
1669
  // \u8BBF\u95EE\u63A7\u5236
831
1670
  accTitle: "\u8BBF\u95EE\u63A7\u5236",
832
1671
  accAllowIps: "\u5141\u8BB8\u8BBF\u95EE\u7684 IP / \u7F51\u6BB5",
@@ -859,6 +1698,32 @@ const T = {
859
1698
  managed: "managed",
860
1699
  orphan: "orphan",
861
1700
  managedLabel: "managed",
1701
+ // \u9875\u9762\u9AA8\u67B6
1702
+ pageTitle: "Overview",
1703
+ pageSub: "Local bridge status and agent sessions",
1704
+ heroServiceLabel: "Bridge service",
1705
+ heroRegLabel: "Scheduler registration",
1706
+ heroBindLabel: "Panel binding",
1707
+ heroRunning: "Running",
1708
+ heroRegYes: "Registered",
1709
+ heroRegNo: "Not registered",
1710
+ heroBindYes: "Paired",
1711
+ heroBindNo: "Unpaired",
1712
+ kpiActiveLabel: "Active sessions",
1713
+ kpiPersistedLabel: "Persisted sessions",
1714
+ kpiOrphanLabel: "Orphan processes",
1715
+ kpiUptimeLabel: "Uptime",
1716
+ kpiActiveFoot: "Agents currently working",
1717
+ kpiPersistedFoot: "Sessions that can be resumed",
1718
+ kpiOrphanFoot: "Leftover processes to clean up",
1719
+ kpiUptimeFoot: "Since last start",
1720
+ // \u5F85\u5904\u7406\u4E8B\u9879
1721
+ attnNoScheduler: "Not connected to a scheduler",
1722
+ attnNoSchedulerDetail: "Configure a server address so agents can receive tasks.",
1723
+ attnOrphans: "{n} orphan process(es) detected",
1724
+ attnOrphansDetail: "They are still running but no longer managed by Bridge.",
1725
+ attnActionScheduler: "Configure",
1726
+ attnActionOrphans: "Review",
862
1727
  cardBridge: "Bridge",
863
1728
  cardRegistration: "Registration",
864
1729
  cardBinding: "Binding",
@@ -935,6 +1800,7 @@ const T = {
935
1800
  setRestarting: "Restarting...",
936
1801
  // Node Info
937
1802
  nodeTitle: "Node Info",
1803
+ nodeRefresh: "\u21BB Refresh",
938
1804
  nodeHostname: "Hostname",
939
1805
  nodePlatform: "Platform",
940
1806
  nodeUptime: "Uptime",
@@ -964,12 +1830,13 @@ const T = {
964
1830
  schedAddTarget: "+ Add scheduler",
965
1831
  schedEditTarget: "Edit",
966
1832
  schedDeleteTarget: "Delete",
1833
+ schedCopyConnKey: "Copy key",
967
1834
  schedConfirmDelete: "Delete this scheduler configuration?",
968
1835
  schedFormNew: "New scheduler",
969
1836
  schedUrl: "Server URL (serverUrl)",
970
1837
  schedUrlHint: "Required, must start with http:// or https://; the API key is kept when the URL changes",
971
1838
  schedUserKey: "User API Key (userKey)",
972
- schedUserKeyHint: "Required; leave blank to keep the configured value",
1839
+ schedUserKeyHint: "Required; leave blank to keep the configured value. The same address can be added multiple times \u2014 a different API key means a different account/tenant, registered in parallel",
973
1840
  schedTenantId: "Tenant ID (optional)",
974
1841
  schedTenantIdHint: "Leave blank to auto-derive from userKey on the server",
975
1842
  schedInstanceName: "Instance name (optional)",
@@ -985,18 +1852,37 @@ const T = {
985
1852
  schedTunnelOff: "Off",
986
1853
  schedInstanceId: "Instance ID",
987
1854
  schedInstanceIdHint: "Inherited automatically when editing the URL (same instance, new address); backfilled after a new target registers successfully",
1855
+ schedAdvanced: "Advanced",
1856
+ schedAdvancedSummary: "Advanced \xB7 {n} optional field(s) filled",
1857
+ schedAdvancedEmpty: "Advanced \xB7 all optional, defaults are fine",
1858
+ schedAdvancedN: "{n} filled",
988
1859
  schedSave: "Save config",
989
1860
  schedSaving: "Saving...",
990
1861
  schedSaved: "Configuration saved",
991
1862
  schedSavedRegistering: "Configuration saved, re-registering...",
992
1863
  schedSaveFail: "Failed to save",
1864
+ schedDupNoKey: "This address already has an entry. To add another one for the same address, enter a different API key (a different account/tenant); to modify the existing entry, click Edit on it in the list.",
1865
+ schedTenantShort: "tenant",
993
1866
  schedSaveAndRestart: "Save & restart",
994
1867
  schedSaveRestartConfirm: "Save the configuration and restart the bridge? The service will be briefly interrupted; running agents are preserved.",
1868
+ aftersaveTitle: "Configuration saved",
1869
+ aftersaveDesc: "The new configuration has been written. Apply it now?",
1870
+ aftersaveRegister: "Re-register now",
1871
+ aftersaveRegisterRec: "Recommended",
1872
+ aftersaveRestart: "Restart Bridge to apply",
1873
+ aftersaveRestartHint: "Needed for tunnel-mode changes; restart briefly interrupts the service, running agents are preserved",
1874
+ aftersaveSkip: "Not now",
1875
+ aftersaveRegistering: "Re-registering...",
1876
+ aftersaveRestarting: "Restarting...",
1877
+ aftersaveRegisterOk: "Re-registered successfully, new configuration is live",
1878
+ aftersaveRegisterFail: "Re-register failed",
1879
+ aftersaveSkipDone: "Saved. The configuration applies on next Bridge start; you can also restart manually anytime.",
995
1880
  schedReRegister: "Re-register",
996
1881
  schedReRegistering: "Registering...",
997
1882
  schedReRegistered: "Re-registered successfully",
998
1883
  schedReRegisterFail: "Re-registration failed",
999
- schedDesc: "After saving, click 'Re-register' to apply the new config immediately; some changes such as tunnel mode require a bridge restart.",
1884
+ schedDesc: "After saving you'll be asked how to apply it: re-registering now is recommended; some changes such as tunnel mode require a bridge restart.",
1885
+ schedPrefillNotice: "The server address {0} was pre-filled from the dashboard. Paste your userKey below and save (secrets are never passed via the link).",
1000
1886
  // Access control
1001
1887
  accTitle: "Access Control",
1002
1888
  accAllowIps: "Allowed IPs / networks",
@@ -1050,6 +1936,7 @@ function toggleLang() {
1050
1936
  localStorage.setItem("dashboard-lang", lang);
1051
1937
  document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
1052
1938
  document.getElementById("langBtn").textContent = t("langName");
1939
+ updateThemeBtn();
1053
1940
  if (document.getElementById("settingsModal").style.display === "flex") {
1054
1941
  renderSettingsUI();
1055
1942
  settingsLang = lang;
@@ -1057,6 +1944,30 @@ function toggleLang() {
1057
1944
  if (lastData) render(lastData);
1058
1945
  }
1059
1946
 
1947
+ // \u2500\u2500 \u4E3B\u9898\u504F\u597D\uFF08\u9ED8\u8BA4\u6DF1\u8272\uFF1B\u4E0E\u767B\u5F55/\u8BBE\u7F6E\u9875\u5171\u7528 dashboard-theme\uFF09\u2500\u2500
1948
+ function isLightTheme() {
1949
+ return document.documentElement.classList.contains("light-theme");
1950
+ }
1951
+
1952
+ // \u6309\u94AE\u5448\u73B0"\u5373\u5C06\u5207\u6362\u5230\u7684\u76EE\u6807"\uFF1A\u6DF1\u8272\u65F6\u663E\u592A\u9633\uFF08\u70B9\u2192\u6D45\u8272\uFF09\uFF0C\u6D45\u8272\u65F6\u663E\u6708\u4EAE\uFF08\u70B9\u2192\u6DF1\u8272\uFF09
1953
+ function updateThemeBtn() {
1954
+ var btn = document.getElementById("themeBtn");
1955
+ if (!btn) return;
1956
+ var light = isLightTheme();
1957
+ btn.textContent = light ? "\\u263D" : "\\u2600";
1958
+ btn.title = light
1959
+ ? (lang === "en" ? "Switch to dark" : "\u5207\u6362\u6DF1\u8272")
1960
+ : (lang === "en" ? "Switch to light" : "\u5207\u6362\u6D45\u8272");
1961
+ }
1962
+
1963
+ function toggleTheme() {
1964
+ var light = document.documentElement.classList.toggle("light-theme");
1965
+ try { localStorage.setItem("dashboard-theme", light ? "light" : "dark"); } catch (e) {}
1966
+ updateThemeBtn();
1967
+ }
1968
+
1969
+ updateThemeBtn();
1970
+
1060
1971
  // \u2500\u2500 \u5DE5\u5177\u51FD\u6570 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1061
1972
  let lastData = null;
1062
1973
 
@@ -1141,105 +2052,237 @@ function barColor(pct) {
1141
2052
  }
1142
2053
 
1143
2054
  // \u2500\u2500 \u6E32\u67D3 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2055
+
2056
+ /** \u8BBE\u7F6E\u4E00\u4E2A\u72B6\u6001\u70B9\uFF08hero \u7528\uFF09\uFF1Aok / warn / danger / \u7A7A\u3002 */
2057
+ function setDot(id, tone, pulse) {
2058
+ const el = document.getElementById(id);
2059
+ if (!el) return;
2060
+ el.className = "dot" + (tone ? " " + tone : "") + (pulse ? " pulse" : "");
2061
+ }
2062
+
2063
+ /** \u5F85\u5904\u7406\u4E8B\u9879\u7684\u52A8\u4F5C\u76EE\u6807\uFF1A\u70B9\u51FB\u540E\u8DF3\u8F6C\u5230\u5BF9\u5E94\u8BBE\u7F6E\u9875\u3002 */
2064
+ let attentionActionKind = null;
2065
+
2066
+ function runAttentionAction() {
2067
+ if (attentionActionKind === "scheduler") {
2068
+ openSettings();
2069
+ switchSettingsPage("scheduler");
2070
+ } else if (attentionActionKind === "orphans") {
2071
+ const table = document.getElementById("unknownTable");
2072
+ if (table && table.style.display === "none") toggleUnknown();
2073
+ const el = document.getElementById("orphanContainer");
2074
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
2075
+ }
2076
+ }
2077
+
2078
+ /**
2079
+ * \u5F85\u5904\u7406\u4E8B\u9879\u6761\uFF1A\u53EA\u5728\u771F\u7684\u9700\u8981\u7528\u6237\u52A8\u624B\u65F6\u51FA\u73B0\u3002
2080
+ * \u4F18\u5148\u7EA7\uFF1A\u672A\u6CE8\u518C\u8C03\u5EA6\u4E2D\u5FC3 > \u5B64\u513F\u8FDB\u7A0B\u3002\u4E24\u8005\u4E92\u4E0D\u8986\u76D6\uFF0C\u4F46\u4E00\u6B21\u53EA\u63D0\u793A\u6700\u7D27\u8981\u7684\u4E00\u6761\uFF0C
2081
+ * \u907F\u514D\u9762\u677F\u53D8\u6210\u4E00\u5806\u9EC4\u6761\u3002
2082
+ */
2083
+ function renderAttention(data) {
2084
+ const bar = document.getElementById("attentionBar");
2085
+ const textEl = document.getElementById("attentionText");
2086
+ const actionEl = document.getElementById("attentionAction");
2087
+ if (!bar || !textEl || !actionEl) return;
2088
+
2089
+ const registered = Boolean(data.registration?.registered);
2090
+ const orphanCount = data.processes?.orphan ?? 0;
2091
+
2092
+ if (!registered) {
2093
+ textEl.innerHTML =
2094
+ "<b>" + escHtml(t("attnNoScheduler")) + "</b> " +
2095
+ escHtml(t("attnNoSchedulerDetail"));
2096
+ actionEl.textContent = t("attnActionScheduler");
2097
+ actionEl.style.display = "";
2098
+ attentionActionKind = "scheduler";
2099
+ bar.hidden = false;
2100
+ return;
2101
+ }
2102
+
2103
+ if (orphanCount > 0) {
2104
+ textEl.innerHTML =
2105
+ "<b>" + escHtml(t("attnOrphans").replace("{n}", String(orphanCount))) + "</b> " +
2106
+ escHtml(t("attnOrphansDetail"));
2107
+ actionEl.textContent = t("attnActionOrphans");
2108
+ actionEl.style.display = "";
2109
+ attentionActionKind = "orphans";
2110
+ bar.hidden = false;
2111
+ return;
2112
+ }
2113
+
2114
+ bar.hidden = true;
2115
+ attentionActionKind = null;
2116
+ }
2117
+
2118
+ /** \u5065\u5EB7\u603B\u89C8\uFF1A\u4E09\u4E2A\u300C\u662F\u5426\u53EF\u7528\u300D\u7684\u5224\u65AD\uFF0C\u53D6\u4EE3\u539F\u5148 4 \u5F20\u5E73\u94FA\u72B6\u6001\u5361\u3002 */
2119
+ function renderHero(data) {
2120
+ const b = data.binding || {};
2121
+ const r = data.registration || {};
2122
+
2123
+ // Bridge \u670D\u52A1\uFF08\u9875\u9762\u80FD\u6253\u5F00\u5C31\u8BF4\u660E\u8FDB\u7A0B\u6D3B\u7740\uFF0C\u72B6\u6001\u70B9\u6052\u4E3A ok\uFF09
2124
+ document.getElementById("heroServiceLabel").textContent = t("heroServiceLabel");
2125
+ document.getElementById("heroServiceValue").textContent = t("heroRunning");
2126
+ setDot("heroServiceDot", "ok", true);
2127
+ const platform = [data.bridge?.platform, data.bridge?.arch].filter(Boolean).join(" ");
2128
+ document.getElementById("heroServiceDetail").textContent =
2129
+ [platform, data.bridge?.nodeVersion, fmtDuration(data.bridge?.uptime)]
2130
+ .filter(Boolean)
2131
+ .join(" \xB7 ");
2132
+
2133
+ // \u8C03\u5EA6\u4E2D\u5FC3\u6CE8\u518C
2134
+ document.getElementById("heroRegLabel").textContent = t("heroRegLabel");
2135
+ document.getElementById("heroRegValue").textContent =
2136
+ r.registered ? t("heroRegYes") : t("heroRegNo");
2137
+ setDot("heroRegDot", r.registered ? "ok" : "warn", false);
2138
+ document.getElementById("heroRegDetail").textContent =
2139
+ r.error || r.instanceId || r.lastAttempt
2140
+ ? r.error || r.instanceId || fmtTime(r.lastAttempt)
2141
+ : "-";
2142
+
2143
+ // \u9762\u677F\u7ED1\u5B9A
2144
+ document.getElementById("heroBindLabel").textContent = t("heroBindLabel");
2145
+ document.getElementById("heroBindValue").textContent =
2146
+ b.paired ? t("heroBindYes") : t("heroBindNo");
2147
+ setDot("heroBindDot", b.paired ? "ok" : "warn", false);
2148
+ document.getElementById("heroBindDetail").textContent =
2149
+ b.schedulerBaseUrl || b.instanceId || "-";
2150
+ }
2151
+
2152
+ /** KPI \u78C1\u8D34\uFF1A\u56DB\u4E2A\u6700\u5173\u952E\u7684\u6570\u5B57\u3002 */
2153
+ function renderKpis(data) {
2154
+ const activeCount = data.sessions?.activeCount ?? 0;
2155
+ const persistedCount = data.sessions?.persistedCount ?? 0;
2156
+ const orphanCount = data.processes?.orphan ?? 0;
2157
+ const managedCount = data.processes?.managed ?? 0;
2158
+
2159
+ document.getElementById("kpiActiveLabel").textContent = t("kpiActiveLabel");
2160
+ document.getElementById("kpiPersistedLabel").textContent = t("kpiPersistedLabel");
2161
+ document.getElementById("kpiOrphanLabel").textContent = t("kpiOrphanLabel");
2162
+ document.getElementById("kpiUptimeLabel").textContent = t("kpiUptimeLabel");
2163
+
2164
+ const activeEl = document.getElementById("kpiActive");
2165
+ activeEl.textContent = String(activeCount);
2166
+ activeEl.className = "kpi-value " + (activeCount > 0 ? "ok" : "muted");
2167
+ document.getElementById("kpiActiveFoot").textContent = t("kpiActiveFoot");
2168
+
2169
+ document.getElementById("kpiPersisted").textContent = String(persistedCount);
2170
+ document.getElementById("kpiPersistedFoot").textContent =
2171
+ t("kpiPersistedFoot") + (managedCount > 0 ? " \xB7 " + managedCount + " " + t("managedLabel") : "");
2172
+
2173
+ const orphanEl = document.getElementById("kpiOrphan");
2174
+ orphanEl.textContent = String(orphanCount);
2175
+ orphanEl.className = "kpi-value " + (orphanCount > 0 ? "warn" : "muted");
2176
+ document.getElementById("kpiOrphanFoot").textContent = t("kpiOrphanFoot");
2177
+
2178
+ document.getElementById("kpiUptime").textContent = fmtDuration(data.bridge?.uptime);
2179
+ document.getElementById("kpiUptimeFoot").textContent = t("kpiUptimeFoot");
2180
+ }
2181
+
1144
2182
  function render(data) {
1145
2183
  lastData = data;
1146
2184
  document.getElementById("refreshTime").textContent = fmtTime(data.collectedAt);
1147
2185
  document.getElementById("bridgeHostname").textContent = data.bridge?.hostname || "-";
1148
2186
  document.getElementById("bridgeVersion").textContent = "v" + (data.bridge?.version || "-");
1149
2187
  document.getElementById("langBtn").textContent = t("langName");
1150
- document.getElementById("refreshBtn").textContent = t("refreshBtn");
2188
+ document.getElementById("pageTitle").textContent = t("pageTitle");
2189
+ document.getElementById("pageSub").textContent = t("pageSub");
1151
2190
 
1152
2191
  // Section titles
1153
- document.getElementById("sectionSysRes").firstChild.textContent = t("secSysRes");
1154
- document.getElementById("sectionSessions").firstChild.textContent = t("secSessions");
2192
+ document.getElementById("sectionSysRes").textContent = t("secSysRes");
2193
+ document.getElementById("sectionSessions").textContent = t("secSessions");
1155
2194
  document.getElementById("unknownLabel").textContent = " " + t("secUnknown");
1156
- document.getElementById("sectionOrphans").firstChild.textContent = t("secOrphans");
1157
- document.getElementById("sectionPersisted").firstChild.textContent = t("secPersisted");
2195
+ document.getElementById("sectionOrphans").textContent = t("secOrphans");
2196
+ document.getElementById("sectionPersisted").textContent = t("secPersisted");
1158
2197
  document.getElementById("orphanBannerText").textContent = t("orphanBanner");
1159
2198
  if (document.getElementById("unknownTable").style.display !== "none") {
1160
2199
  document.getElementById("unknownIcon").classList.add("open");
1161
2200
  }
1162
2201
 
1163
- // Status cards
1164
- const b = data.binding || {};
1165
- const r = data.registration || {};
1166
- const cards = [
1167
- { title: t("cardBridge"), items: [
1168
- [t("fldVersion"), data.bridge?.version || "-"],
1169
- [t("fldHost"), data.bridge?.hostname || "-"],
1170
- [t("fldPlatform"), (data.bridge?.platform || "") + " " + (data.bridge?.arch || "")],
1171
- [t("fldUptime"), fmtDuration(data.bridge?.uptime)],
1172
- [t("fldNode"), data.bridge?.nodeVersion || "-"],
1173
- ]},
1174
- { title: t("cardRegistration"), items: [
1175
- [t("fldStatus"), '<span class="' + statusClass(r.registered) + '">' + (r.registered ? t("stRegistered") : t("stNotRegistered")) + "</span>"],
1176
- [t("fldInstanceId"), r.instanceId || "-"],
1177
- [t("fldLastAttempt"), fmtTime(r.lastAttempt)],
1178
- [t("fldError"), r.error ? '<span class="status-red">' + r.error + "</span>" : t("stNone")],
1179
- ]},
1180
- { title: t("cardBinding"), items: [
1181
- [t("fldStatus"), '<span class="' + statusClass(b.paired) + '">' + (b.paired ? t("stPaired") : t("stUnpaired")) + "</span>"],
1182
- [t("fldInstanceId"), b.instanceId || "-"],
1183
- [t("fldScheduler"), b.schedulerBaseUrl || "-"],
1184
- [t("fldPairingCode"), b.pairingCode || "-"],
1185
- ]},
1186
- { title: t("cardSessions"), items: [
1187
- [t("fldActiveSessions"), '<span class="' + ((data.sessions?.activeCount ?? 0) > 0 ? "status-green" : "status-muted") + '">' + (data.sessions?.activeCount ?? 0) + "</span>"],
1188
- [t("fldPersistedSessions"), (data.sessions?.persistedCount ?? 0)],
1189
- [t("fldManagedProcesses"), data.processes?.managed ?? 0],
1190
- [t("fldOrphanProcesses"), '<span class="' + ((data.processes?.orphan ?? 0) > 0 ? "status-orange" : "status-muted") + '">' + (data.processes?.orphan ?? 0) + "</span>"],
1191
- [t("fldRegistryEntries"), data.processes?.registryCount ?? 0],
1192
- ]},
1193
- ];
1194
-
1195
- document.getElementById("statusCards").innerHTML = cards.map(c => {
1196
- const rows = c.items.map(([k, v]) =>
1197
- '<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
1198
- ).join("");
1199
- return '<div class="card"><div class="card-title">' + c.title + '</div>' + rows + '</div>';
1200
- }).join("");
1201
-
1202
- // Metrics
2202
+ renderHero(data);
2203
+ renderKpis(data);
2204
+ renderAttention(data);
2205
+
2206
+ // Metrics \u2014 \u56DB\u4E2A\u4EEA\u8868\u6A2A\u5411\u5BF9\u9F50\uFF0C\u8BFB\u6570\u7528\u5927\u5B57\uFF0C\u660E\u7EC6\u7528\u5C0F\u5B57
1203
2207
  const m = data.metrics;
1204
2208
  const metricsGrid = document.getElementById("metricsGrid");
2209
+ const metricsCard = document.getElementById("metricsCard");
1205
2210
  if (m && m.ok) {
1206
2211
  const cpuPct = m.cpu?.usagePercent ?? 0;
1207
2212
  const memPct = m.memory?.usagePercent ?? 0;
1208
2213
  const storagePct = m.storage?.usagePercent ?? 0;
2214
+ // \u5B58\u50A8\u8DEF\u5F84\u53EA\u53D6\u6700\u540E\u4E00\u6BB5\uFF08\u5982\u6839\u76D8\u53EA\u663E\u793A\u76D8\u7B26\uFF09\uFF0C\u5B8C\u6574\u8DEF\u5F84\u653E title
2215
+ const storageLabel = m.storage?.path ? fmtShortPath(m.storage.path).split("/").pop() : "";
2216
+ metricsCard.classList.remove("metrics-error");
1209
2217
  metricsGrid.innerHTML = [
1210
- { title: "CPU", usage: cpuPct, lines: [
1211
- [t("fldUsage"), fmtPercent(cpuPct)],
1212
- [t("fldCores"), m.cpu?.coreCount ?? "-"],
1213
- [t("fldModel"), (m.cpu?.model || "-").substring(0, 40)],
1214
- ]},
1215
- { title: "Memory", usage: memPct, lines: [
1216
- [t("fldUsage"), fmtPercent(memPct)],
1217
- [t("fldUsed"), fmtBytes(m.memory?.usedBytes)],
1218
- [t("fldFree"), fmtBytes(m.memory?.freeBytes)],
1219
- [t("fldTotal"), fmtBytes(m.memory?.totalBytes)],
1220
- ]},
1221
- { title: "Storage (" + (m.storage?.path || "") + ")", usage: storagePct, lines: [
1222
- [t("fldUsage"), fmtPercent(storagePct)],
1223
- [t("fldFree"), m.storage ? m.storage.freeGiB + " GiB" : "-"],
1224
- [t("fldTotal"), m.storage ? m.storage.totalGiB + " GiB" : "-"],
1225
- ]},
1226
- { title: "Network", usage: null, lines: [
1227
- [t("fldDownload"), fmtBytes(m.network?.downloadBytesPerSecond) + "/s"],
1228
- [t("fldUpload"), fmtBytes(m.network?.uploadBytesPerSecond) + "/s"],
1229
- [t("fldTotalDown"), fmtBytes(m.network?.downloadTotalBytes)],
1230
- [t("fldTotalUp"), fmtBytes(m.network?.uploadTotalBytes)],
1231
- ]},
2218
+ {
2219
+ name: "CPU",
2220
+ reading: fmtPercent(cpuPct),
2221
+ usage: cpuPct,
2222
+ lines: [
2223
+ [t("fldCores"), m.cpu?.coreCount ?? "-"],
2224
+ [t("fldModel"), (m.cpu?.model || "-").substring(0, 36)],
2225
+ ],
2226
+ },
2227
+ {
2228
+ name: "Memory",
2229
+ reading: fmtPercent(memPct),
2230
+ usage: memPct,
2231
+ lines: [
2232
+ [t("fldUsed"), fmtBytes(m.memory?.usedBytes)],
2233
+ [t("fldTotal"), fmtBytes(m.memory?.totalBytes)],
2234
+ ],
2235
+ },
2236
+ {
2237
+ name: "Storage",
2238
+ reading: fmtPercent(storagePct),
2239
+ usage: storagePct,
2240
+ lines: [
2241
+ [t("fldFree"), m.storage ? m.storage.freeGiB + " GiB" : "-"],
2242
+ [t("fldTotal"), m.storage ? m.storage.totalGiB + " GiB" : "-"],
2243
+ ],
2244
+ // \u53EA\u663E\u793A\u672B\u4E24\u6BB5\u8DEF\u5F84\uFF0C\u5B8C\u6574\u8DEF\u5F84\u653E title \u5C5E\u6027\uFF08\u65E7\u7248\u628A\u6574\u6761\u7EDD\u5BF9\u8DEF\u5F84\u585E\u8FDB\u6807\u9898\u5BFC\u81F4\u6362\u884C\uFF09
2245
+ foot: storageLabel,
2246
+ footFull: m.storage?.path || "",
2247
+ },
2248
+ {
2249
+ name: "Network",
2250
+ // \u4E0A\u4E0B\u884C\u901F\u7387\u662F\u77AC\u65F6\u503C\uFF0C0 \u4E0E"\u65E0\u6570\u636E"\u8BED\u4E49\u4E0D\u540C\uFF1A0 \u662F\u771F\u5B9E\u8BFB\u6570\uFF0C\u4E0D\u5E94\u663E\u793A\u4E3A -
2251
+ reading: fmtBytes(m.network?.downloadBytesPerSecond ?? 0) + "/s",
2252
+ muted: true,
2253
+ usage: null,
2254
+ lines: [
2255
+ [t("fldUpload"), fmtBytes(m.network?.uploadBytesPerSecond ?? 0) + "/s"],
2256
+ [t("fldTotalDown"), fmtBytes(m.network?.downloadTotalBytes)],
2257
+ ],
2258
+ },
1232
2259
  ].map(c => {
2260
+ // \u65E0\u767E\u5206\u6BD4\u8BFB\u6570\u7684\u4EEA\u8868\uFF08Network\uFF09\u4E5F\u5360\u540C\u6837\u9AD8\u5EA6\u7684\u8FDB\u5EA6\u6761\u4F4D\uFF0C
2261
+ // \u5426\u5219\u56DB\u5217\u9AD8\u5EA6\u4E0D\u9F50\uFF0C\u5E95\u90E8\u5BF9\u9F50\u4F1A\u88AB\u7834\u574F
1233
2262
  const barHtml = c.usage != null
1234
- ? '<div class="bar-wrap"><div class="bar"><div class="bar-fill ' + barColor(c.usage) + '" style="width:' + Math.min(c.usage, 100) + '%"></div></div><span class="bar-label">' + fmtPercent(c.usage) + '</span></div>'
1235
- : "";
1236
- const rows = c.lines.map(([k, v]) =>
1237
- '<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
2263
+ ? '<div class="bar"><div class="bar-fill ' + barColor(c.usage) + '" style="width:' + Math.min(c.usage, 100) + '%"></div></div>'
2264
+ : '<div class="bar bar-placeholder"></div>';
2265
+ const lines = c.lines.map(([k, v]) =>
2266
+ '<div class="gauge-line"><dt>' + escHtml(k) + '</dt><dd>' + escHtml(v) + '</dd></div>'
1238
2267
  ).join("");
1239
- return '<div class="card"><div class="card-title">' + c.title + '</div>' + barHtml + rows + '</div>';
2268
+ return '<div class="gauge">' +
2269
+ '<div class="gauge-head">' +
2270
+ '<span class="gauge-name">' + escHtml(c.name) + '</span>' +
2271
+ '<span class="gauge-reading' + (c.muted ? " muted" : "") + '">' + escHtml(c.reading) + '</span>' +
2272
+ '</div>' +
2273
+ barHtml +
2274
+ '<dl class="gauge-lines">' + lines + '</dl>' +
2275
+ (c.foot
2276
+ ? '<div class="gauge-line" style="margin-top:6px"><dt style="font-size:11px" title="' + escHtml(c.footFull || c.foot) + '">' + escHtml(c.foot) + '</dt></div>'
2277
+ : '') +
2278
+ '</div>';
1240
2279
  }).join("");
1241
2280
  } else {
1242
- metricsGrid.innerHTML = '<div class="card"><div class="empty">' + t("metricsUnavailable") + '</div></div>';
2281
+ metricsGrid.innerHTML =
2282
+ '<div class="empty" style="grid-column:1/-1">' +
2283
+ '<span class="empty-icon">&#x2013;</span>' +
2284
+ escHtml(t("metricsUnavailable")) +
2285
+ '</div>';
1243
2286
  }
1244
2287
 
1245
2288
  // Build process lookup maps from process list
@@ -1262,20 +2305,24 @@ function render(data) {
1262
2305
 
1263
2306
  // Active sessions (primary table, with PID from process cross-reference)
1264
2307
  const sessions = data.sessions?.active || [];
1265
- document.getElementById("sessionCount").textContent = "(" + sessions.length + ")";
2308
+ document.getElementById("sessionCount").textContent = sessions.length ? "(" + sessions.length + ")" : "";
1266
2309
  if (sessions.length === 0) {
1267
- document.getElementById("sessionTable").innerHTML = '<div class="empty">' + t("noSessions") + '</div>';
2310
+ document.getElementById("sessionTable").innerHTML =
2311
+ '<div class="empty"><span class="empty-icon">&#x25CB;</span>' + escHtml(t("noSessions")) +
2312
+ '<span class="empty-hint">' + escHtml(t("kpiActiveFoot")) + '</span></div>';
1268
2313
  } else {
1269
2314
  document.getElementById("sessionTable").innerHTML =
1270
2315
  '<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thPid") + '</th><th>' + t("thStatus") + '</th><th>' + t("thAction") + '</th><th>' + t("thWorkspace") + '</th><th>' + t("thProvider") + '</th></tr></thead><tbody>' +
1271
2316
  sessions.map(s => {
1272
2317
  const pid = pidByAgentId[s.agentId];
1273
2318
  const st = s.runtimeStatus || "running";
1274
- return '<tr><td>' + s.agentId + '</td><td>' + (pid ? '<code>' + pid + '</code>' : '<span class="status-muted">-</span>') + '</td>'
1275
- + '<td><span class="' + statusClass(st) + '">' + (t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
1276
- + '<td>' + (s.runtimeActionLabel || '-') + '</td>'
1277
- + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-") + '</td>'
1278
- + '<td>' + (s.providerId || "-") + '</td></tr>';
2319
+ const ws = s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-";
2320
+ return '<tr><td class="strong mono">' + escHtml(s.agentId || "-") + '</td>'
2321
+ + '<td>' + (pid ? '<code>' + escHtml(String(pid)) + '</code>' : '<span class="status-muted">-</span>') + '</td>'
2322
+ + '<td><span class="' + statusClass(st) + '">' + escHtml(t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
2323
+ + '<td>' + escHtml(s.runtimeActionLabel || "-") + '</td>'
2324
+ + '<td title="' + escHtml(s.workspacePath || "") + '">' + escHtml(ws) + '</td>'
2325
+ + '<td>' + escHtml(s.providerId || "-") + '</td></tr>';
1279
2326
  }).join("") +
1280
2327
  '</tbody></table>';
1281
2328
  }
@@ -1288,7 +2335,8 @@ function render(data) {
1288
2335
  document.getElementById("orphanTable").innerHTML =
1289
2336
  '<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thPid") + '</th></tr></thead><tbody>' +
1290
2337
  orphanProcs.map(p =>
1291
- '<tr><td>' + (p.agentId || '<span class="status-muted">-</span>') + '</td><td><code>' + p.pid + '</code></td></tr>'
2338
+ '<tr><td class="mono">' + (p.agentId ? escHtml(p.agentId) : '<span class="status-muted">-</span>') + '</td>'
2339
+ + '<td><code>' + escHtml(String(p.pid)) + '</code></td></tr>'
1292
2340
  ).join("") +
1293
2341
  '</tbody></table>';
1294
2342
  } else {
@@ -1296,33 +2344,42 @@ function render(data) {
1296
2344
  }
1297
2345
 
1298
2346
  // Unknown processes (collapsible, hidden by default)
1299
- document.getElementById("unknownCount").textContent = "(" + unknownProcs.length + ")";
2347
+ document.getElementById("unknownCount").textContent = unknownProcs.length ? "(" + unknownProcs.length + ")" : "";
1300
2348
  const unknownTable = document.getElementById("unknownTable");
1301
2349
  if (unknownProcs.length === 0) {
1302
- unknownTable.innerHTML = '<div class="empty">' + t("noProcesses") + '</div>';
2350
+ unknownTable.innerHTML =
2351
+ '<div class="empty"><span class="empty-icon">&#x25CB;</span>' + escHtml(t("noProcesses")) + '</div>';
1303
2352
  } else {
1304
2353
  unknownTable.innerHTML =
1305
2354
  '<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>' +
1306
2355
  unknownProcs.map(p =>
1307
- '<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>'
2356
+ '<tr><td><code>' + escHtml(String(p.pid)) + '</code></td>'
2357
+ + '<td class="mono">' + escHtml(p.command || "-") + '</td>'
2358
+ + '<td title="' + escHtml(p.cwd || "") + '">' + escHtml(fmtShortPath(p.cwd)) + '</td>'
2359
+ + '<td>' + (p.startedAt ? escHtml(fmtShortDate(p.startedAt)) : "-") + '</td>'
2360
+ + '<td><button class="btn-kill" data-pid="' + escHtml(String(p.pid)) + '">' + t("btnKill") + '</button></td></tr>'
1308
2361
  ).join("") +
1309
2362
  '</tbody></table>';
1310
2363
  }
1311
2364
 
1312
2365
  // Persisted sessions
1313
2366
  const persisted = data.sessions?.persisted || [];
1314
- document.getElementById("persistedCount").textContent = "(" + persisted.length + ")";
2367
+ document.getElementById("persistedCount").textContent = persisted.length ? "(" + persisted.length + ")" : "";
1315
2368
  if (persisted.length === 0) {
1316
- document.getElementById("persistedTable").innerHTML = '<div class="empty">' + t("noPersisted") + '</div>';
2369
+ document.getElementById("persistedTable").innerHTML =
2370
+ '<div class="empty"><span class="empty-icon">&#x25CB;</span>' + escHtml(t("noPersisted")) + '</div>';
1317
2371
  } else {
1318
2372
  document.getElementById("persistedTable").innerHTML =
1319
2373
  '<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>' +
1320
- persisted.map(s =>
1321
- '<tr><td>' + s.agentId + '</td><td>' + (s.status || "stopped") + '</td>'
1322
- + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-") + '</td>'
1323
- + '<td>' + (s.command || "-") + '</td>'
1324
- + '<td>' + fmtTime(s.startedAt) + '</td></tr>'
1325
- ).join("") +
2374
+ persisted.map(s => {
2375
+ const ws = s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-";
2376
+ const status = s.status || "stopped";
2377
+ return '<tr><td class="strong mono">' + escHtml(s.agentId || "-") + '</td>'
2378
+ + '<td><span class="tag ' + (status === "running" ? "green" : "gray") + '">' + escHtml(status) + '</span></td>'
2379
+ + '<td title="' + escHtml(s.workspacePath || "") + '">' + escHtml(ws) + '</td>'
2380
+ + '<td class="mono">' + escHtml(s.command || "-") + '</td>'
2381
+ + '<td>' + escHtml(fmtTime(s.startedAt)) + '</td></tr>';
2382
+ }).join("") +
1326
2383
  '</tbody></table>';
1327
2384
  }
1328
2385
  }
@@ -1503,6 +2560,7 @@ function renderSettingsUI() {
1503
2560
  document.getElementById("descRestart").textContent = t("setRestartDesc");
1504
2561
  document.getElementById("btnRestartBridge").textContent = t("setRestartBridge");
1505
2562
  document.getElementById("navNode").textContent = t("nodeTitle");
2563
+ document.getElementById("btnRefreshNodeInfo").textContent = t("nodeRefresh");
1506
2564
  document.getElementById("navLogs").textContent = t("logTitle");
1507
2565
  document.getElementById("navScheduler").textContent = t("schedTitle");
1508
2566
  document.getElementById("lblNodeHostname").textContent = t("nodeHostname");
@@ -1541,10 +2599,10 @@ function renderSettingsUI() {
1541
2599
  document.getElementById("optTunnelOff").textContent = t("schedTunnelOff");
1542
2600
  document.getElementById("lblSchedInstanceId").textContent = t("schedInstanceId");
1543
2601
  document.getElementById("hintSchedInstanceId").textContent = t("schedInstanceIdHint");
2602
+ document.getElementById("lblSchedAdvanced").textContent = t("schedAdvanced");
2603
+ updateSchedAdvancedBadge();
1544
2604
  document.getElementById("btnSaveServerConfig").textContent = t("schedSave");
1545
- document.getElementById("btnSaveRestart").textContent = t("schedSaveAndRestart");
1546
2605
  document.getElementById("descSched").textContent = t("schedDesc");
1547
- document.getElementById("btnReRegister").textContent = t("schedReRegister");
1548
2606
  document.getElementById("navAccess").textContent = t("accTitle");
1549
2607
  document.getElementById("lblAccessIps").textContent = t("accAllowIps");
1550
2608
  document.getElementById("hintAccessIps").textContent = t("accAllowIpsHint");
@@ -1656,6 +2714,8 @@ function confirmRestartBridge() {
1656
2714
  // \u2500\u2500 Node Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1657
2715
  let nodeInfoTimer = null;
1658
2716
  function refreshNodeInfo() {
2717
+ var btn = document.getElementById("btnRefreshNodeInfo");
2718
+ if (btn) { btn.disabled = true; btn.textContent = t("logRefreshing"); }
1659
2719
  fetch("/runtime/node-info").then(function(r) { return r.json(); }).then(function(d) {
1660
2720
  if (!d.ok) return;
1661
2721
  document.getElementById("valNodeHostname").textContent = d.hostname;
@@ -1670,7 +2730,12 @@ function refreshNodeInfo() {
1670
2730
  document.getElementById("valNodeRegistered").textContent = d.registered ? t("nodeRegYes") : t("nodeRegNo");
1671
2731
  document.getElementById("valNodeInstanceId").textContent = d.instanceId || "-";
1672
2732
  document.getElementById("valNodeActiveSessions").textContent = d.activeSessions;
1673
- }).catch(function() {});
2733
+ }).catch(function() {}).then(function() {
2734
+ // \u6210\u529F/\u5931\u8D25\u90FD\u6062\u590D\u6309\u94AE\uFF1B\u6210\u529F\u540E\u77ED\u6682\u663E\u793A\u5B8C\u6210\u6001\u518D\u8FD8\u539F\uFF0C\u7ED9\u7528\u6237"\u786E\u5B9E\u5237\u65B0\u4E86"\u7684\u53CD\u9988
2735
+ if (!btn) return;
2736
+ btn.disabled = false;
2737
+ btn.textContent = t("nodeRefresh");
2738
+ });
1674
2739
  }
1675
2740
 
1676
2741
  // \u2500\u2500 Logs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -1746,8 +2811,11 @@ function renderSchedTargetList() {
1746
2811
  var meta = document.createElement("div");
1747
2812
  meta.className = "s-target-meta";
1748
2813
  var parts = [];
2814
+ // \u540C\u4E00\u5730\u5740\u53EF\u914D\u591A\u884C\uFF08\u4E0D\u540C API Key = \u4E0D\u540C\u8D26\u53F7/\u79DF\u6237\uFF09\uFF0C\u5FC5\u987B\u8BA9\u7528\u6237\u770B\u5F97\u51FA\u662F\u54EA\u4E00\u6761
2815
+ if (item.userKeyMasked) parts.push(item.userKeyMasked);
1749
2816
  if (item.instanceName) parts.push(item.instanceName);
1750
2817
  if (item.instanceId) parts.push("ID: " + item.instanceId);
2818
+ if (item.tenantId) parts.push(t("schedTenantShort") + ": " + item.tenantId);
1751
2819
  if (item.tunnel === true) parts.push("tunnel");
1752
2820
  else if (item.tunnel === false && item.registerAddr) parts.push("direct");
1753
2821
  meta.textContent = parts.join(" \xB7 ");
@@ -1755,6 +2823,18 @@ function renderSchedTargetList() {
1755
2823
  main.appendChild(meta);
1756
2824
  var actions = document.createElement("div");
1757
2825
  actions.className = "s-target-actions";
2826
+ // \u590D\u5236\u8FDE\u63A5\u5BC6\u94A5\uFF1A\u7528\u6237\u8981\u5728 AwS \u9762\u677F\u767B\u8BB0\u8FD9\u53F0 bridge \u65F6\u5FC5\u987B\u586B\u5B83\uFF0C\u800C\u5217\u8868\u53EA\u663E\u793A\u63A9\u7801\uFF0C
2827
+ // \u6CA1\u6709\u8FD9\u4E2A\u5165\u53E3\u5C31\u53EA\u80FD\u53BB\u673A\u5668\u4E0A cat config.json\u3002\u4EC5\u5728\u5DF2\u914D\u7F6E\u5BC6\u94A5\u65F6\u51FA\u73B0\u3002
2828
+ if (item.connectionKeySet) {
2829
+ var copyBtn = document.createElement("button");
2830
+ copyBtn.className = "s-btn s-btn-sm s-btn-secondary";
2831
+ copyBtn.textContent = t("schedCopyConnKey");
2832
+ copyBtn.onclick = function(ev) {
2833
+ ev.stopPropagation();
2834
+ copyTargetConnectionKey(item.targetKey, copyBtn);
2835
+ };
2836
+ actions.appendChild(copyBtn);
2837
+ }
1758
2838
  var editBtn = document.createElement("button");
1759
2839
  editBtn.className = "s-btn s-btn-sm s-btn-secondary";
1760
2840
  editBtn.textContent = t("schedEditTarget");
@@ -1768,6 +2848,60 @@ function renderSchedTargetList() {
1768
2848
  }
1769
2849
  }
1770
2850
 
2851
+ // \u6309 targetKey \u53D6\u8BE5\u76EE\u6807\u7684\u660E\u6587\u8FDE\u63A5\u5BC6\u94A5\u5E76\u590D\u5236\u5230\u526A\u8D34\u677F\u3002
2852
+ // \u53EA\u5728\u9762\u677F\u5DF2\u767B\u5F55\u65F6\u53EF\u7528\uFF08\u540E\u7AEF requirePanelAuth\uFF09\uFF1B\u6309\u94AE\u6587\u6848\u5728\u6210\u529F/\u5931\u8D25\u540E\u77ED\u6682\u53D8\u5316\u7ED9\u51FA\u53CD\u9988\u3002
2853
+ function copyTargetConnectionKey(targetKey, btn) {
2854
+ var original = btn.textContent;
2855
+ btn.disabled = true;
2856
+ fetch("/runtime/server-config/connection-key?targetKey=" + encodeURIComponent(targetKey), {
2857
+ credentials: "same-origin",
2858
+ })
2859
+ .then(function(response) {
2860
+ if (!response.ok) {
2861
+ return response.json().then(function(data) {
2862
+ throw new Error((data && data.error) || ("HTTP " + response.status));
2863
+ });
2864
+ }
2865
+ return response.json();
2866
+ })
2867
+ .then(function(data) {
2868
+ var key = String((data && data.connectionKey) || "");
2869
+ if (!key) throw new Error("empty key");
2870
+ return copyTextToClipboard(key).then(function() {
2871
+ btn.textContent = "\u2713";
2872
+ setTimeout(function() { btn.textContent = original; btn.disabled = false; }, 1500);
2873
+ });
2874
+ })
2875
+ .catch(function(error) {
2876
+ btn.textContent = "\u590D\u5236\u5931\u8D25";
2877
+ console.warn("[Panel] \u590D\u5236\u8FDE\u63A5\u5BC6\u94A5\u5931\u8D25:", error);
2878
+ setTimeout(function() { btn.textContent = original; btn.disabled = false; }, 2000);
2879
+ });
2880
+ }
2881
+
2882
+ // \u4E0E\u6D4F\u89C8\u5668\u526A\u8D34\u677F API \u4EA4\u4E92\uFF1B\u4E0D\u53EF\u7528\u65F6\u56DE\u9000\u5230\u9690\u85CF textarea + execCommand\u3002
2883
+ function copyTextToClipboard(text) {
2884
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2885
+ return navigator.clipboard.writeText(text);
2886
+ }
2887
+ return new Promise(function(resolve, reject) {
2888
+ try {
2889
+ var area = document.createElement("textarea");
2890
+ area.value = text;
2891
+ area.setAttribute("readonly", "");
2892
+ area.style.position = "fixed";
2893
+ area.style.opacity = "0";
2894
+ document.body.appendChild(area);
2895
+ area.select();
2896
+ var ok = document.execCommand("copy");
2897
+ document.body.removeChild(area);
2898
+ ok ? resolve() : reject(new Error("execCommand copy failed"));
2899
+ } catch (error) {
2900
+ reject(error);
2901
+ }
2902
+ });
2903
+ }
2904
+
1771
2905
  // \u5C06\u6307\u5B9A target \u8F7D\u5165\u8868\u5355\uFF08\u7F16\u8F91\u6A21\u5F0F\uFF09\uFF1A\u5BC6\u94A5\u7559\u7A7A=\u4FDD\u6301\u4E0D\u53D8\uFF08placeholder \u663E\u793A\u63A9\u7801\uFF09
1772
2906
  function loadTargetToForm(idx) {
1773
2907
  schedEditIndex = idx;
@@ -1786,9 +2920,66 @@ function loadTargetToForm(idx) {
1786
2920
  ck.placeholder = item.connectionKeyMasked || "";
1787
2921
  document.getElementById("valSchedInstanceId").textContent = item.instanceId || "-";
1788
2922
  document.getElementById("btnDeleteTarget").style.display = "";
2923
+ // \u7F16\u8F91\u5DF2\u914D\u7F6E\u8FC7\u53EF\u9009\u9879\u7684\u76EE\u6807\u65F6\u81EA\u52A8\u5C55\u5F00\uFF0C\u8BA9\u7528\u6237\u770B\u5230\u73B0\u72B6\uFF1B\u5168\u65B0\u586B\u5199\u4FDD\u6301\u6536\u7EB3
2924
+ setSchedAdvancedOpen(countFilledAdvanced(item) > 0);
2925
+ updateSchedAdvancedBadge();
1789
2926
  renderSchedTargetList();
1790
2927
  }
1791
2928
 
2929
+ // \u2500\u2500 \u9AD8\u7EA7\u8BBE\u7F6E\u6298\u53E0\u533A \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2930
+
2931
+ /** \u7EDF\u8BA1\u53EF\u9009\u9879\u91CC\u5DF2\u6709\u5185\u5BB9\u7684\u5B57\u6BB5\u6570\uFF08\u5B9E\u4F8B\u540D\u79F0\u5728\u4E3B\u8868\u5355\u3001\u5B9E\u4F8B ID \u53EA\u8BFB\u56DE\u663E\uFF0C\u5747\u4E0D\u8BA1\u5165\uFF09\u3002 */
2932
+ function countFilledAdvanced(target) {
2933
+ var n = 0;
2934
+ if (target && String(target.tenantId || "").trim()) n += 1;
2935
+ if (target && String(target.connectionKeyMasked || "")) n += 1;
2936
+ if (target && String(target.registerAddr || "").trim()) n += 1;
2937
+ if (target && target.tunnel !== undefined && target.tunnel !== "auto" && target.tunnel !== null) n += 1;
2938
+ return n;
2939
+ }
2940
+
2941
+ function isSchedAdvancedOpen() {
2942
+ var body = document.getElementById("schedAdvancedBody");
2943
+ return Boolean(body && !body.hidden);
2944
+ }
2945
+
2946
+ function setSchedAdvancedOpen(open) {
2947
+ var body = document.getElementById("schedAdvancedBody");
2948
+ var head = document.getElementById("schedAdvancedHead");
2949
+ if (!body || !head) return;
2950
+ body.hidden = !open;
2951
+ head.setAttribute("aria-expanded", open ? "true" : "false");
2952
+ }
2953
+
2954
+ function toggleSchedAdvanced() {
2955
+ setSchedAdvancedOpen(!isSchedAdvancedOpen());
2956
+ }
2957
+
2958
+ // \u9AD8\u7EA7\u533A\u5185\u4EFB\u4F55\u8F93\u5165/\u9009\u62E9\u53D8\u5316\u90FD\u5237\u65B0\u6298\u53E0\u5934\u5FBD\u6807\uFF08\u4E8B\u4EF6\u59D4\u6258\uFF0C\u5B57\u6BB5\u589E\u51CF\u4E0D\u7528\u91CD\u6302\uFF09
2959
+ document.addEventListener("input", function(e) {
2960
+ if (!e.target || !e.target.closest) return;
2961
+ if (e.target.closest("#schedAdvancedBody")) updateSchedAdvancedBadge();
2962
+ });
2963
+ document.addEventListener("change", function(e) {
2964
+ if (!e.target || !e.target.closest) return;
2965
+ if (e.target.closest("#schedAdvancedBody")) updateSchedAdvancedBadge();
2966
+ });
2967
+
2968
+ /** \u6298\u53E0\u5934\u5FBD\u6807\uFF1A\u6536\u7EB3\u72B6\u6001\u4E0B\u63D0\u793A"\u91CC\u9762\u6709\u51E0\u4E2A\u53EF\u9009\u9879\u5DF2\u586B"\uFF0C\u8BA9\u7528\u6237\u4E0D\u5C55\u5F00\u4E5F\u77E5\u9053\u73B0\u72B6\u3002 */
2969
+ function updateSchedAdvancedBadge() {
2970
+ var el = document.getElementById("schedAdvancedCount");
2971
+ if (!el) return;
2972
+ var filled = countFilledAdvanced({
2973
+ tenantId: document.getElementById("cfgTenantId").value,
2974
+ connectionKeyMasked: document.getElementById("cfgConnectionKey").placeholder,
2975
+ registerAddr: document.getElementById("cfgRegisterAddr").value,
2976
+ tunnel: document.getElementById("cfgTunnel").value,
2977
+ });
2978
+ el.textContent = filled > 0
2979
+ ? t("schedAdvancedN").replace("{n}", String(filled))
2980
+ : "";
2981
+ }
2982
+
1792
2983
  // \u8FDB\u5165\u65B0\u589E\u6A21\u5F0F\uFF1A\u6E05\u7A7A\u8868\u5355
1793
2984
  function startAddTarget() {
1794
2985
  schedEditIndex = -1;
@@ -1800,10 +2991,76 @@ function startAddTarget() {
1800
2991
  document.getElementById("cfgTunnel").value = "auto";
1801
2992
  document.getElementById("valSchedInstanceId").textContent = "-";
1802
2993
  document.getElementById("btnDeleteTarget").style.display = "none";
2994
+ // \u65B0\u589E\uFF1A\u53EF\u9009\u9879\u5168\u90E8\u4E3A\u7A7A\uFF0C\u9ED8\u8BA4\u6536\u7EB3\uFF08\u9884\u586B\u53EF\u80FD\u968F\u540E\u5C55\u5F00\uFF09
2995
+ setSchedAdvancedOpen(false);
2996
+ updateSchedAdvancedBadge();
1803
2997
  hideSchedMsg();
2998
+ applyPrefillIfPending();
1804
2999
  renderSchedTargetList();
1805
3000
  }
1806
3001
 
3002
+ /**
3003
+ * Dashboard\u300C\u5F15\u5BFC\u914D\u7F6E\u300D\u8DF3\u8F6C\u53C2\u6570\uFF08URL hash\uFF09\uFF1A
3004
+ * #sched=add&serverUrl=...&instanceName=...&tunnel=auto
3005
+ *
3006
+ * \u5B89\u5168\u7EA6\u675F\uFF1A**\u7EDD\u4E0D\u63A5\u53D7 userKey / connectionKey**\u3002\u5BC6\u94A5\u53EA\u8D70\u7528\u6237\u624B\u8F93\u6216 config.json\uFF0C
3007
+ * \u653E\u8FDB URL \u4F1A\u7559\u5728\u6D4F\u89C8\u5668\u5386\u53F2\u3001\u4EE3\u7406\u65E5\u5FD7\u4E0E\u622A\u56FE\u91CC\u3002
3008
+ */
3009
+ var schedPrefill = null;
3010
+
3011
+ function readSchedPrefill() {
3012
+ var raw = String(window.location.hash || "").replace(/^#/, "");
3013
+ if (!raw) return null;
3014
+ var params = {};
3015
+ raw.split("&").forEach(function(pair) {
3016
+ var idx = pair.indexOf("=");
3017
+ if (idx < 0) return;
3018
+ var key = decodeURIComponent(pair.slice(0, idx));
3019
+ // \u6CE8\u610F\uFF1A\u672C\u6BB5\u4F4D\u4E8E\u6A21\u677F\u5B57\u7B26\u4E32\u5185\uFF0C\u6B63\u5219\u91CC\u7684\u53CD\u659C\u6760\u5FC5\u987B\u5199\u4E24\u904D\uFF08\u5426\u5219\u4F1A\u88AB\u5F53\u6210\u8F6C\u4E49\u7B26\uFF09\u3002
3020
+ var value = decodeURIComponent(pair.slice(idx + 1).replace(/\\+/g, " "));
3021
+ params[key] = value;
3022
+ });
3023
+ if (params.sched !== "add") return null;
3024
+ var serverUrl = String(params.serverUrl || "").trim();
3025
+ if (!serverUrl) return null;
3026
+ var tunnel = String(params.tunnel || "auto");
3027
+ if (tunnel !== "true" && tunnel !== "false" && tunnel !== "auto") tunnel = "auto";
3028
+ return {
3029
+ serverUrl: serverUrl,
3030
+ instanceName: String(params.instanceName || "").trim(),
3031
+ tunnel: tunnel,
3032
+ };
3033
+ }
3034
+
3035
+ /** \u628A\u5E26\u5165\u503C\u5199\u8FDB\u300C\u65B0\u589E\u300D\u8868\u5355\uFF08\u4EC5\u5728\u7528\u6237\u8FD8\u6CA1\u8F93\u5165\u5185\u5BB9\u65F6\u8986\u76D6\uFF0C\u907F\u514D\u6253\u65AD\u624B\u52A8\u7F16\u8F91\uFF09 */
3036
+ function applyPrefillIfPending() {
3037
+ if (!schedPrefill || schedEditIndex >= 0) return;
3038
+ var serverUrlInput = document.getElementById("cfgServerUrl");
3039
+ if (!serverUrlInput) return;
3040
+ if (!serverUrlInput.value.trim()) serverUrlInput.value = schedPrefill.serverUrl;
3041
+ var nameInput = document.getElementById("cfgInstanceName");
3042
+ if (nameInput && !nameInput.value.trim() && schedPrefill.instanceName) {
3043
+ nameInput.value = schedPrefill.instanceName;
3044
+ }
3045
+ var tunnelSelect = document.getElementById("cfgTunnel");
3046
+ if (tunnelSelect) tunnelSelect.value = schedPrefill.tunnel;
3047
+ updateSchedAdvancedBadge();
3048
+ showPrefillNotice();
3049
+ }
3050
+
3051
+ function showPrefillNotice() {
3052
+ var notice = document.getElementById("schedPrefillNotice");
3053
+ if (!notice || !schedPrefill) return;
3054
+ notice.textContent =
3055
+ t("schedPrefillNotice").replace("{0}", schedPrefill.serverUrl);
3056
+ notice.style.display = "block";
3057
+ }
3058
+
3059
+ function hidePrefillNotice() {
3060
+ var notice = document.getElementById("schedPrefillNotice");
3061
+ if (notice) notice.style.display = "none";
3062
+ }
3063
+
1807
3064
  function hideSchedMsg() {
1808
3065
  var msg = document.getElementById("schedMsg");
1809
3066
  msg.className = "s-msg";
@@ -1816,14 +3073,16 @@ function showSchedMsg(text, ok) {
1816
3073
  msg.className = "s-msg " + (ok ? "success" : "error");
1817
3074
  }
1818
3075
 
1819
- // \u8868\u5355 \u2192 target \u5BF9\u8C61\uFF08\u5BC6\u94A5\u539F\u6837\u643A\u5E26\uFF0C\u7559\u7A7A\u7531\u540E\u7AEF\u6309 originalServerUrl/serverUrl \u7EE7\u627F\uFF09
3076
+ // \u8868\u5355 \u2192 target \u5BF9\u8C61\uFF08\u5BC6\u94A5\u539F\u6837\u643A\u5E26\uFF0C\u7559\u7A7A\u7531\u540E\u7AEF\u6309 originalTargetKey/\u539F\u5730\u5740\u7EE7\u627F\uFF09
1820
3077
  function formToTarget() {
3078
+ var editing = schedEditIndex >= 0 ? schedTargets[schedEditIndex] : null;
1821
3079
  return {
1822
3080
  serverUrl: document.getElementById("cfgServerUrl").value.trim(),
1823
- // \u7F16\u8F91\u6A21\u5F0F\u643A\u5E26\u539F\u5730\u5740\uFF1A\u5730\u5740\u53D8\u66F4\u65F6\u540E\u7AEF\u6309\u539F\u5730\u5740\u7EE7\u627F instanceId/\u5BC6\u94A5\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF0C\u65E0\u9700\u91CD\u65B0\u6CE8\u518C\uFF09
1824
- originalServerUrl: schedEditIndex >= 0 && schedTargets[schedEditIndex]
1825
- ? (schedTargets[schedEditIndex].serverUrl || "")
1826
- : "",
3081
+ // \u7F16\u8F91\u6A21\u5F0F\u643A\u5E26\u539F\u8EAB\u4EFD\uFF1A\u540C\u5740\u53EF\u80FD\u6709\u591A\u884C\uFF08\u4E0D\u540C API Key\uFF09\uFF0C
3082
+ // originalTargetKey \u8BA9\u540E\u7AEF\u7CBE\u786E\u77E5\u9053\u8981\u7EE7\u627F\u54EA\u4E00\u6761\u7684 instanceId / \u672A\u66B4\u9732\u5B57\u6BB5
3083
+ originalTargetKey: editing ? (editing.targetKey || "") : "",
3084
+ // \u517C\u5BB9\u65E7\u540E\u7AEF/\u6539\u5730\u5740\u573A\u666F\uFF1A\u5730\u5740\u53D8\u66F4\u65F6\u6309\u539F\u5730\u5740\u7EE7\u627F\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF0C\u65E0\u9700\u91CD\u65B0\u6CE8\u518C\uFF09
3085
+ originalServerUrl: editing ? (editing.serverUrl || "") : "",
1827
3086
  userKey: document.getElementById("cfgUserKey").value,
1828
3087
  tenantId: document.getElementById("cfgTenantId").value.trim(),
1829
3088
  instanceName: document.getElementById("cfgInstanceName").value.trim(),
@@ -1844,6 +3103,40 @@ function refreshServerConfig() {
1844
3103
  }).catch(function() {});
1845
3104
  }
1846
3105
 
3106
+ /**
3107
+ * \u5F52\u4E00\u5316\u5730\u5740\u7528\u4E8E\u300C\u65B0\u589E\u65F6\u649E\u5DF2\u6709\u5730\u5740\u300D\u7684\u6BD4\u8F83\uFF08\u4E0E\u540E\u7AEF\u4E00\u81F4\uFF0C\u5FFD\u7565\u5C3E\u659C\u6760/\u534F\u8BAE\u5927\u5C0F\u5199/localhost\uFF09\u3002
3108
+ * \u6CE8\u610F\uFF1A\u672C\u6BB5\u4F4D\u4E8E\u6A21\u677F\u5B57\u7B26\u4E32\u5185\uFF0C\u6B63\u5219\u91CC\u7684\u53CD\u659C\u6760\u5FC5\u987B\u5199\u4E24\u904D\uFF08\u5426\u5219\u4F1A\u88AB\u5F53\u6210\u8F6C\u4E49\u7B26\uFF09\u3002
3109
+ */
3110
+ function normalizeTargetUrlForCompare(raw) {
3111
+ var value = String(raw || "").trim();
3112
+ if (!value) return "";
3113
+ try {
3114
+ var url = new URL(value);
3115
+ url.protocol = url.protocol.toLowerCase();
3116
+ if (url.hostname.toLowerCase() === "localhost") url.hostname = "127.0.0.1";
3117
+ return url.origin;
3118
+ } catch (e) {
3119
+ return value.replace(/\\/+$/, "");
3120
+ }
3121
+ }
3122
+
3123
+ /**
3124
+ * \u65B0\u589E\u65F6\u67E5\u627E\u300C\u540C\u5730\u5740 + \u540C API Key\u300D\u7684\u5DF2\u6709\u6761\u76EE\uFF08-1 = \u65E0\u51B2\u7A81\uFF09\u3002
3125
+ * \u540C\u5730\u5740\u4E0D\u540C Key \u5C5E\u4E8E\u4E0D\u540C\u8D26\u53F7/\u79DF\u6237\uFF0C\u662F\u5408\u6CD5\u914D\u7F6E\uFF0C\u4E0D\u7B97\u91CD\u590D\uFF1B
3126
+ * \u53EA\u6709\u5730\u5740\u4E0E Key \u90FD\u76F8\u540C\u624D\u662F\u540C\u4E00\u4E2A\u76EE\u6807\u6CE8\u518C\u4E24\u6B21\u3002
3127
+ */
3128
+ function findDuplicateTargetIndex(serverUrl, userKey) {
3129
+ var urlKey = normalizeTargetUrlForCompare(serverUrl);
3130
+ var keyText = String(userKey || "").trim();
3131
+ if (!urlKey || !keyText) return -1;
3132
+ for (var i = 0; i < schedTargets.length; i++) {
3133
+ if (normalizeTargetUrlForCompare(schedTargets[i].serverUrl) !== urlKey) continue;
3134
+ // \u5DF2\u6709\u6761\u76EE\u53EA\u56DE\u4F20\u63A9\u7801\uFF0C\u65E0\u6CD5\u6BD4\u5BF9\u660E\u6587 Key\uFF1A\u5730\u5740\u76F8\u540C\u5373\u63D0\u793A\uFF08\u7531\u540E\u7AEF\u505A\u6700\u7EC8\u5224\u5B9A\uFF09
3135
+ return i;
3136
+ }
3137
+ return -1;
3138
+ }
3139
+
1847
3140
  // \u5168\u91CF\u63D0\u4EA4 targets\uFF1BrestartAfter \u4E3A true \u65F6\u4FDD\u5B58\u6210\u529F\u540E\u91CD\u542F Bridge
1848
3141
  function saveServerConfig(restartAfter) {
1849
3142
  var form = formToTarget();
@@ -1852,6 +3145,17 @@ function saveServerConfig(restartAfter) {
1852
3145
  return Promise.resolve(false);
1853
3146
  }
1854
3147
 
3148
+ // \u65B0\u589E\u6A21\u5F0F\u4E0B\u82E5\u5730\u5740\u5DF2\u5B58\u5728\uFF1A\u540C\u5740\u5141\u8BB8\u65B0\u589E\uFF08\u4E0D\u540C API Key = \u4E0D\u540C\u8D26\u53F7/\u79DF\u6237\uFF09\uFF0C
3149
+ // \u4F46\u540C\u4E00\u4E2A Key \u518D\u5B58\u4E00\u6B21\u6CA1\u6709\u610F\u4E49\u3002\u5DF2\u6709\u6761\u76EE\u53EA\u56DE\u4F20\u63A9\u7801\uFF0C\u65E0\u6CD5\u6BD4\u5BF9\u65B0\u586B\u7684\u660E\u6587 Key\uFF0C
3150
+ // \u56E0\u6B64\u8FD9\u91CC\u53EA\u505A\u63D0\u793A\uFF0C\u6700\u7EC8\u4EE5\u670D\u52A1\u7AEF\u6309\u300C\u5730\u5740 + userKey\u300D\u7684\u8EAB\u4EFD\u6821\u9A8C\u4E3A\u51C6\u3002
3151
+ if (schedEditIndex < 0) {
3152
+ var sameUrlIndex = findDuplicateTargetIndex(form.serverUrl, form.userKey);
3153
+ if (sameUrlIndex >= 0 && !form.userKey) {
3154
+ showSchedMsg(t("schedDupNoKey"), false);
3155
+ return Promise.resolve(false);
3156
+ }
3157
+ }
3158
+
1855
3159
  // \u7EC4\u88C5\u5168\u91CF\u5217\u8868\uFF1A\u7F16\u8F91\u6A21\u5F0F\u66FF\u6362\u5BF9\u5E94\u9879\uFF0C\u65B0\u589E\u6A21\u5F0F\u8FFD\u52A0\uFF1B\u5176\u4F59\u9879\u4ECE\u7F13\u5B58\u89C6\u56FE\u539F\u6837\u5E26\u4E0A\uFF08\u5BC6\u94A5\u7559\u7A7A\u7531\u540E\u7AEF\u7EE7\u627F\uFF09
1856
3160
  var list = schedTargets.map(function(item) {
1857
3161
  return {
@@ -1883,23 +3187,15 @@ function saveServerConfig(restartAfter) {
1883
3187
  refreshServerConfig();
1884
3188
  schedEditIndex = keepIndex;
1885
3189
  if (restartAfter) {
3190
+ // \u300C\u4FDD\u5B58\u5E76\u91CD\u542F\u300D\u5165\u53E3\uFF08\u7EF4\u62A4\u9875\u7B49\uFF09\uFF1A\u4FDD\u6301\u539F\u6709\u7ACB\u5373\u91CD\u542F\u8BED\u4E49
1886
3191
  showSchedMsg(t("schedSaved"), true);
1887
3192
  doRestartBridge();
1888
3193
  return true;
1889
3194
  }
1890
- // \u70ED\u5207\u6362\uFF1A\u4FDD\u5B58\u540E\u7ACB\u5373\u6309\u65B0\u914D\u7F6E\u91CD\u65B0\u6CE8\u518C\uFF08\u7F16\u8F91\u5730\u5740/\u65B0\u589E target \u5747\u5373\u65F6\u751F\u6548\uFF0C
1891
- // \u65E0\u9700\u91CD\u542F Bridge\uFF1B\u5B9E\u4F8B\u4FE1\u606F\u6309 instanceId \u5728\u670D\u52A1\u7AEF\u590D\u7528\uFF09
1892
- showSchedMsg(t("schedSavedRegistering"), true);
1893
- btn.textContent = t("schedReRegistering");
1894
- return runReRegister().then(function(reg) {
1895
- if (reg.ok) {
1896
- showSchedMsg(t("schedReRegistered"), true);
1897
- refreshNodeInfo();
1898
- } else {
1899
- showSchedMsg(t("schedReRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
1900
- }
1901
- return reg.ok;
1902
- });
3195
+ // \u4FDD\u5B58\u6210\u529F\uFF1A\u5F39\u6846\u8BA9\u7528\u6237\u9009\u62E9\u5982\u4F55\u5E94\u7528\uFF08\u91CD\u65B0\u6CE8\u518C\u4E3A\u63A8\u8350\u9879\uFF0C\u4E5F\u53EF\u8DF3\u8FC7\uFF09\uFF0C
3196
+ // \u4E0D\u518D\u81EA\u52A8\u9759\u9ED8\u70ED\u5207\u6362\u2014\u2014\u907F\u514D\u7528\u6237\u8FD8\u6CA1\u770B\u6E05\u63D0\u793A\uFF0C\u6CE8\u518C\u5C31\u5DF2\u7ECF\u53D1\u751F
3197
+ showAfterSaveModal();
3198
+ return true;
1903
3199
  }
1904
3200
  showSchedMsg(t("schedSaveFail") + (d.error ? ": " + d.error : ""), false);
1905
3201
  return false;
@@ -1915,10 +3211,64 @@ function saveServerConfig(restartAfter) {
1915
3211
  });
1916
3212
  }
1917
3213
 
1918
- // \u4FDD\u5B58\u5E76\u91CD\u542F\uFF1A\u5148\u786E\u8BA4\uFF0C\u4FDD\u5B58\u6210\u529F\u540E\u7531 saveServerConfig \u89E6\u53D1\u91CD\u542F
1919
- function saveServerConfigAndRestart() {
1920
- if (!confirm(t("schedSaveRestartConfirm"))) return;
1921
- saveServerConfig(true);
3214
+ // \u2500\u2500 \u4FDD\u5B58\u6210\u529F\u540E\u7684\u300C\u5E94\u7528\u65B9\u5F0F\u300D\u5F39\u6846 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3215
+ function showAfterSaveModal() {
3216
+ document.getElementById("aftersaveTitle").textContent = t("aftersaveTitle");
3217
+ document.getElementById("aftersaveDesc").textContent = t("aftersaveDesc");
3218
+ var regBtn = document.getElementById("aftersaveBtnRegister");
3219
+ regBtn.textContent = t("aftersaveRegister");
3220
+ regBtn.setAttribute("data-recommended", t("aftersaveRegisterRec"));
3221
+ regBtn.disabled = false;
3222
+ var restartBtn = document.getElementById("aftersaveBtnRestart");
3223
+ restartBtn.textContent = t("aftersaveRestart");
3224
+ restartBtn.title = t("aftersaveRestartHint");
3225
+ restartBtn.disabled = false;
3226
+ document.getElementById("aftersaveBtnSkip").textContent = t("aftersaveSkip");
3227
+ document.getElementById("afterSaveModal").style.display = "flex";
3228
+ }
3229
+
3230
+ function closeAfterSave(e) {
3231
+ // \u70B9\u906E\u7F69\u7B49\u540C\u300C\u6682\u4E0D\u300D\uFF1A\u4FDD\u5B58\u5DF2\u6210\u529F\uFF0C\u6CA1\u6709\u53EF\u53D6\u6D88\u7684\u64CD\u4F5C
3232
+ if (e && e.target !== e.currentTarget) return;
3233
+ afterSaveAction("skip");
3234
+ }
3235
+
3236
+ function afterSaveAction(action) {
3237
+ var modal = document.getElementById("afterSaveModal");
3238
+ var regBtn = document.getElementById("aftersaveBtnRegister");
3239
+ var restartBtn = document.getElementById("aftersaveBtnRestart");
3240
+ var msg = document.getElementById("schedMsg");
3241
+
3242
+ if (action === "register") {
3243
+ regBtn.disabled = true;
3244
+ regBtn.textContent = t("aftersaveRegistering");
3245
+ runReRegister().then(function(reg) {
3246
+ modal.style.display = "none";
3247
+ regBtn.disabled = false;
3248
+ regBtn.textContent = t("aftersaveRegister");
3249
+ if (reg.ok) {
3250
+ showSchedMsg(t("aftersaveRegisterOk"), true);
3251
+ refreshNodeInfo();
3252
+ } else {
3253
+ showSchedMsg(t("aftersaveRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
3254
+ }
3255
+ });
3256
+ return;
3257
+ }
3258
+
3259
+ if (action === "restart") {
3260
+ restartBtn.disabled = true;
3261
+ restartBtn.textContent = t("aftersaveRestarting");
3262
+ modal.style.display = "none";
3263
+ restartBtn.disabled = false;
3264
+ restartBtn.textContent = t("aftersaveRestart");
3265
+ doRestartBridge();
3266
+ return;
3267
+ }
3268
+
3269
+ // skip\uFF1A\u914D\u7F6E\u4FDD\u6301\u843D\u76D8\u72B6\u6001\uFF0C\u4E0B\u6B21 Bridge \u542F\u52A8\u65F6\u751F\u6548
3270
+ modal.style.display = "none";
3271
+ showSchedMsg(t("aftersaveSkipDone"), true);
1922
3272
  }
1923
3273
 
1924
3274
  // \u5220\u9664\u5F53\u524D\u7F16\u8F91\u7684 target\uFF1A\u4ECE\u5217\u8868\u79FB\u9664\u540E\u5168\u91CF\u63D0\u4EA4
@@ -1972,37 +3322,13 @@ function deleteTargetFromList() {
1972
3322
  });
1973
3323
  }
1974
3324
 
1975
- // \u53D1\u8D77\u91CD\u65B0\u6CE8\u518C\u8BF7\u6C42\uFF08\u4E0D\u7BA1\u7406\u6309\u94AE\u72B6\u6001\uFF0C\u4F9B\u624B\u52A8\u6309\u94AE\u4E0E\u4FDD\u5B58\u540E\u7684\u70ED\u5207\u6362\u94FE\u5171\u7528\uFF09
3325
+ // \u53D1\u8D77\u91CD\u65B0\u6CE8\u518C\u8BF7\u6C42\uFF08\u4E0D\u7BA1\u7406\u6309\u94AE\u72B6\u6001\uFF0C\u4F9B\u5F39\u6846\u4E0E\u5220\u9664\u540E\u7684\u70ED\u5207\u6362\u94FE\u5171\u7528\uFF09
1976
3326
  function runReRegister() {
1977
3327
  return fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1978
3328
  .then(function(r) { return r.json(); })
1979
3329
  .catch(function() { return { ok: false }; });
1980
3330
  }
1981
3331
 
1982
- function doReRegister() {
1983
- var btn = document.getElementById("btnReRegister");
1984
- var msg = document.getElementById("schedMsg");
1985
- msg.className = "s-msg";
1986
- msg.style.display = "none";
1987
- btn.disabled = true;
1988
- btn.textContent = t("schedReRegistering");
1989
- runReRegister()
1990
- .then(function(d) {
1991
- if (d.ok) {
1992
- msg.textContent = t("schedReRegistered");
1993
- msg.className = "s-msg success";
1994
- refreshNodeInfo();
1995
- } else {
1996
- msg.textContent = t("schedReRegisterFail") + (d.error ? ": " + d.error : "");
1997
- msg.className = "s-msg error";
1998
- }
1999
- })
2000
- .then(function() {
2001
- btn.disabled = false;
2002
- btn.textContent = t("schedReRegister");
2003
- });
2004
- }
2005
-
2006
3332
  // \u2500\u2500 Access Control \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2007
3333
  // \u52A0\u8F7D\u5F53\u524D IP \u767D\u540D\u5355\u5230\u8868\u5355\uFF08\u7A7A\u5217\u8868 = \u9ED8\u8BA4\u5168\u653E\u884C\uFF09
2008
3334
  function refreshAccessControl() {
@@ -2089,109 +3415,132 @@ switchSettingsPage = function(page) {
2089
3415
  else if (page === "logs") { refreshLogs(); startLogAutoRefresh(); }
2090
3416
  else if (page === "scheduler") { refreshNodeInfo(); refreshServerConfig(); }
2091
3417
  };
3418
+
3419
+ // \u2500\u2500 Dashboard\u300C\u5F15\u5BFC\u914D\u7F6E\u300D\u5E26\u6765\u7684\u8FDE\u63A5\u4FE1\u606F\uFF08#sched=add&serverUrl=...\uFF09\u2500\u2500
3420
+ // \u6253\u5F00\u8BBE\u7F6E\u5E76\u5207\u5230\u300C\u8C03\u5EA6\u4E2D\u5FC3\u300D\u9875\uFF1BuserKey \u5FC5\u987B\u7531\u7528\u6237\u624B\u52A8\u7C98\u8D34\uFF08\u4E0D\u8FDB URL\uFF09\u3002
3421
+ schedPrefill = readSchedPrefill();
3422
+ if (schedPrefill) {
3423
+ openSettings();
3424
+ switchSettingsPage("scheduler");
3425
+ }
2092
3426
  </script>
2093
3427
  </body>
2094
- </html>`,Me=`<!DOCTYPE html>
3428
+ </html>`,Fe=`<!DOCTYPE html>
2095
3429
  <html lang="zh-CN">
2096
3430
  <head>
2097
3431
  <meta charset="UTF-8">
2098
3432
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2099
3433
  <title>Bridge Dashboard - Login</title>
3434
+ ${O}
3435
+ ${z}
2100
3436
  <style>
2101
- :root {
2102
- --bg: #0f1117;
2103
- --surface: #1a1d2e;
2104
- --surface2: #232738;
2105
- --border: #2d3248;
2106
- --text: #e1e4f0;
2107
- --text2: #8b90a8;
2108
- --accent: #5b8def;
2109
- --accent-hover: #4a7de0;
2110
- --red: #f05454;
2111
- --radius: 10px;
2112
- }
2113
- * { margin:0; padding:0; box-sizing:border-box; }
3437
+ /* \u4E0E\u4E3B\u9762\u677F\u5171\u7528\u540C\u4E00\u5957\u8BBE\u8BA1\u4EE4\u724C\uFF08\u9ED8\u8BA4\u6DF1\u8272\uFF0Chtml.light-theme \u5207\u6D45\u8272\uFF09 */
3438
+ ${j}
3439
+
3440
+ * { margin: 0; padding: 0; box-sizing: border-box; }
3441
+
2114
3442
  body {
2115
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
3443
+ font-family: var(--font-sans);
2116
3444
  background: var(--bg);
2117
3445
  color: var(--text);
2118
3446
  min-height: 100vh;
2119
3447
  display: flex;
2120
3448
  align-items: center;
2121
3449
  justify-content: center;
3450
+ padding: 20px;
3451
+ -webkit-font-smoothing: antialiased;
2122
3452
  }
3453
+
2123
3454
  .login-card {
2124
3455
  background: var(--surface);
2125
3456
  border: 1px solid var(--border);
2126
- border-radius: var(--radius);
2127
- padding: 40px 36px;
2128
- width: 360px;
2129
- max-width: 90vw;
3457
+ border-radius: var(--radius-lg);
3458
+ box-shadow: var(--shadow-2);
3459
+ padding: 36px 32px 32px;
3460
+ width: 384px;
3461
+ max-width: 100%;
3462
+ }
3463
+
3464
+ .login-brand {
3465
+ display: flex;
3466
+ align-items: center;
3467
+ gap: 10px;
3468
+ justify-content: center;
3469
+ margin-bottom: 20px;
2130
3470
  }
3471
+ .brand-mark {
3472
+ width: 26px;
3473
+ height: 26px;
3474
+ border-radius: 7px;
3475
+ object-fit: contain;
3476
+ flex-shrink: 0;
3477
+ }
3478
+ .login-brand-name { font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
3479
+
2131
3480
  .login-card h1 {
2132
- font-size: 20px;
2133
- font-weight: 600;
2134
- margin-bottom: 4px;
3481
+ font-size: 19px;
3482
+ font-weight: 650;
3483
+ letter-spacing: -0.02em;
2135
3484
  text-align: center;
3485
+ margin-bottom: 6px;
2136
3486
  }
2137
3487
  .login-card p.subtitle {
2138
3488
  font-size: 13px;
2139
- color: var(--text2);
3489
+ color: var(--text-3);
2140
3490
  text-align: center;
2141
- margin-bottom: 28px;
2142
- }
2143
- .form-group {
2144
- margin-bottom: 18px;
3491
+ margin-bottom: 26px;
2145
3492
  }
3493
+
3494
+ .form-group { margin-bottom: 16px; }
2146
3495
  .form-group label {
2147
3496
  display: block;
2148
- font-size: 13px;
2149
- font-weight: 500;
2150
- margin-bottom: 6px;
2151
- color: var(--text2);
3497
+ font-size: 12.5px;
3498
+ font-weight: 600;
3499
+ margin-bottom: 7px;
3500
+ color: var(--text-2);
2152
3501
  }
2153
3502
  .form-group input {
2154
3503
  width: 100%;
2155
3504
  padding: 10px 12px;
2156
- background: var(--surface2);
3505
+ background: var(--bg);
2157
3506
  border: 1px solid var(--border);
2158
- border-radius: 6px;
3507
+ border-radius: var(--radius-sm);
2159
3508
  color: var(--text);
2160
- font-size: 14px;
3509
+ font-size: 13.5px;
3510
+ font-family: inherit;
2161
3511
  outline: none;
2162
- transition: border-color .15s;
3512
+ transition: border-color .15s, box-shadow .15s;
2163
3513
  }
2164
3514
  .form-group input:focus {
2165
3515
  border-color: var(--accent);
3516
+ box-shadow: 0 0 0 3px var(--accent-soft);
2166
3517
  }
2167
- .form-group input::placeholder {
2168
- color: var(--text2);
2169
- opacity: 0.6;
2170
- }
3518
+ .form-group input::placeholder { color: var(--text-3); }
3519
+
2171
3520
  button {
2172
3521
  width: 100%;
2173
3522
  padding: 10px;
2174
3523
  background: var(--accent);
2175
3524
  border: none;
2176
- border-radius: 6px;
3525
+ border-radius: var(--radius-sm);
2177
3526
  color: #fff;
2178
- font-size: 14px;
3527
+ font-size: 13.5px;
2179
3528
  font-weight: 600;
3529
+ font-family: inherit;
2180
3530
  cursor: pointer;
2181
- transition: background .15s;
2182
- margin-top: 4px;
3531
+ transition: background .15s, opacity .15s;
3532
+ margin-top: 6px;
2183
3533
  }
2184
3534
  button:hover { background: var(--accent-hover); }
2185
- button:disabled {
2186
- opacity: 0.5;
2187
- cursor: not-allowed;
2188
- }
3535
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
3536
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
3537
+
2189
3538
  .error-msg {
2190
- background: rgba(240,84,84,0.12);
2191
- color: var(--red);
2192
- padding: 8px 12px;
2193
- border-radius: 6px;
2194
- font-size: 13px;
3539
+ background: var(--danger-soft);
3540
+ color: var(--danger);
3541
+ padding: 9px 12px;
3542
+ border-radius: var(--radius-sm);
3543
+ font-size: 12.5px;
2195
3544
  margin-bottom: 16px;
2196
3545
  display: none;
2197
3546
  }
@@ -2199,23 +3548,40 @@ switchSettingsPage = function(page) {
2199
3548
  </style>
2200
3549
  </head>
2201
3550
  <body>
3551
+ <button type="button" class="theme-fab" id="themeFab" onclick="toggleThemeFab()" title="\u5207\u6362\u4E3B\u9898">&#x263D;</button>
2202
3552
  <div class="login-card">
2203
- <h1>Bridge Dashboard</h1>
2204
- <p class="subtitle">Please log in to continue</p>
3553
+ <div class="login-brand">
3554
+ <img class="brand-mark" src="${Y}" alt="AgentsWorkStudio Bridge">
3555
+ <span class="login-brand-name">AgentsWorkStudio</span>
3556
+ </div>
3557
+ <h1>Bridge \u63A7\u5236\u53F0</h1>
3558
+ <p class="subtitle">\u767B\u5F55\u540E\u53EF\u7BA1\u7406\u672C\u673A Bridge</p>
2205
3559
  <div class="error-msg" id="errorMsg"></div>
2206
3560
  <form id="loginForm" onsubmit="return doLogin(event)">
2207
3561
  <div class="form-group">
2208
- <label for="username">Username</label>
2209
- <input type="text" id="username" name="username" placeholder="Username" autocomplete="username" autofocus>
3562
+ <label for="username">\u7528\u6237\u540D</label>
3563
+ <input type="text" id="username" name="username" placeholder="\u8BF7\u8F93\u5165\u7528\u6237\u540D" autocomplete="username" autofocus>
2210
3564
  </div>
2211
3565
  <div class="form-group">
2212
- <label for="password">Password</label>
2213
- <input type="password" id="password" name="password" placeholder="Password" autocomplete="current-password">
3566
+ <label for="password">\u5BC6\u7801</label>
3567
+ <input type="password" id="password" name="password" placeholder="\u8BF7\u8F93\u5165\u5BC6\u7801" autocomplete="current-password">
2214
3568
  </div>
2215
- <button type="submit" id="loginBtn">Log In</button>
3569
+ <button type="submit" id="loginBtn">\u767B\u5F55</button>
2216
3570
  </form>
2217
3571
  </div>
2218
3572
  <script>
3573
+ /**
3574
+ * \u767B\u5F55\u540E\u5E26\u7740\u539F hash \u56DE\u4E3B\u9875\u3002
3575
+ *
3576
+ * \u5F15\u5BFC\u914D\u7F6E\u8DF3\u8F6C\u5F62\u5982 /#sched=add&serverUrl=...\uFF1Ahash \u4E0D\u4F1A\u968F\u8BF7\u6C42\u53D1\u7ED9\u670D\u52A1\u7AEF\uFF0C
3577
+ * \u4F46\u670D\u52A1\u7AEF\u628A\u9996\u9875\u91CD\u5B9A\u5411\u5230 /login \u65F6\u5B83\u4ECD\u5728\u5730\u5740\u680F\u91CC\u3002\u6B64\u5904\u82E5\u8DF3 "/" \u4F1A\u628A\u5B83\u4E22\u6389\uFF0C
3578
+ * \u300C\u5E26\u4E86\u914D\u7F6E\u8FDB\u6765\u5374\u4EC0\u4E48\u90FD\u6CA1\u5E26\u5165\u300D\u3002setup \u9875\u540C\u7406\uFF08\u9996\u6B21\u767B\u5F55\u8981\u5148\u8BBE\u7F6E\u8D26\u53F7\u5BC6\u7801\uFF09\u3002
3579
+ */
3580
+ function continueWithHash(path) {
3581
+ var hash = String(window.location.hash || "");
3582
+ window.location.href = path + hash;
3583
+ }
3584
+
2219
3585
  async function doLogin(e) {
2220
3586
  e.preventDefault();
2221
3587
  const btn = document.getElementById("loginBtn");
@@ -2237,9 +3603,9 @@ async function doLogin(e) {
2237
3603
  if (resp.ok) {
2238
3604
  const data = await resp.json();
2239
3605
  if (data.needSetup) {
2240
- window.location.href = "/setup";
3606
+ continueWithHash("/setup");
2241
3607
  } else {
2242
- window.location.href = "/";
3608
+ continueWithHash("/");
2243
3609
  }
2244
3610
  return;
2245
3611
  }
@@ -2261,27 +3627,20 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
2261
3627
  if (e.key === "Enter") doLogin(e);
2262
3628
  });
2263
3629
  </script>
3630
+ ${X}
2264
3631
  </body>
2265
- </html>`,je=`<!DOCTYPE html>
3632
+ </html>`,_e=`<!DOCTYPE html>
2266
3633
  <html lang="zh-CN">
2267
3634
  <head>
2268
3635
  <meta charset="UTF-8">
2269
3636
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2270
3637
  <title>Bridge Dashboard - Setup</title>
3638
+ ${O}
3639
+ ${z}
2271
3640
  <style>
2272
- :root {
2273
- --bg: #0f1117;
2274
- --surface: #1a1d2e;
2275
- --surface2: #232738;
2276
- --border: #2d3248;
2277
- --text: #e1e4f0;
2278
- --text2: #8b90a8;
2279
- --accent: #5b8def;
2280
- --accent-hover: #4a7de0;
2281
- --green: #43c6a8;
2282
- --red: #f05454;
2283
- --radius: 10px;
2284
- }
3641
+ /* \u4E0E\u4E3B\u9762\u677F\u5171\u7528\u8BBE\u8BA1\u4EE4\u724C\uFF08--text2/--surface2/--green/--red \u4E3A\u517C\u5BB9\u522B\u540D\uFF09 */
3642
+ ${j}
3643
+
2285
3644
  * { margin:0; padding:0; box-sizing:border-box; }
2286
3645
  body {
2287
3646
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
@@ -2425,6 +3784,7 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
2425
3784
  </style>
2426
3785
  </head>
2427
3786
  <body>
3787
+ <button type="button" class="theme-fab" id="themeFab" onclick="toggleThemeFab()" title="\u5207\u6362\u4E3B\u9898">&#x263D;</button>
2428
3788
  <div class="setup-card" id="setupCard">
2429
3789
  <h1>Bridge Dashboard</h1>
2430
3790
  <p class="subtitle">First-time setup \u2014 \u9996\u6B21\u767B\u5F55\u8BBE\u7F6E</p>
@@ -2498,7 +3858,8 @@ async function doSave(e) {
2498
3858
  document.getElementById("setupForm").style.display = "none";
2499
3859
  document.querySelector(".hint").style.display = "none";
2500
3860
  document.querySelector(".badge").style.display = "none";
2501
- setTimeout(() => { window.location.href = "/"; }, 1500);
3861
+ // \u4FDD\u7559 hash\uFF1A\u5F15\u5BFC\u914D\u7F6E\u662F /#sched=add&serverUrl=... \u2192 /login \u2192 /setup \u4E00\u8DEF\u5E26\u8FC7\u6765\u7684
3862
+ setTimeout(() => { window.location.href = "/" + String(window.location.hash || ""); }, 1500);
2502
3863
  } else {
2503
3864
  errorEl.textContent = data.error || "Save failed";
2504
3865
  errorEl.classList.add("visible");
@@ -2531,7 +3892,8 @@ async function doSkip() {
2531
3892
  document.getElementById("setupForm").style.display = "none";
2532
3893
  document.querySelector(".hint").style.display = "none";
2533
3894
  document.querySelector(".badge").style.display = "none";
2534
- setTimeout(() => { window.location.href = "/"; }, 1500);
3895
+ // \u4FDD\u7559 hash\uFF1A\u5F15\u5BFC\u914D\u7F6E\u662F /#sched=add&serverUrl=... \u2192 /login \u2192 /setup \u4E00\u8DEF\u5E26\u8FC7\u6765\u7684
3896
+ setTimeout(() => { window.location.href = "/" + String(window.location.hash || ""); }, 1500);
2535
3897
  } else {
2536
3898
  errorEl.textContent = data.error || "Skip failed";
2537
3899
  errorEl.classList.add("visible");
@@ -2548,6 +3910,7 @@ async function doSkip() {
2548
3910
 
2549
3911
  document.getElementById("setupForm").addEventListener("submit", doSave);
2550
3912
  </script>
3913
+ ${X}
2551
3914
  </body>
2552
- </html>`;export{c as dashboardRouter,Ce as noteDashboardActivity};
3915
+ </html>`;export{f as dashboardRouter,Pe as noteDashboardActivity};
2553
3916