aws-runtime-bridge 1.9.172 → 1.9.179

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 (383) hide show
  1. package/CHANGELOG.md +314 -0
  2. package/README.md +61 -2
  3. package/dist/adapter/AcodeSdkAdapter.d.ts +374 -1
  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 +184 -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/bridge-temp-files.d.ts +1 -0
  70. package/dist/routes/bridge-temp-files.js +2 -0
  71. package/dist/routes/browser-listeners.d.ts +17 -0
  72. package/dist/routes/browser-sessions.d.ts +10 -0
  73. package/dist/routes/capabilities.js +1 -1
  74. package/dist/routes/dashboard-icon.d.ts +9 -0
  75. package/dist/routes/dashboard-icon.js +2 -0
  76. package/dist/routes/dashboard-theme.d.ts +24 -0
  77. package/dist/routes/dashboard-theme.js +157 -0
  78. package/dist/routes/dashboard.d.ts +9 -0
  79. package/dist/routes/dashboard.js +2021 -628
  80. package/dist/routes/debug-mount.d.ts +16 -0
  81. package/dist/routes/desktop-control.d.ts +15 -0
  82. package/dist/routes/desktop-control.js +2 -0
  83. package/dist/routes/events.d.ts +4 -0
  84. package/dist/routes/file-browser.d.ts +15 -10
  85. package/dist/routes/file-browser.js +1 -1
  86. package/dist/routes/file-change-diff.d.ts +25 -0
  87. package/dist/routes/file-diff-rollback.d.ts +28 -0
  88. package/dist/routes/file-snapshot.d.ts +7 -0
  89. package/dist/routes/git.d.ts +185 -0
  90. package/dist/routes/git.js +34 -34
  91. package/dist/routes/instance-tool-status.d.ts +5 -0
  92. package/dist/routes/instance.d.ts +8 -0
  93. package/dist/routes/instance.js +1 -1
  94. package/dist/routes/jdks.d.ts +4 -0
  95. package/dist/routes/launch.d.ts +5 -0
  96. package/dist/routes/lsp-providers.d.ts +11 -0
  97. package/dist/routes/lsp-providers.js +2 -0
  98. package/dist/routes/lsp.d.ts +41 -0
  99. package/dist/routes/lsp.js +4 -2
  100. package/dist/routes/mcp-extra.d.ts +15 -0
  101. package/dist/routes/mcp.d.ts +16 -0
  102. package/dist/routes/mcp.js +1 -1
  103. package/dist/routes/plugins.d.ts +19 -0
  104. package/dist/routes/plugins.js +1 -1
  105. package/dist/routes/processes.d.ts +4 -0
  106. package/dist/routes/properties.d.ts +4 -0
  107. package/dist/routes/pty.d.ts +29 -0
  108. package/dist/routes/runtime-binding.d.ts +3 -0
  109. package/dist/routes/sessions.d.ts +22 -0
  110. package/dist/routes/sessions.js +1 -1
  111. package/dist/routes/skills.d.ts +15 -0
  112. package/dist/routes/skills.js +1 -1
  113. package/dist/routes/sub-agents.d.ts +11 -0
  114. package/dist/routes/sub-agents.js +1 -1
  115. package/dist/routes/task-context.d.ts +5 -0
  116. package/dist/routes/tech-stack.d.ts +6 -0
  117. package/dist/routes/tech-stack.js +2 -0
  118. package/dist/routes/terminal.d.ts +122 -0
  119. package/dist/routes/terminal.js +22 -22
  120. package/dist/routes/workspace-index.d.ts +12 -0
  121. package/dist/routes/workspace-index.js +2 -0
  122. package/dist/routes/yml.d.ts +4 -0
  123. package/dist/scripts/migrate-user-scopes.d.ts +26 -0
  124. package/dist/scripts/migrate-user-scopes.js +2 -0
  125. package/dist/services/access-control.d.ts +21 -0
  126. package/dist/services/acode-bg-manager.d.ts +21 -0
  127. package/dist/services/acode-package.d.ts +10 -0
  128. package/dist/services/acode-package.js +1 -1
  129. package/dist/services/agent-capability-sync.d.ts +26 -0
  130. package/dist/services/agent-capability-sync.js +1 -1
  131. package/dist/services/agent-event-store.d.ts +20 -0
  132. package/dist/services/agent-ownership.d.ts +58 -0
  133. package/dist/services/agent-ownership.js +3 -0
  134. package/dist/services/agent-process-manager.d.ts +154 -0
  135. package/dist/services/agent-pty-manager.d.ts +39 -0
  136. package/dist/services/agent-pty-manager.js +5 -4
  137. package/dist/services/agent-startup-memory.d.ts +34 -0
  138. package/dist/services/agent-startup-memory.js +4 -4
  139. package/dist/services/ai-config-service.d.ts +26 -52
  140. package/dist/services/ai-config-service.js +1 -3
  141. package/dist/services/ai-source-apply.d.ts +7 -15
  142. package/dist/services/ai-source-apply.js +1 -1
  143. package/dist/services/auto-register.d.ts +165 -1
  144. package/dist/services/auto-register.js +1 -1
  145. package/dist/services/aws-client-agent-mcp.d.ts +14 -0
  146. package/dist/services/aws-client-agent-mcp.js +2 -2
  147. package/dist/services/bridge-package-publish-hooks.d.ts +14 -0
  148. package/dist/services/bridge-pairing.d.ts +82 -0
  149. package/dist/services/bridge-pairing.js +2 -0
  150. package/dist/services/bridge-temp-files.d.ts +65 -0
  151. package/dist/services/bridge-temp-files.js +2 -0
  152. package/dist/services/browser-session-manager.d.ts +71 -0
  153. package/dist/services/capability-config.d.ts +60 -15
  154. package/dist/services/capability-config.js +2 -2
  155. package/dist/services/change-cache-retention.d.ts +9 -0
  156. package/dist/services/changelog.d.ts +9 -0
  157. package/dist/services/cli-commands.d.ts +82 -0
  158. package/dist/services/cli-commands.js +6 -6
  159. package/dist/services/connection-key-rotation.d.ts +27 -0
  160. package/dist/services/connection-key-rotation.js +2 -0
  161. package/dist/services/debug-mount.d.ts +49 -0
  162. package/dist/services/desktop-mcp-manager.d.ts +103 -1
  163. package/dist/services/desktop-mcp-manager.js +1 -1
  164. package/dist/services/event-delta-pusher.d.ts +15 -0
  165. package/dist/services/fileSnapshotStore.d.ts +42 -0
  166. package/dist/services/fileSnapshotStore.js +1 -1
  167. package/dist/services/gitignore.d.ts +21 -0
  168. package/dist/services/gitignore.js +2 -0
  169. package/dist/services/instance-ai-config-service.d.ts +9 -0
  170. package/dist/services/instance-ai-config-service.js +1 -1
  171. package/dist/services/instance-features.d.ts +127 -0
  172. package/dist/services/instance-features.js +1 -1
  173. package/dist/services/instance-init-service.d.ts +3 -0
  174. package/dist/services/instance-init-service.js +1 -1
  175. package/dist/services/instance-state.d.ts +23 -0
  176. package/dist/services/jdk-scanner.d.ts +15 -0
  177. package/dist/services/launch-manager.d.ts +138 -0
  178. package/dist/services/launch-metrics.d.ts +42 -0
  179. package/dist/services/launch-streams.d.ts +19 -0
  180. package/dist/services/lifecycle-state.d.ts +5 -0
  181. package/dist/services/log-retention.d.ts +38 -0
  182. package/dist/services/log-retention.js +1 -1
  183. package/dist/services/lsp/install-manager.d.ts +55 -0
  184. package/dist/services/lsp/install-manager.js +2 -0
  185. package/dist/services/lsp/provider-registry.d.ts +108 -0
  186. package/dist/services/lsp/provider-registry.js +2 -0
  187. package/dist/services/lsp/uri-rewrite.d.ts +44 -0
  188. package/dist/services/lsp/uri-rewrite.js +2 -0
  189. package/dist/services/lsp-stdio-bridge.d.ts +41 -4
  190. package/dist/services/lsp-stdio-bridge.js +3 -3
  191. package/dist/services/mcp-deploy.d.ts +45 -4
  192. package/dist/services/mcp-deploy.js +4 -4
  193. package/dist/services/mcp-env-injector.d.ts +52 -0
  194. package/dist/services/mcp-launch-binding-queue.d.ts +12 -0
  195. package/dist/services/mcp-package.d.ts +22 -0
  196. package/dist/services/mcp-package.js +1 -1
  197. package/dist/services/mcp-probe-client.d.ts +19 -0
  198. package/dist/services/mcp-probe-client.js +1 -1
  199. package/dist/services/open-doc-events.d.ts +43 -0
  200. package/dist/services/open-doc-events.js +2 -0
  201. package/dist/services/open-doc-registry.d.ts +59 -0
  202. package/dist/services/open-doc-registry.js +2 -0
  203. package/dist/services/orphan-monitor.d.ts +43 -0
  204. package/dist/services/panel-auth.d.ts +24 -0
  205. package/dist/services/panel-env.d.ts +28 -0
  206. package/dist/services/plugin-deployment.d.ts +34 -1
  207. package/dist/services/plugin-deployment.js +2 -2
  208. package/dist/services/plugin-event-buffer.d.ts +21 -0
  209. package/dist/services/plugin-fs.d.ts +11 -0
  210. package/dist/services/plugin-handler-adapter.d.ts +13 -0
  211. package/dist/services/plugin-listener-runner.d.ts +10 -0
  212. package/dist/services/plugin-loader.d.ts +15 -0
  213. package/dist/services/plugin-manager.d.ts +95 -9
  214. package/dist/services/plugin-manager.js +4 -4
  215. package/dist/services/plugin-manifest.d.ts +14 -0
  216. package/dist/services/plugin-package.d.ts +13 -0
  217. package/dist/services/plugin-runtime.d.ts +15 -0
  218. package/dist/services/port-detector.d.ts +15 -0
  219. package/dist/services/process-detail.d.ts +3 -0
  220. package/dist/services/process-detector.d.ts +132 -0
  221. package/dist/services/process-registry.d.ts +120 -0
  222. package/dist/services/process-top.d.ts +6 -0
  223. package/dist/services/request-identity.d.ts +77 -0
  224. package/dist/services/request-identity.js +2 -0
  225. package/dist/services/request-invalid-dump.d.ts +4 -0
  226. package/dist/services/request-invalid-dump.js +1 -1
  227. package/dist/services/ring-buffer.d.ts +33 -0
  228. package/dist/services/runtime-binding.d.ts +56 -4
  229. package/dist/services/runtime-binding.js +4 -4
  230. package/dist/services/runtime-lifecycle-policy.d.ts +17 -0
  231. package/dist/services/runtime-resources-snapshot.d.ts +26 -0
  232. package/dist/services/runtime-resources-snapshot.js +2 -2
  233. package/dist/services/sdk-tool-file-change-recorder.d.ts +9 -0
  234. package/dist/services/session-output.d.ts +63 -0
  235. package/dist/services/session-output.js +3 -3
  236. package/dist/services/skill-manager.d.ts +42 -5
  237. package/dist/services/skill-manager.js +3 -3
  238. package/dist/services/snapshot-retention.d.ts +30 -0
  239. package/dist/services/snapshot-retention.js +2 -0
  240. package/dist/services/startup-config-wizard.d.ts +7 -1
  241. package/dist/services/startup-config-wizard.js +10 -10
  242. package/dist/services/system-metrics.d.ts +3 -0
  243. package/dist/services/tech-stack-scanner.d.ts +43 -0
  244. package/dist/services/tech-stack-scanner.js +2 -0
  245. package/dist/services/terminal-persistence.d.ts +43 -0
  246. package/dist/services/tool-installer.d.ts +12 -0
  247. package/dist/services/tunnel-client.d.ts +99 -2
  248. package/dist/services/tunnel-client.js +1 -1
  249. package/dist/services/update-log.d.ts +6 -0
  250. package/dist/services/update-notifier.d.ts +17 -0
  251. package/dist/services/update-notifier.js +2 -2
  252. package/dist/services/user-scope.d.ts +115 -0
  253. package/dist/services/user-scope.js +2 -0
  254. package/dist/services/windows-service-manager.d.ts +12 -0
  255. package/dist/services/workspace-files.d.ts +64 -0
  256. package/dist/services/workspace-index/index-manager.d.ts +46 -0
  257. package/dist/services/workspace-index/index-manager.js +2 -0
  258. package/dist/services/workspace-index/indexer.d.ts +15 -0
  259. package/dist/services/workspace-index/indexer.js +2 -0
  260. package/dist/services/workspace-index/query.d.ts +74 -0
  261. package/dist/services/workspace-index/query.js +5 -0
  262. package/dist/services/workspace-index/resolver.d.ts +26 -0
  263. package/dist/services/workspace-index/resolver.js +2 -0
  264. package/dist/services/workspace-index/types.d.ts +89 -0
  265. package/dist/services/workspace-index/types.js +2 -0
  266. package/dist/services/workspace-watch.d.ts +4 -0
  267. package/dist/services/workspaceTaskChangeWatcher.d.ts +49 -0
  268. package/dist/services/workspaceTaskChangeWatcher.js +2 -2
  269. package/dist/types.d.ts +9 -0
  270. package/dist/utils/config-write-queue.d.ts +11 -0
  271. package/dist/utils/env-safety.d.ts +21 -0
  272. package/dist/utils/file-utils.d.ts +15 -0
  273. package/dist/utils/instance-ai-config-debug-logger.d.ts +15 -0
  274. package/dist/utils/lifecycle-log.d.ts +5 -0
  275. package/dist/utils/logger.d.ts +13 -0
  276. package/dist/utils/logger.js +2 -1
  277. package/dist/utils/npm-version.d.ts +10 -0
  278. package/dist/utils/path-utils.d.ts +12 -0
  279. package/dist/utils/scheduler-url.d.ts +18 -0
  280. package/dist/utils/scheduler-url.js +2 -0
  281. package/dist/utils/sdk-package-loader.d.ts +13 -0
  282. package/dist/utils/sdk-package-loader.js +1 -1
  283. package/dist/utils/target-identity.d.ts +31 -0
  284. package/dist/utils/target-identity.js +2 -0
  285. package/dist/utils/validation.d.ts +17 -0
  286. package/dist/utils/yaml-utils.d.ts +48 -0
  287. package/package/acode/dist/__tests__/summary-request-helper.d.ts +12 -0
  288. package/package/acode/dist/__tests__/summary-request-helper.js +2 -0
  289. package/package/acode/dist/ask-user-question-tool.d.ts +17 -0
  290. package/package/acode/dist/await-complete-tool.d.ts +12 -0
  291. package/package/acode/dist/background-command-manager.d.ts +105 -0
  292. package/package/acode/dist/background-command-store.d.ts +27 -0
  293. package/package/acode/dist/background-command-tools.d.ts +8 -0
  294. package/package/acode/dist/background-output-ringbuffer.d.ts +15 -0
  295. package/package/acode/dist/built-in-file-tools.d.ts +65 -0
  296. package/package/acode/dist/built-in-symbols-tool.d.ts +3 -0
  297. package/package/acode/dist/builtins/commandArtifacts.d.ts +3 -0
  298. package/package/acode/dist/builtins/shellCommandPreparation.d.ts +17 -0
  299. package/package/acode/dist/builtins/shellCommandRunner.d.ts +4 -0
  300. package/package/acode/dist/builtins/shellOutputDecoder.d.ts +15 -0
  301. package/package/acode/dist/cache-control.d.ts +71 -0
  302. package/package/acode/dist/cache-control.js +2 -0
  303. package/package/acode/dist/commands/commandLoader.d.ts +3 -0
  304. package/package/acode/dist/commands/commandParser.d.ts +3 -0
  305. package/package/acode/dist/commands/commandRegistry.d.ts +3 -0
  306. package/package/acode/dist/compaction.d.ts +388 -4
  307. package/package/acode/dist/compaction.js +19 -17
  308. package/package/acode/dist/config.d.ts +3 -0
  309. package/package/acode/dist/context-overflow.d.ts +32 -0
  310. package/package/acode/dist/fs-utils.d.ts +18 -0
  311. package/package/acode/dist/hooks/hookBus.d.ts +3 -0
  312. package/package/acode/dist/image-utils.d.ts +35 -0
  313. package/package/acode/dist/index.d.ts +1 -0
  314. package/package/acode/dist/index.js +1 -1
  315. package/package/acode/dist/mcpToolNames.d.ts +32 -0
  316. package/package/acode/dist/microcompact.d.ts +110 -0
  317. package/package/acode/dist/microcompact.js +2 -0
  318. package/package/acode/dist/parsers/ast-cache.d.ts +41 -0
  319. package/package/acode/dist/parsers/file-index.d.ts +83 -0
  320. package/package/acode/dist/parsers/file-index.js +4 -0
  321. package/package/acode/dist/parsers/language-detector.d.ts +4 -0
  322. package/package/acode/dist/parsers/tree-sitter-loader.d.ts +12 -0
  323. package/package/acode/dist/parsers/types.d.ts +11 -0
  324. package/package/acode/dist/paths.d.ts +14 -0
  325. package/package/acode/dist/plugin-types.d.ts +34 -0
  326. package/package/acode/dist/read-file-state.d.ts +15 -0
  327. package/package/acode/dist/runtime-environment.d.ts +16 -0
  328. package/package/acode/dist/runtime.d.ts +828 -9
  329. package/package/acode/dist/runtime.js +40 -33
  330. package/package/acode/dist/skill-tool.d.ts +20 -0
  331. package/package/acode/dist/sub-agent-manager.d.ts +169 -0
  332. package/package/acode/dist/sub-agent-manager.js +1 -1
  333. package/package/acode/dist/sub-agent-store.d.ts +50 -0
  334. package/package/acode/dist/sub-agent-tools.d.ts +15 -0
  335. package/package/acode/dist/sub-agent-types.d.ts +13 -0
  336. package/package/acode/dist/terminal-tool.d.ts +15 -0
  337. package/package/acode/dist/terminal-types.d.ts +10 -0
  338. package/package/acode/dist/todo-store.d.ts +15 -0
  339. package/package/acode/dist/token-estimator.d.ts +32 -0
  340. package/package/acode/dist/token-estimator.js +2 -0
  341. package/package/acode/dist/tool-allocation.d.ts +113 -0
  342. package/package/acode/dist/tool-concurrency/index.d.ts +8 -0
  343. package/package/acode/dist/tool-concurrency/observability.d.ts +32 -0
  344. package/package/acode/dist/tool-concurrency/resource-lock.d.ts +30 -0
  345. package/package/acode/dist/tool-concurrency/scheduler.d.ts +19 -0
  346. package/package/acode/dist/tool-concurrency/types.d.ts +57 -0
  347. package/package/acode/dist/tool-group-manager.d.ts +195 -0
  348. package/package/acode/dist/tool-group-manager.js +4 -4
  349. package/package/acode/dist/types.d.ts +192 -0
  350. package/package/aws-client-agent-mcp/README.md +130 -215
  351. package/package/aws-client-agent-mcp/dist/ability-market-tools.d.ts +57 -0
  352. package/package/aws-client-agent-mcp/dist/ability-market-tools.js +1 -1
  353. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +290 -3
  354. package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
  355. package/package/aws-client-agent-mcp/dist/config.d.ts +18 -0
  356. package/package/aws-client-agent-mcp/dist/constants.d.ts +2 -0
  357. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +70 -0
  358. package/package/aws-client-agent-mcp/dist/http-client.d.ts +61 -0
  359. package/package/aws-client-agent-mcp/dist/index.d.ts +10 -0
  360. package/package/aws-client-agent-mcp/dist/logger.d.ts +24 -0
  361. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +610 -0
  362. package/package/aws-client-agent-mcp/dist/mcp-server.js +25 -23
  363. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +158 -0
  364. package/package/aws-client-agent-mcp/dist/mcp-tools.js +38 -22
  365. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +102 -2
  366. package/package/aws-client-agent-mcp/dist/memory-store.js +27 -2
  367. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +18 -0
  368. package/package/aws-client-agent-mcp/dist/memory-tools.js +13 -9
  369. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +86 -0
  370. package/package/aws-client-agent-mcp/dist/messageContent.d.ts +24 -0
  371. package/package/aws-client-agent-mcp/dist/office/office-equation.d.ts +19 -0
  372. package/package/aws-client-agent-mcp/dist/office/office-image.d.ts +18 -0
  373. package/package/aws-client-agent-mcp/dist/office/office-workspace.d.ts +29 -0
  374. package/package/aws-client-agent-mcp/dist/parent-lifecycle.d.ts +18 -0
  375. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts +6 -0
  376. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +5 -1
  377. package/package/aws-client-agent-mcp/dist/server-memory-store.js +1 -1
  378. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +41 -0
  379. package/package/aws-client-agent-mcp/dist/tool-groups.d.ts +136 -0
  380. package/package/aws-client-agent-mcp/dist/types.d.ts +174 -0
  381. package/package/aws-client-agent-mcp/dist/watchdog.d.ts +7 -0
  382. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +108 -0
  383. 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;
82
+ }
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;
58
91
  }
59
- .header-right {
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;
107
230
  }
108
231
 
109
- .stat-row {
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; }
247
+ }
248
+
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);
133
382
  }
134
- .section-title .count {
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);
443
+ }
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;
1316
+ }
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;
578
1353
  }
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;
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;
583
1359
  }
584
- .settings-head {
585
- display: flex; align-items: center; justify-content: space-between;
586
- padding: 16px 20px; border-bottom: 1px solid var(--border);
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",
@@ -1035,11 +1921,22 @@ function t(k) {
1035
1921
  return ((_a = T[lang]) === null || _a === void 0 ? void 0 : _a[k]) || T.zh[k] || k;
1036
1922
  }
1037
1923
 
1924
+ // \u6309\u9762\u677F\u5F53\u524D\u8BED\u8A00\u683C\u5F0F\u5316\u65F6\u95F4\u3002
1925
+ // \u4E0D\u80FD\u76F4\u63A5\u7528 toLocaleString()\uFF08\u4E0D\u4F20\u53C2\u6570\uFF09\uFF1A\u90A3\u4F1A\u53D6\u6D4F\u89C8\u5668/OS \u7684\u8BED\u8A00\uFF0C\u4E0E\u9762\u677F\u81EA\u5DF1\u7684
1926
+ // \u4E2D\u82F1\u5207\u6362\u6309\u94AE\uFF08lang\uFF09\u8131\u8282\u2014\u2014\u7528\u6237\u5207\u5230 English \u540E\uFF0C\u65E5\u671F\u4ECD\u6309\u7CFB\u7EDF\u8BED\u8A00\u6E32\u67D3\uFF0C\u4E2D\u82F1\u5939\u6742\u3002
1927
+ function fmtTime(value) {
1928
+ var _a;
1929
+ var date = value ? new Date(value) : null;
1930
+ if (!date || isNaN(date.getTime())) return "-";
1931
+ return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", { hour12: false });
1932
+ }
1933
+
1038
1934
  function toggleLang() {
1039
1935
  lang = lang === "zh" ? "en" : "zh";
1040
1936
  localStorage.setItem("dashboard-lang", lang);
1041
1937
  document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
1042
1938
  document.getElementById("langBtn").textContent = t("langName");
1939
+ updateThemeBtn();
1043
1940
  if (document.getElementById("settingsModal").style.display === "flex") {
1044
1941
  renderSettingsUI();
1045
1942
  settingsLang = lang;
@@ -1047,6 +1944,30 @@ function toggleLang() {
1047
1944
  if (lastData) render(lastData);
1048
1945
  }
1049
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
+
1050
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
1051
1972
  let lastData = null;
1052
1973
 
@@ -1082,7 +2003,20 @@ function escHtml(s) {
1082
2003
  }
1083
2004
  function fmtShortDate(iso) {
1084
2005
  if (!iso) return "-";
1085
- return iso.slice(0, 19).replace("T", " ");
2006
+ // \u4E0D\u80FD\u76F4\u63A5\u5207\u5B57\u7B26\u4E32\uFF1A\u90A3\u6837\u663E\u793A\u7684\u662F UTC \u6302\u949F\u503C\uFF08\u5982 2026-08-18 04:35:45\uFF09\uFF0C
2007
+ // \u4E0E\u6D4F\u89C8\u5668\u6240\u5728\u65F6\u533A\u7684\u672C\u5730\u65F6\u95F4\u76F8\u5DEE\u4E00\u4E2A\u504F\u79FB\u3002\u8FD9\u91CC\u8F6C\u6210 Date \u518D\u6309\u672C\u5730\u65F6\u533A\u6E32\u67D3\uFF0C
2008
+ // \u8BA9\u9762\u677F\u663E\u793A\u7684\u523B\u4E0E\u7528\u6237\u5B9E\u9645\u611F\u77E5\u7684\u4E00\u81F4\u3002
2009
+ var date = new Date(iso);
2010
+ if (isNaN(date.getTime())) return String(iso).slice(0, 19).replace("T", " ");
2011
+ return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
2012
+ year: "numeric",
2013
+ month: "2-digit",
2014
+ day: "2-digit",
2015
+ hour: "2-digit",
2016
+ minute: "2-digit",
2017
+ second: "2-digit",
2018
+ hour12: false,
2019
+ });
1086
2020
  }
1087
2021
  function fmtShortPath(p) {
1088
2022
  if (!p) return "-";
@@ -1118,105 +2052,237 @@ function barColor(pct) {
1118
2052
  }
1119
2053
 
1120
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
+
1121
2182
  function render(data) {
1122
2183
  lastData = data;
1123
- document.getElementById("refreshTime").textContent = new Date(data.collectedAt).toLocaleString();
2184
+ document.getElementById("refreshTime").textContent = fmtTime(data.collectedAt);
1124
2185
  document.getElementById("bridgeHostname").textContent = data.bridge?.hostname || "-";
1125
2186
  document.getElementById("bridgeVersion").textContent = "v" + (data.bridge?.version || "-");
1126
2187
  document.getElementById("langBtn").textContent = t("langName");
1127
- document.getElementById("refreshBtn").textContent = t("refreshBtn");
2188
+ document.getElementById("pageTitle").textContent = t("pageTitle");
2189
+ document.getElementById("pageSub").textContent = t("pageSub");
1128
2190
 
1129
2191
  // Section titles
1130
- document.getElementById("sectionSysRes").firstChild.textContent = t("secSysRes");
1131
- document.getElementById("sectionSessions").firstChild.textContent = t("secSessions");
2192
+ document.getElementById("sectionSysRes").textContent = t("secSysRes");
2193
+ document.getElementById("sectionSessions").textContent = t("secSessions");
1132
2194
  document.getElementById("unknownLabel").textContent = " " + t("secUnknown");
1133
- document.getElementById("sectionOrphans").firstChild.textContent = t("secOrphans");
1134
- document.getElementById("sectionPersisted").firstChild.textContent = t("secPersisted");
2195
+ document.getElementById("sectionOrphans").textContent = t("secOrphans");
2196
+ document.getElementById("sectionPersisted").textContent = t("secPersisted");
1135
2197
  document.getElementById("orphanBannerText").textContent = t("orphanBanner");
1136
2198
  if (document.getElementById("unknownTable").style.display !== "none") {
1137
2199
  document.getElementById("unknownIcon").classList.add("open");
1138
2200
  }
1139
2201
 
1140
- // Status cards
1141
- const b = data.binding || {};
1142
- const r = data.registration || {};
1143
- const cards = [
1144
- { title: t("cardBridge"), items: [
1145
- [t("fldVersion"), data.bridge?.version || "-"],
1146
- [t("fldHost"), data.bridge?.hostname || "-"],
1147
- [t("fldPlatform"), (data.bridge?.platform || "") + " " + (data.bridge?.arch || "")],
1148
- [t("fldUptime"), fmtDuration(data.bridge?.uptime)],
1149
- [t("fldNode"), data.bridge?.nodeVersion || "-"],
1150
- ]},
1151
- { title: t("cardRegistration"), items: [
1152
- [t("fldStatus"), '<span class="' + statusClass(r.registered) + '">' + (r.registered ? t("stRegistered") : t("stNotRegistered")) + "</span>"],
1153
- [t("fldInstanceId"), r.instanceId || "-"],
1154
- [t("fldLastAttempt"), r.lastAttempt ? new Date(r.lastAttempt).toLocaleString() : "-"],
1155
- [t("fldError"), r.error ? '<span class="status-red">' + r.error + "</span>" : t("stNone")],
1156
- ]},
1157
- { title: t("cardBinding"), items: [
1158
- [t("fldStatus"), '<span class="' + statusClass(b.paired) + '">' + (b.paired ? t("stPaired") : t("stUnpaired")) + "</span>"],
1159
- [t("fldInstanceId"), b.instanceId || "-"],
1160
- [t("fldScheduler"), b.schedulerBaseUrl || "-"],
1161
- [t("fldPairingCode"), b.pairingCode || "-"],
1162
- ]},
1163
- { title: t("cardSessions"), items: [
1164
- [t("fldActiveSessions"), '<span class="' + ((data.sessions?.activeCount ?? 0) > 0 ? "status-green" : "status-muted") + '">' + (data.sessions?.activeCount ?? 0) + "</span>"],
1165
- [t("fldPersistedSessions"), (data.sessions?.persistedCount ?? 0)],
1166
- [t("fldManagedProcesses"), data.processes?.managed ?? 0],
1167
- [t("fldOrphanProcesses"), '<span class="' + ((data.processes?.orphan ?? 0) > 0 ? "status-orange" : "status-muted") + '">' + (data.processes?.orphan ?? 0) + "</span>"],
1168
- [t("fldRegistryEntries"), data.processes?.registryCount ?? 0],
1169
- ]},
1170
- ];
1171
-
1172
- document.getElementById("statusCards").innerHTML = cards.map(c => {
1173
- const rows = c.items.map(([k, v]) =>
1174
- '<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
1175
- ).join("");
1176
- return '<div class="card"><div class="card-title">' + c.title + '</div>' + rows + '</div>';
1177
- }).join("");
1178
-
1179
- // 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
1180
2207
  const m = data.metrics;
1181
2208
  const metricsGrid = document.getElementById("metricsGrid");
2209
+ const metricsCard = document.getElementById("metricsCard");
1182
2210
  if (m && m.ok) {
1183
2211
  const cpuPct = m.cpu?.usagePercent ?? 0;
1184
2212
  const memPct = m.memory?.usagePercent ?? 0;
1185
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");
1186
2217
  metricsGrid.innerHTML = [
1187
- { title: "CPU", usage: cpuPct, lines: [
1188
- [t("fldUsage"), fmtPercent(cpuPct)],
1189
- [t("fldCores"), m.cpu?.coreCount ?? "-"],
1190
- [t("fldModel"), (m.cpu?.model || "-").substring(0, 40)],
1191
- ]},
1192
- { title: "Memory", usage: memPct, lines: [
1193
- [t("fldUsage"), fmtPercent(memPct)],
1194
- [t("fldUsed"), fmtBytes(m.memory?.usedBytes)],
1195
- [t("fldFree"), fmtBytes(m.memory?.freeBytes)],
1196
- [t("fldTotal"), fmtBytes(m.memory?.totalBytes)],
1197
- ]},
1198
- { title: "Storage (" + (m.storage?.path || "") + ")", usage: storagePct, lines: [
1199
- [t("fldUsage"), fmtPercent(storagePct)],
1200
- [t("fldFree"), m.storage ? m.storage.freeGiB + " GiB" : "-"],
1201
- [t("fldTotal"), m.storage ? m.storage.totalGiB + " GiB" : "-"],
1202
- ]},
1203
- { title: "Network", usage: null, lines: [
1204
- [t("fldDownload"), fmtBytes(m.network?.downloadBytesPerSecond) + "/s"],
1205
- [t("fldUpload"), fmtBytes(m.network?.uploadBytesPerSecond) + "/s"],
1206
- [t("fldTotalDown"), fmtBytes(m.network?.downloadTotalBytes)],
1207
- [t("fldTotalUp"), fmtBytes(m.network?.uploadTotalBytes)],
1208
- ]},
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
+ },
1209
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
1210
2262
  const barHtml = c.usage != null
1211
- ? '<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>'
1212
- : "";
1213
- const rows = c.lines.map(([k, v]) =>
1214
- '<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>'
1215
2267
  ).join("");
1216
- 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>';
1217
2279
  }).join("");
1218
2280
  } else {
1219
- 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>';
1220
2286
  }
1221
2287
 
1222
2288
  // Build process lookup maps from process list
@@ -1239,20 +2305,24 @@ function render(data) {
1239
2305
 
1240
2306
  // Active sessions (primary table, with PID from process cross-reference)
1241
2307
  const sessions = data.sessions?.active || [];
1242
- document.getElementById("sessionCount").textContent = "(" + sessions.length + ")";
2308
+ document.getElementById("sessionCount").textContent = sessions.length ? "(" + sessions.length + ")" : "";
1243
2309
  if (sessions.length === 0) {
1244
- 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>';
1245
2313
  } else {
1246
2314
  document.getElementById("sessionTable").innerHTML =
1247
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>' +
1248
2316
  sessions.map(s => {
1249
2317
  const pid = pidByAgentId[s.agentId];
1250
2318
  const st = s.runtimeStatus || "running";
1251
- return '<tr><td>' + s.agentId + '</td><td>' + (pid ? '<code>' + pid + '</code>' : '<span class="status-muted">-</span>') + '</td>'
1252
- + '<td><span class="' + statusClass(st) + '">' + (t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
1253
- + '<td>' + (s.runtimeActionLabel || '-') + '</td>'
1254
- + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-") + '</td>'
1255
- + '<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>';
1256
2326
  }).join("") +
1257
2327
  '</tbody></table>';
1258
2328
  }
@@ -1265,7 +2335,8 @@ function render(data) {
1265
2335
  document.getElementById("orphanTable").innerHTML =
1266
2336
  '<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thPid") + '</th></tr></thead><tbody>' +
1267
2337
  orphanProcs.map(p =>
1268
- '<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>'
1269
2340
  ).join("") +
1270
2341
  '</tbody></table>';
1271
2342
  } else {
@@ -1273,33 +2344,42 @@ function render(data) {
1273
2344
  }
1274
2345
 
1275
2346
  // Unknown processes (collapsible, hidden by default)
1276
- document.getElementById("unknownCount").textContent = "(" + unknownProcs.length + ")";
2347
+ document.getElementById("unknownCount").textContent = unknownProcs.length ? "(" + unknownProcs.length + ")" : "";
1277
2348
  const unknownTable = document.getElementById("unknownTable");
1278
2349
  if (unknownProcs.length === 0) {
1279
- 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>';
1280
2352
  } else {
1281
2353
  unknownTable.innerHTML =
1282
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>' +
1283
2355
  unknownProcs.map(p =>
1284
- '<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>'
1285
2361
  ).join("") +
1286
2362
  '</tbody></table>';
1287
2363
  }
1288
2364
 
1289
2365
  // Persisted sessions
1290
2366
  const persisted = data.sessions?.persisted || [];
1291
- document.getElementById("persistedCount").textContent = "(" + persisted.length + ")";
2367
+ document.getElementById("persistedCount").textContent = persisted.length ? "(" + persisted.length + ")" : "";
1292
2368
  if (persisted.length === 0) {
1293
- 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>';
1294
2371
  } else {
1295
2372
  document.getElementById("persistedTable").innerHTML =
1296
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>' +
1297
- persisted.map(s =>
1298
- '<tr><td>' + s.agentId + '</td><td>' + (s.status || "stopped") + '</td>'
1299
- + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.replace(/\\\\/g, "/").split("/").pop() : "-") + '</td>'
1300
- + '<td>' + (s.command || "-") + '</td>'
1301
- + '<td>' + (s.startedAt ? new Date(s.startedAt).toLocaleString() : "-") + '</td></tr>'
1302
- ).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("") +
1303
2383
  '</tbody></table>';
1304
2384
  }
1305
2385
  }
@@ -1345,12 +2425,16 @@ const UPDATE_LABELS = {
1345
2425
  label_installing: "\u5B89\u88C5\u4E2D...",
1346
2426
  label_restart: "\u91CD\u542F",
1347
2427
  label_failed: "\u66F4\u65B0\u5931\u8D25",
2428
+ confirm_title: "\u786E\u8BA4\u66F4\u65B0\u5E76\u91CD\u542F Bridge",
2429
+ confirm_message: "\u66F4\u65B0\u540E\u5C06\u91CD\u65B0\u542F\u52A8\u5B9E\u4F8B Bridge\uFF0C\u6B63\u5728\u8FD0\u884C\u7684 agent \u5B9E\u4F8B\u4F1A\u505C\u6B62\uFF0C\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u4E2D\u65AD\u3002\u786E\u8BA4\u73B0\u5728\u66F4\u65B0\uFF1F",
1348
2430
  },
1349
2431
  en: {
1350
2432
  label_available: "\u2192 {v}",
1351
2433
  label_installing: "installing...",
1352
2434
  label_restart: "restart",
1353
2435
  label_failed: "failed",
2436
+ confirm_title: "Confirm update and restart Bridge",
2437
+ confirm_message: "The update will restart the Bridge. Running agent instances will stop and in-progress tasks will be interrupted. Update now?",
1354
2438
  },
1355
2439
  };
1356
2440
 
@@ -1389,6 +2473,9 @@ function showUpdateAvailable(data) {
1389
2473
 
1390
2474
  async function doInstallUpdate() {
1391
2475
  const indicator = document.getElementById("updateIndicator");
2476
+ if (!confirm(ul("confirm_title") + "\\n\\n" + ul("confirm_message"))) {
2477
+ return;
2478
+ }
1392
2479
  updateState.installing = true;
1393
2480
  indicator.className = "update-indicator installing";
1394
2481
  indicator.textContent = ul("label_installing");
@@ -1473,6 +2560,7 @@ function renderSettingsUI() {
1473
2560
  document.getElementById("descRestart").textContent = t("setRestartDesc");
1474
2561
  document.getElementById("btnRestartBridge").textContent = t("setRestartBridge");
1475
2562
  document.getElementById("navNode").textContent = t("nodeTitle");
2563
+ document.getElementById("btnRefreshNodeInfo").textContent = t("nodeRefresh");
1476
2564
  document.getElementById("navLogs").textContent = t("logTitle");
1477
2565
  document.getElementById("navScheduler").textContent = t("schedTitle");
1478
2566
  document.getElementById("lblNodeHostname").textContent = t("nodeHostname");
@@ -1511,10 +2599,10 @@ function renderSettingsUI() {
1511
2599
  document.getElementById("optTunnelOff").textContent = t("schedTunnelOff");
1512
2600
  document.getElementById("lblSchedInstanceId").textContent = t("schedInstanceId");
1513
2601
  document.getElementById("hintSchedInstanceId").textContent = t("schedInstanceIdHint");
2602
+ document.getElementById("lblSchedAdvanced").textContent = t("schedAdvanced");
2603
+ updateSchedAdvancedBadge();
1514
2604
  document.getElementById("btnSaveServerConfig").textContent = t("schedSave");
1515
- document.getElementById("btnSaveRestart").textContent = t("schedSaveAndRestart");
1516
2605
  document.getElementById("descSched").textContent = t("schedDesc");
1517
- document.getElementById("btnReRegister").textContent = t("schedReRegister");
1518
2606
  document.getElementById("navAccess").textContent = t("accTitle");
1519
2607
  document.getElementById("lblAccessIps").textContent = t("accAllowIps");
1520
2608
  document.getElementById("hintAccessIps").textContent = t("accAllowIpsHint");
@@ -1626,6 +2714,8 @@ function confirmRestartBridge() {
1626
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
1627
2715
  let nodeInfoTimer = null;
1628
2716
  function refreshNodeInfo() {
2717
+ var btn = document.getElementById("btnRefreshNodeInfo");
2718
+ if (btn) { btn.disabled = true; btn.textContent = t("logRefreshing"); }
1629
2719
  fetch("/runtime/node-info").then(function(r) { return r.json(); }).then(function(d) {
1630
2720
  if (!d.ok) return;
1631
2721
  document.getElementById("valNodeHostname").textContent = d.hostname;
@@ -1640,7 +2730,12 @@ function refreshNodeInfo() {
1640
2730
  document.getElementById("valNodeRegistered").textContent = d.registered ? t("nodeRegYes") : t("nodeRegNo");
1641
2731
  document.getElementById("valNodeInstanceId").textContent = d.instanceId || "-";
1642
2732
  document.getElementById("valNodeActiveSessions").textContent = d.activeSessions;
1643
- }).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
+ });
1644
2739
  }
1645
2740
 
1646
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
@@ -1660,7 +2755,7 @@ function refreshLogs() {
1660
2755
  var html = "";
1661
2756
  for (var i = 0; i < d.logs.length; i++) {
1662
2757
  var e = d.logs[i];
1663
- html += '<div class="s-log-entry"><span class="ts">' + escHtml(e.timestamp.slice(0, 19).replace("T", " ")) + '</span> <span class="ll ' + e.level.toUpperCase() + '">[' + e.level.toUpperCase() + ']</span>';
2758
+ html += '<div class="s-log-entry"><span class="ts">' + escHtml(fmtShortDate(e.timestamp)) + '</span> <span class="ll ' + e.level.toUpperCase() + '">[' + e.level.toUpperCase() + ']</span>';
1664
2759
  if (e.prefix) html += ' [' + escHtml(e.prefix) + ']';
1665
2760
  html += ' ' + escHtml(e.message) + '</div>';
1666
2761
  }
@@ -1716,8 +2811,11 @@ function renderSchedTargetList() {
1716
2811
  var meta = document.createElement("div");
1717
2812
  meta.className = "s-target-meta";
1718
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);
1719
2816
  if (item.instanceName) parts.push(item.instanceName);
1720
2817
  if (item.instanceId) parts.push("ID: " + item.instanceId);
2818
+ if (item.tenantId) parts.push(t("schedTenantShort") + ": " + item.tenantId);
1721
2819
  if (item.tunnel === true) parts.push("tunnel");
1722
2820
  else if (item.tunnel === false && item.registerAddr) parts.push("direct");
1723
2821
  meta.textContent = parts.join(" \xB7 ");
@@ -1725,6 +2823,18 @@ function renderSchedTargetList() {
1725
2823
  main.appendChild(meta);
1726
2824
  var actions = document.createElement("div");
1727
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
+ }
1728
2838
  var editBtn = document.createElement("button");
1729
2839
  editBtn.className = "s-btn s-btn-sm s-btn-secondary";
1730
2840
  editBtn.textContent = t("schedEditTarget");
@@ -1738,6 +2848,60 @@ function renderSchedTargetList() {
1738
2848
  }
1739
2849
  }
1740
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
+
1741
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
1742
2906
  function loadTargetToForm(idx) {
1743
2907
  schedEditIndex = idx;
@@ -1756,9 +2920,66 @@ function loadTargetToForm(idx) {
1756
2920
  ck.placeholder = item.connectionKeyMasked || "";
1757
2921
  document.getElementById("valSchedInstanceId").textContent = item.instanceId || "-";
1758
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();
1759
2926
  renderSchedTargetList();
1760
2927
  }
1761
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
+
1762
2983
  // \u8FDB\u5165\u65B0\u589E\u6A21\u5F0F\uFF1A\u6E05\u7A7A\u8868\u5355
1763
2984
  function startAddTarget() {
1764
2985
  schedEditIndex = -1;
@@ -1770,10 +2991,76 @@ function startAddTarget() {
1770
2991
  document.getElementById("cfgTunnel").value = "auto";
1771
2992
  document.getElementById("valSchedInstanceId").textContent = "-";
1772
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();
1773
2997
  hideSchedMsg();
2998
+ applyPrefillIfPending();
1774
2999
  renderSchedTargetList();
1775
3000
  }
1776
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
+
1777
3064
  function hideSchedMsg() {
1778
3065
  var msg = document.getElementById("schedMsg");
1779
3066
  msg.className = "s-msg";
@@ -1786,14 +3073,16 @@ function showSchedMsg(text, ok) {
1786
3073
  msg.className = "s-msg " + (ok ? "success" : "error");
1787
3074
  }
1788
3075
 
1789
- // \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
1790
3077
  function formToTarget() {
3078
+ var editing = schedEditIndex >= 0 ? schedTargets[schedEditIndex] : null;
1791
3079
  return {
1792
3080
  serverUrl: document.getElementById("cfgServerUrl").value.trim(),
1793
- // \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
1794
- originalServerUrl: schedEditIndex >= 0 && schedTargets[schedEditIndex]
1795
- ? (schedTargets[schedEditIndex].serverUrl || "")
1796
- : "",
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 || "") : "",
1797
3086
  userKey: document.getElementById("cfgUserKey").value,
1798
3087
  tenantId: document.getElementById("cfgTenantId").value.trim(),
1799
3088
  instanceName: document.getElementById("cfgInstanceName").value.trim(),
@@ -1814,6 +3103,40 @@ function refreshServerConfig() {
1814
3103
  }).catch(function() {});
1815
3104
  }
1816
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
+
1817
3140
  // \u5168\u91CF\u63D0\u4EA4 targets\uFF1BrestartAfter \u4E3A true \u65F6\u4FDD\u5B58\u6210\u529F\u540E\u91CD\u542F Bridge
1818
3141
  function saveServerConfig(restartAfter) {
1819
3142
  var form = formToTarget();
@@ -1822,6 +3145,17 @@ function saveServerConfig(restartAfter) {
1822
3145
  return Promise.resolve(false);
1823
3146
  }
1824
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
+
1825
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
1826
3160
  var list = schedTargets.map(function(item) {
1827
3161
  return {
@@ -1853,23 +3187,15 @@ function saveServerConfig(restartAfter) {
1853
3187
  refreshServerConfig();
1854
3188
  schedEditIndex = keepIndex;
1855
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
1856
3191
  showSchedMsg(t("schedSaved"), true);
1857
3192
  doRestartBridge();
1858
3193
  return true;
1859
3194
  }
1860
- // \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
1861
- // \u65E0\u9700\u91CD\u542F Bridge\uFF1B\u5B9E\u4F8B\u4FE1\u606F\u6309 instanceId \u5728\u670D\u52A1\u7AEF\u590D\u7528\uFF09
1862
- showSchedMsg(t("schedSavedRegistering"), true);
1863
- btn.textContent = t("schedReRegistering");
1864
- return runReRegister().then(function(reg) {
1865
- if (reg.ok) {
1866
- showSchedMsg(t("schedReRegistered"), true);
1867
- refreshNodeInfo();
1868
- } else {
1869
- showSchedMsg(t("schedReRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
1870
- }
1871
- return reg.ok;
1872
- });
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;
1873
3199
  }
1874
3200
  showSchedMsg(t("schedSaveFail") + (d.error ? ": " + d.error : ""), false);
1875
3201
  return false;
@@ -1885,10 +3211,64 @@ function saveServerConfig(restartAfter) {
1885
3211
  });
1886
3212
  }
1887
3213
 
1888
- // \u4FDD\u5B58\u5E76\u91CD\u542F\uFF1A\u5148\u786E\u8BA4\uFF0C\u4FDD\u5B58\u6210\u529F\u540E\u7531 saveServerConfig \u89E6\u53D1\u91CD\u542F
1889
- function saveServerConfigAndRestart() {
1890
- if (!confirm(t("schedSaveRestartConfirm"))) return;
1891
- 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);
1892
3272
  }
1893
3273
 
1894
3274
  // \u5220\u9664\u5F53\u524D\u7F16\u8F91\u7684 target\uFF1A\u4ECE\u5217\u8868\u79FB\u9664\u540E\u5168\u91CF\u63D0\u4EA4
@@ -1942,37 +3322,13 @@ function deleteTargetFromList() {
1942
3322
  });
1943
3323
  }
1944
3324
 
1945
- // \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
1946
3326
  function runReRegister() {
1947
3327
  return fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1948
3328
  .then(function(r) { return r.json(); })
1949
3329
  .catch(function() { return { ok: false }; });
1950
3330
  }
1951
3331
 
1952
- function doReRegister() {
1953
- var btn = document.getElementById("btnReRegister");
1954
- var msg = document.getElementById("schedMsg");
1955
- msg.className = "s-msg";
1956
- msg.style.display = "none";
1957
- btn.disabled = true;
1958
- btn.textContent = t("schedReRegistering");
1959
- runReRegister()
1960
- .then(function(d) {
1961
- if (d.ok) {
1962
- msg.textContent = t("schedReRegistered");
1963
- msg.className = "s-msg success";
1964
- refreshNodeInfo();
1965
- } else {
1966
- msg.textContent = t("schedReRegisterFail") + (d.error ? ": " + d.error : "");
1967
- msg.className = "s-msg error";
1968
- }
1969
- })
1970
- .then(function() {
1971
- btn.disabled = false;
1972
- btn.textContent = t("schedReRegister");
1973
- });
1974
- }
1975
-
1976
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
1977
3333
  // \u52A0\u8F7D\u5F53\u524D IP \u767D\u540D\u5355\u5230\u8868\u5355\uFF08\u7A7A\u5217\u8868 = \u9ED8\u8BA4\u5168\u653E\u884C\uFF09
1978
3334
  function refreshAccessControl() {
@@ -2059,109 +3415,132 @@ switchSettingsPage = function(page) {
2059
3415
  else if (page === "logs") { refreshLogs(); startLogAutoRefresh(); }
2060
3416
  else if (page === "scheduler") { refreshNodeInfo(); refreshServerConfig(); }
2061
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
+ }
2062
3426
  </script>
2063
3427
  </body>
2064
- </html>`,Me=`<!DOCTYPE html>
3428
+ </html>`,Fe=`<!DOCTYPE html>
2065
3429
  <html lang="zh-CN">
2066
3430
  <head>
2067
3431
  <meta charset="UTF-8">
2068
3432
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2069
3433
  <title>Bridge Dashboard - Login</title>
3434
+ ${O}
3435
+ ${z}
2070
3436
  <style>
2071
- :root {
2072
- --bg: #0f1117;
2073
- --surface: #1a1d2e;
2074
- --surface2: #232738;
2075
- --border: #2d3248;
2076
- --text: #e1e4f0;
2077
- --text2: #8b90a8;
2078
- --accent: #5b8def;
2079
- --accent-hover: #4a7de0;
2080
- --red: #f05454;
2081
- --radius: 10px;
2082
- }
2083
- * { 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
+
2084
3442
  body {
2085
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
3443
+ font-family: var(--font-sans);
2086
3444
  background: var(--bg);
2087
3445
  color: var(--text);
2088
3446
  min-height: 100vh;
2089
3447
  display: flex;
2090
3448
  align-items: center;
2091
3449
  justify-content: center;
3450
+ padding: 20px;
3451
+ -webkit-font-smoothing: antialiased;
2092
3452
  }
3453
+
2093
3454
  .login-card {
2094
3455
  background: var(--surface);
2095
3456
  border: 1px solid var(--border);
2096
- border-radius: var(--radius);
2097
- padding: 40px 36px;
2098
- width: 360px;
2099
- 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;
2100
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
+
2101
3480
  .login-card h1 {
2102
- font-size: 20px;
2103
- font-weight: 600;
2104
- margin-bottom: 4px;
3481
+ font-size: 19px;
3482
+ font-weight: 650;
3483
+ letter-spacing: -0.02em;
2105
3484
  text-align: center;
3485
+ margin-bottom: 6px;
2106
3486
  }
2107
3487
  .login-card p.subtitle {
2108
3488
  font-size: 13px;
2109
- color: var(--text2);
3489
+ color: var(--text-3);
2110
3490
  text-align: center;
2111
- margin-bottom: 28px;
2112
- }
2113
- .form-group {
2114
- margin-bottom: 18px;
3491
+ margin-bottom: 26px;
2115
3492
  }
3493
+
3494
+ .form-group { margin-bottom: 16px; }
2116
3495
  .form-group label {
2117
3496
  display: block;
2118
- font-size: 13px;
2119
- font-weight: 500;
2120
- margin-bottom: 6px;
2121
- color: var(--text2);
3497
+ font-size: 12.5px;
3498
+ font-weight: 600;
3499
+ margin-bottom: 7px;
3500
+ color: var(--text-2);
2122
3501
  }
2123
3502
  .form-group input {
2124
3503
  width: 100%;
2125
3504
  padding: 10px 12px;
2126
- background: var(--surface2);
3505
+ background: var(--bg);
2127
3506
  border: 1px solid var(--border);
2128
- border-radius: 6px;
3507
+ border-radius: var(--radius-sm);
2129
3508
  color: var(--text);
2130
- font-size: 14px;
3509
+ font-size: 13.5px;
3510
+ font-family: inherit;
2131
3511
  outline: none;
2132
- transition: border-color .15s;
3512
+ transition: border-color .15s, box-shadow .15s;
2133
3513
  }
2134
3514
  .form-group input:focus {
2135
3515
  border-color: var(--accent);
3516
+ box-shadow: 0 0 0 3px var(--accent-soft);
2136
3517
  }
2137
- .form-group input::placeholder {
2138
- color: var(--text2);
2139
- opacity: 0.6;
2140
- }
3518
+ .form-group input::placeholder { color: var(--text-3); }
3519
+
2141
3520
  button {
2142
3521
  width: 100%;
2143
3522
  padding: 10px;
2144
3523
  background: var(--accent);
2145
3524
  border: none;
2146
- border-radius: 6px;
3525
+ border-radius: var(--radius-sm);
2147
3526
  color: #fff;
2148
- font-size: 14px;
3527
+ font-size: 13.5px;
2149
3528
  font-weight: 600;
3529
+ font-family: inherit;
2150
3530
  cursor: pointer;
2151
- transition: background .15s;
2152
- margin-top: 4px;
3531
+ transition: background .15s, opacity .15s;
3532
+ margin-top: 6px;
2153
3533
  }
2154
3534
  button:hover { background: var(--accent-hover); }
2155
- button:disabled {
2156
- opacity: 0.5;
2157
- cursor: not-allowed;
2158
- }
3535
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
3536
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
3537
+
2159
3538
  .error-msg {
2160
- background: rgba(240,84,84,0.12);
2161
- color: var(--red);
2162
- padding: 8px 12px;
2163
- border-radius: 6px;
2164
- 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;
2165
3544
  margin-bottom: 16px;
2166
3545
  display: none;
2167
3546
  }
@@ -2169,23 +3548,40 @@ switchSettingsPage = function(page) {
2169
3548
  </style>
2170
3549
  </head>
2171
3550
  <body>
3551
+ <button type="button" class="theme-fab" id="themeFab" onclick="toggleThemeFab()" title="\u5207\u6362\u4E3B\u9898">&#x263D;</button>
2172
3552
  <div class="login-card">
2173
- <h1>Bridge Dashboard</h1>
2174
- <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>
2175
3559
  <div class="error-msg" id="errorMsg"></div>
2176
3560
  <form id="loginForm" onsubmit="return doLogin(event)">
2177
3561
  <div class="form-group">
2178
- <label for="username">Username</label>
2179
- <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>
2180
3564
  </div>
2181
3565
  <div class="form-group">
2182
- <label for="password">Password</label>
2183
- <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">
2184
3568
  </div>
2185
- <button type="submit" id="loginBtn">Log In</button>
3569
+ <button type="submit" id="loginBtn">\u767B\u5F55</button>
2186
3570
  </form>
2187
3571
  </div>
2188
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
+
2189
3585
  async function doLogin(e) {
2190
3586
  e.preventDefault();
2191
3587
  const btn = document.getElementById("loginBtn");
@@ -2207,9 +3603,9 @@ async function doLogin(e) {
2207
3603
  if (resp.ok) {
2208
3604
  const data = await resp.json();
2209
3605
  if (data.needSetup) {
2210
- window.location.href = "/setup";
3606
+ continueWithHash("/setup");
2211
3607
  } else {
2212
- window.location.href = "/";
3608
+ continueWithHash("/");
2213
3609
  }
2214
3610
  return;
2215
3611
  }
@@ -2231,27 +3627,20 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
2231
3627
  if (e.key === "Enter") doLogin(e);
2232
3628
  });
2233
3629
  </script>
3630
+ ${X}
2234
3631
  </body>
2235
- </html>`,je=`<!DOCTYPE html>
3632
+ </html>`,_e=`<!DOCTYPE html>
2236
3633
  <html lang="zh-CN">
2237
3634
  <head>
2238
3635
  <meta charset="UTF-8">
2239
3636
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2240
3637
  <title>Bridge Dashboard - Setup</title>
3638
+ ${O}
3639
+ ${z}
2241
3640
  <style>
2242
- :root {
2243
- --bg: #0f1117;
2244
- --surface: #1a1d2e;
2245
- --surface2: #232738;
2246
- --border: #2d3248;
2247
- --text: #e1e4f0;
2248
- --text2: #8b90a8;
2249
- --accent: #5b8def;
2250
- --accent-hover: #4a7de0;
2251
- --green: #43c6a8;
2252
- --red: #f05454;
2253
- --radius: 10px;
2254
- }
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
+
2255
3644
  * { margin:0; padding:0; box-sizing:border-box; }
2256
3645
  body {
2257
3646
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
@@ -2395,6 +3784,7 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
2395
3784
  </style>
2396
3785
  </head>
2397
3786
  <body>
3787
+ <button type="button" class="theme-fab" id="themeFab" onclick="toggleThemeFab()" title="\u5207\u6362\u4E3B\u9898">&#x263D;</button>
2398
3788
  <div class="setup-card" id="setupCard">
2399
3789
  <h1>Bridge Dashboard</h1>
2400
3790
  <p class="subtitle">First-time setup \u2014 \u9996\u6B21\u767B\u5F55\u8BBE\u7F6E</p>
@@ -2468,7 +3858,8 @@ async function doSave(e) {
2468
3858
  document.getElementById("setupForm").style.display = "none";
2469
3859
  document.querySelector(".hint").style.display = "none";
2470
3860
  document.querySelector(".badge").style.display = "none";
2471
- 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);
2472
3863
  } else {
2473
3864
  errorEl.textContent = data.error || "Save failed";
2474
3865
  errorEl.classList.add("visible");
@@ -2501,7 +3892,8 @@ async function doSkip() {
2501
3892
  document.getElementById("setupForm").style.display = "none";
2502
3893
  document.querySelector(".hint").style.display = "none";
2503
3894
  document.querySelector(".badge").style.display = "none";
2504
- 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);
2505
3897
  } else {
2506
3898
  errorEl.textContent = data.error || "Skip failed";
2507
3899
  errorEl.classList.add("visible");
@@ -2518,6 +3910,7 @@ async function doSkip() {
2518
3910
 
2519
3911
  document.getElementById("setupForm").addEventListener("submit", doSave);
2520
3912
  </script>
3913
+ ${X}
2521
3914
  </body>
2522
- </html>`;export{c as dashboardRouter,Ce as noteDashboardActivity};
3915
+ </html>`;export{f as dashboardRouter,Pe as noteDashboardActivity};
2523
3916