@jsonstudio/rcc 0.89.2202 → 0.90.1

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 (260) hide show
  1. package/README.md +27 -0
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli/commands/claude.js +1 -0
  5. package/dist/cli/commands/claude.js.map +1 -1
  6. package/dist/cli/commands/codex.js +1 -0
  7. package/dist/cli/commands/codex.js.map +1 -1
  8. package/dist/cli/commands/guardian-daemon.d.ts +2 -0
  9. package/dist/cli/commands/guardian-daemon.js +299 -0
  10. package/dist/cli/commands/guardian-daemon.js.map +1 -0
  11. package/dist/cli/commands/init/camoufox.js +1 -1
  12. package/dist/cli/commands/init/camoufox.js.map +1 -1
  13. package/dist/cli/commands/init.js +15 -1
  14. package/dist/cli/commands/init.js.map +1 -1
  15. package/dist/cli/commands/launcher/types.d.ts +6 -0
  16. package/dist/cli/commands/launcher-kernel.js +456 -109
  17. package/dist/cli/commands/launcher-kernel.js.map +1 -1
  18. package/dist/cli/commands/port.js +28 -8
  19. package/dist/cli/commands/port.js.map +1 -1
  20. package/dist/cli/commands/restart.d.ts +4 -0
  21. package/dist/cli/commands/restart.js +91 -42
  22. package/dist/cli/commands/restart.js.map +1 -1
  23. package/dist/cli/commands/start-types.d.ts +4 -0
  24. package/dist/cli/commands/start.js +112 -68
  25. package/dist/cli/commands/start.js.map +1 -1
  26. package/dist/cli/commands/stop.d.ts +3 -0
  27. package/dist/cli/commands/stop.js +30 -63
  28. package/dist/cli/commands/stop.js.map +1 -1
  29. package/dist/cli/config/init-config.js +15 -1
  30. package/dist/cli/config/init-config.js.map +1 -1
  31. package/dist/cli/config/init-provider-catalog.js +13 -5
  32. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  33. package/dist/cli/guardian/client.d.ts +38 -0
  34. package/dist/cli/guardian/client.js +237 -0
  35. package/dist/cli/guardian/client.js.map +1 -0
  36. package/dist/cli/guardian/paths.d.ts +7 -0
  37. package/dist/cli/guardian/paths.js +13 -0
  38. package/dist/cli/guardian/paths.js.map +1 -0
  39. package/dist/cli/guardian/types.d.ts +30 -0
  40. package/dist/cli/guardian/types.js +2 -0
  41. package/dist/cli/guardian/types.js.map +1 -0
  42. package/dist/cli/register/guardian-daemon-command.d.ts +2 -0
  43. package/dist/cli/register/guardian-daemon-command.js +5 -0
  44. package/dist/cli/register/guardian-daemon-command.js.map +1 -0
  45. package/dist/cli/server/port-utils.js +57 -1
  46. package/dist/cli/server/port-utils.js.map +1 -1
  47. package/dist/cli.js +48 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/commands/oauth.js +6 -6
  50. package/dist/commands/oauth.js.map +1 -1
  51. package/dist/commands/provider-update.js +12 -0
  52. package/dist/commands/provider-update.js.map +1 -1
  53. package/dist/config/routecodex-config-loader.js +66 -1
  54. package/dist/config/routecodex-config-loader.js.map +1 -1
  55. package/dist/config/virtual-router-builder.js +18 -0
  56. package/dist/config/virtual-router-builder.js.map +1 -1
  57. package/dist/config/virtual-router-types.js +20 -5
  58. package/dist/config/virtual-router-types.js.map +1 -1
  59. package/dist/daemon-admin-ui/assets/index-C8vP_c5E.js +15 -0
  60. package/dist/daemon-admin-ui/assets/index-DjIoHmNv.css +1 -0
  61. package/dist/daemon-admin-ui/index.html +13 -0
  62. package/dist/docs/daemon-admin-ui.html +328 -57
  63. package/dist/index.d.ts +9 -0
  64. package/dist/index.js +268 -10
  65. package/dist/index.js.map +1 -1
  66. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.d.ts +1 -0
  67. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js +36 -0
  68. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js.map +1 -1
  69. package/dist/manager/modules/quota/provider-quota-daemon.events.js +50 -1
  70. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  71. package/dist/providers/auth/antigravity-user-agent.js +78 -31
  72. package/dist/providers/auth/antigravity-user-agent.js.map +1 -1
  73. package/dist/providers/auth/gemini-cli-userinfo-helper.js +94 -63
  74. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  75. package/dist/providers/auth/iflow-userinfo-helper.js +1 -1
  76. package/dist/providers/auth/iflow-userinfo-helper.js.map +1 -1
  77. package/dist/providers/auth/oauth-error-message.d.ts +1 -0
  78. package/dist/providers/auth/oauth-error-message.js +44 -0
  79. package/dist/providers/auth/oauth-error-message.js.map +1 -0
  80. package/dist/providers/auth/oauth-lifecycle/error-detection.js +42 -8
  81. package/dist/providers/auth/oauth-lifecycle/error-detection.js.map +1 -1
  82. package/dist/providers/auth/oauth-lifecycle/token-io.d.ts +1 -0
  83. package/dist/providers/auth/oauth-lifecycle/token-io.js +12 -0
  84. package/dist/providers/auth/oauth-lifecycle/token-io.js.map +1 -1
  85. package/dist/providers/auth/oauth-lifecycle.js +502 -87
  86. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  87. package/dist/providers/auth/oauth-repair-cooldown.js +2 -7
  88. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
  89. package/dist/providers/auth/oauth-repair-env.js +3 -5
  90. package/dist/providers/auth/oauth-repair-env.js.map +1 -1
  91. package/dist/providers/auth/oauth-utils/error-extraction.js +42 -8
  92. package/dist/providers/auth/oauth-utils/error-extraction.js.map +1 -1
  93. package/dist/providers/auth/qwen-userinfo-helper.js +18 -3
  94. package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
  95. package/dist/providers/auth/tokenfile-auth.d.ts +1 -0
  96. package/dist/providers/auth/tokenfile-auth.js +15 -9
  97. package/dist/providers/auth/tokenfile-auth.js.map +1 -1
  98. package/dist/providers/core/config/camoufox-actions.d.ts +31 -0
  99. package/dist/providers/core/config/camoufox-actions.js +461 -0
  100. package/dist/providers/core/config/camoufox-actions.js.map +1 -0
  101. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  102. package/dist/providers/core/config/camoufox-launcher.js +518 -160
  103. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  104. package/dist/providers/core/config/oauth-flows.js +6 -44
  105. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  106. package/dist/providers/core/config/provider-oauth-configs.js +51 -7
  107. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  108. package/dist/providers/core/config/service-profiles.js +13 -4
  109. package/dist/providers/core/config/service-profiles.js.map +1 -1
  110. package/dist/providers/core/runtime/http-transport-provider.d.ts +1 -0
  111. package/dist/providers/core/runtime/http-transport-provider.js +60 -1
  112. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  113. package/dist/providers/core/runtime/provider-error-classifier.js +32 -15
  114. package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
  115. package/dist/providers/core/runtime/provider-family-profile-utils.js +1 -1
  116. package/dist/providers/core/runtime/provider-family-profile-utils.js.map +1 -1
  117. package/dist/providers/core/runtime/provider-response-postprocessor.js +61 -14
  118. package/dist/providers/core/runtime/provider-response-postprocessor.js.map +1 -1
  119. package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
  120. package/dist/providers/core/strategies/oauth-auth-code-flow.js +124 -19
  121. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  122. package/dist/providers/core/strategies/oauth-device-flow.js +6 -3
  123. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  124. package/dist/providers/profile/families/iflow-profile.js +83 -10
  125. package/dist/providers/profile/families/iflow-profile.js.map +1 -1
  126. package/dist/providers/profile/families/qwen-profile.js +203 -0
  127. package/dist/providers/profile/families/qwen-profile.js.map +1 -1
  128. package/dist/scripts/camoufox/launch-auth.mjs +112 -5
  129. package/dist/server/handlers/config-admin-handler.js +9 -2
  130. package/dist/server/handlers/config-admin-handler.js.map +1 -1
  131. package/dist/server/handlers/handler-utils.js +3 -14
  132. package/dist/server/handlers/handler-utils.js.map +1 -1
  133. package/dist/server/handlers/logging.js +3 -4
  134. package/dist/server/handlers/logging.js.map +1 -1
  135. package/dist/server/runtime/http-server/clock-client-reaper.d.ts +1 -0
  136. package/dist/server/runtime/http-server/clock-client-reaper.js +21 -15
  137. package/dist/server/runtime/http-server/clock-client-reaper.js.map +1 -1
  138. package/dist/server/runtime/http-server/clock-client-registry-utils.d.ts +4 -0
  139. package/dist/server/runtime/http-server/clock-client-registry-utils.js +74 -16
  140. package/dist/server/runtime/http-server/clock-client-registry-utils.js.map +1 -1
  141. package/dist/server/runtime/http-server/clock-client-registry.d.ts +15 -0
  142. package/dist/server/runtime/http-server/clock-client-registry.js +300 -6
  143. package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -1
  144. package/dist/server/runtime/http-server/clock-client-routes.js +49 -19
  145. package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -1
  146. package/dist/server/runtime/http-server/clock-daemon-log-throttle.d.ts +16 -0
  147. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js +49 -0
  148. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js.map +1 -1
  149. package/dist/server/runtime/http-server/clock-scope-resolution.d.ts +14 -0
  150. package/dist/server/runtime/http-server/clock-scope-resolution.js +212 -0
  151. package/dist/server/runtime/http-server/clock-scope-resolution.js.map +1 -0
  152. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +5 -3
  153. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  154. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +104 -15
  155. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
  156. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +2 -2
  157. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  158. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.d.ts +24 -0
  159. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js +316 -70
  160. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js.map +1 -1
  161. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +190 -1
  162. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  163. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +18 -29
  164. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -1
  165. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +2 -0
  166. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
  167. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +8 -1
  168. package/dist/server/runtime/http-server/daemon-admin-routes.js +30 -0
  169. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  170. package/dist/server/runtime/http-server/executor/client-injection-flow.d.ts +14 -0
  171. package/dist/server/runtime/http-server/executor/client-injection-flow.js +287 -0
  172. package/dist/server/runtime/http-server/executor/client-injection-flow.js.map +1 -0
  173. package/dist/server/runtime/http-server/executor/index.d.ts +1 -1
  174. package/dist/server/runtime/http-server/executor/index.js +1 -1
  175. package/dist/server/runtime/http-server/executor/index.js.map +1 -1
  176. package/dist/server/runtime/http-server/executor/provider-response-converter.js +236 -62
  177. package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
  178. package/dist/server/runtime/http-server/executor/provider-response-utils.js +5 -0
  179. package/dist/server/runtime/http-server/executor/provider-response-utils.js.map +1 -1
  180. package/dist/server/runtime/http-server/executor/request-executor-core-utils.d.ts +2 -0
  181. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js +60 -0
  182. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js.map +1 -1
  183. package/dist/server/runtime/http-server/executor/request-retry-helpers.js +20 -8
  184. package/dist/server/runtime/http-server/executor/request-retry-helpers.js.map +1 -1
  185. package/dist/server/runtime/http-server/executor/sse-error-handler.d.ts +1 -0
  186. package/dist/server/runtime/http-server/executor/sse-error-handler.js +13 -2
  187. package/dist/server/runtime/http-server/executor/sse-error-handler.js.map +1 -1
  188. package/dist/server/runtime/http-server/executor/usage-aggregator.d.ts +0 -12
  189. package/dist/server/runtime/http-server/executor/usage-aggregator.js +84 -88
  190. package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
  191. package/dist/server/runtime/http-server/executor-metadata.js +328 -7
  192. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  193. package/dist/server/runtime/http-server/executor-response.d.ts +1 -0
  194. package/dist/server/runtime/http-server/executor-response.js +52 -50
  195. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  196. package/dist/server/runtime/http-server/http-server-bootstrap.js +55 -6
  197. package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
  198. package/dist/server/runtime/http-server/http-server-clock-daemon.d.ts +1 -0
  199. package/dist/server/runtime/http-server/http-server-clock-daemon.js +199 -44
  200. package/dist/server/runtime/http-server/http-server-clock-daemon.js.map +1 -1
  201. package/dist/server/runtime/http-server/http-server-lifecycle.js +4 -4
  202. package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
  203. package/dist/server/runtime/http-server/hub-shadow-compare.js +1 -1
  204. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  205. package/dist/server/runtime/http-server/index.d.ts +1 -0
  206. package/dist/server/runtime/http-server/index.js +1 -0
  207. package/dist/server/runtime/http-server/index.js.map +1 -1
  208. package/dist/server/runtime/http-server/middleware.js +82 -4
  209. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  210. package/dist/server/runtime/http-server/request-executor.js +26 -7
  211. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  212. package/dist/server/runtime/http-server/routes.d.ts +2 -1
  213. package/dist/server/runtime/http-server/routes.js +4 -2
  214. package/dist/server/runtime/http-server/routes.js.map +1 -1
  215. package/dist/server/runtime/http-server/session-dir.js +12 -1
  216. package/dist/server/runtime/http-server/session-dir.js.map +1 -1
  217. package/dist/server/runtime/http-server/stats-manager.d.ts +35 -0
  218. package/dist/server/runtime/http-server/stats-manager.js +269 -21
  219. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  220. package/dist/server/runtime/http-server/stopmessage-scope-rebind.d.ts +13 -0
  221. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js +168 -0
  222. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js.map +1 -0
  223. package/dist/server/runtime/http-server/tmux-session-probe.d.ts +10 -0
  224. package/dist/server/runtime/http-server/tmux-session-probe.js +97 -0
  225. package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -1
  226. package/dist/server-lifecycle/port-utils.d.ts +2 -1
  227. package/dist/server-lifecycle/port-utils.js +84 -4
  228. package/dist/server-lifecycle/port-utils.js.map +1 -1
  229. package/dist/token-daemon/index.d.ts +1 -0
  230. package/dist/token-daemon/index.js +17 -12
  231. package/dist/token-daemon/index.js.map +1 -1
  232. package/dist/utils/clock-client-token.d.ts +2 -1
  233. package/dist/utils/clock-client-token.js +52 -8
  234. package/dist/utils/clock-client-token.js.map +1 -1
  235. package/dist/utils/clock-scope-trace.d.ts +11 -0
  236. package/dist/utils/clock-scope-trace.js +41 -0
  237. package/dist/utils/clock-scope-trace.js.map +1 -0
  238. package/dist/utils/llms-engine-shadow.js +1 -1
  239. package/dist/utils/llms-engine-shadow.js.map +1 -1
  240. package/docs/DAEMON_CONTROL_PLANE.md +1 -0
  241. package/docs/ROUTING_POLICY_SCHEMA.md +4 -2
  242. package/docs/daemon-admin-ui.html +328 -57
  243. package/docs/design/servertool-stopmessage-lifecycle.md +109 -0
  244. package/docs/exec-command-guard-policy.example.v1.json +7 -1
  245. package/docs/providers/antigravity-gemini-provider-compat.md +2 -2
  246. package/package.json +23 -6
  247. package/scripts/build-core.mjs +12 -0
  248. package/scripts/camoufox/launch-auth.mjs +112 -5
  249. package/scripts/ci/repo-sanity.mjs +1 -0
  250. package/scripts/cleanup-stale-server-pids.mjs +142 -0
  251. package/scripts/install-global.sh +8 -0
  252. package/scripts/install-verify.mjs +33 -16
  253. package/scripts/run-bg.sh +226 -43
  254. package/scripts/run-fg-gtimeout.sh +158 -14
  255. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +3 -3
  256. package/scripts/tests/ci-jest.mjs +9 -1
  257. package/scripts/triage-errorsamples.mjs +216 -0
  258. package/scripts/verify-codex-error-samples.mjs +92 -15
  259. package/scripts/verify-e2e-toolcall.mjs +12 -1
  260. package/scripts/verify-install-e2e.mjs +69 -28
@@ -0,0 +1 @@
1
+ :root{--bg: #070b14;--bg-2: #0d1426;--panel: rgba(255, 255, 255, .06);--panel-2: rgba(255, 255, 255, .03);--border: rgba(255, 255, 255, .1);--text: rgba(255, 255, 255, .92);--muted: rgba(255, 255, 255, .62);--ok: #40d67b;--warn: #ffbf47;--err: #ff6a6a;--accent: #5ca8ff;--radius: 12px;--shadow: 0 12px 28px rgba(0, 0, 0, .35)}*{box-sizing:border-box}html,body,#root{margin:0;min-height:100%}body{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:var(--text);background:radial-gradient(circle at 0% 0%,rgba(84,122,255,.25),transparent 40%),radial-gradient(circle at 100% 100%,rgba(30,180,180,.18),transparent 42%),linear-gradient(145deg,var(--bg-2),var(--bg))}.app{max-width:1800px;margin:0 auto;padding:14px}.topbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.title{margin:0;font-size:16px;font-weight:680}.subtitle{margin:3px 0 0;color:var(--muted);font-size:12px}.pill-row{display:flex;gap:8px;flex-wrap:wrap}.pill{border:1px solid var(--border);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:#ffffff0a;white-space:nowrap}.pill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok),transparent 55%)}.pill.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn),transparent 50%)}.pill.err{color:var(--err);border-color:color-mix(in srgb,var(--err),transparent 50%)}.panel{border:1px solid var(--border);border-radius:var(--radius);background:linear-gradient(180deg,var(--panel),var(--panel-2));box-shadow:var(--shadow);padding:12px}.auth-grid{display:grid;grid-template-columns:2fr 1fr;gap:10px;margin-bottom:10px}.nav{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}.nav button{border:1px solid var(--border);background:#ffffff0a;color:var(--muted);border-radius:999px;padding:8px 12px;font-size:12px;cursor:pointer}.nav button.active,button.active{color:var(--text);border-color:color-mix(in srgb,var(--accent),transparent 45%);background:color-mix(in srgb,var(--accent),transparent 82%)}.actionbar{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.grid{display:grid;gap:10px}.grid-2{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.grid-3{grid-template-columns:minmax(0,1.1fr) minmax(0,1fr) minmax(0,1fr)}.grid-wide-left{grid-template-columns:minmax(0,1.7fr) minmax(0,1fr)}.grid-wide-right{grid-template-columns:minmax(0,1fr) minmax(0,1.7fr)}.card-title{margin:0;font-size:13px;font-weight:650}.card-sub{margin:4px 0 10px;font-size:12px;color:var(--muted)}input,select,textarea,button{font:inherit}label{font-size:12px;color:var(--muted)}input,select,textarea{border:1px solid var(--border);background:#00000047;color:var(--text);border-radius:10px;padding:7px 9px;font-size:12px}input,select{height:32px}textarea{width:100%;min-height:340px;resize:vertical;font-family:ui-monospace,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;line-height:1.45}button{border:1px solid var(--border);background:#ffffff0a;color:var(--text);border-radius:10px;padding:7px 10px;font-size:12px;cursor:pointer}button.primary{border-color:color-mix(in srgb,var(--accent),transparent 40%);background:color-mix(in srgb,var(--accent),transparent 84%)}button.warn{border-color:color-mix(in srgb,var(--warn),transparent 45%);background:color-mix(in srgb,var(--warn),transparent 82%)}button.danger{border-color:color-mix(in srgb,var(--err),transparent 40%);background:color-mix(in srgb,var(--err),transparent 84%)}button:disabled{opacity:.5;cursor:not-allowed}.mono{font-family:ui-monospace,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.muted{color:var(--muted)}.notice{border:1px solid var(--border);border-radius:10px;padding:8px 10px;background:#0003;font-size:12px}.table-wrap{border:1px solid rgba(255,255,255,.08);border-radius:10px;overflow:auto;max-height:58vh;background:#00000029}.table-wrap.short{max-height:32vh}.table{width:100%;border-collapse:collapse;table-layout:fixed}.table th,.table td{border-bottom:1px solid rgba(255,255,255,.06);padding:7px 8px;font-size:12px;text-align:left;vertical-align:top;word-break:break-word}.table th{position:sticky;top:0;background:#0e121ef5;z-index:1;color:var(--muted)}.table tbody tr:hover td{background:#ffffff08}.table tbody tr.selected td{background:color-mix(in srgb,var(--accent),transparent 88%)}.log{border:1px solid var(--border);border-radius:10px;padding:8px;background:#00000042;min-height:70px;max-height:190px;overflow:auto;white-space:pre-wrap;font-size:12px;line-height:1.35}.kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}.kpi{border:1px solid var(--border);border-radius:10px;padding:8px;background:#ffffff05}.kpi .v{font-size:15px;font-weight:700}.kpi .k{font-size:11px;color:var(--muted)}.split{display:grid;gap:8px;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.app.density-comfortable .panel{padding:14px}.app.density-comfortable .table th,.app.density-comfortable .table td{padding:9px 10px;font-size:13px}.app.density-comfortable input,.app.density-comfortable select,.app.density-comfortable textarea,.app.density-comfortable button,.app.density-comfortable .pill,.app.density-comfortable .notice,.app.density-comfortable .card-sub,.app.density-comfortable .muted{font-size:13px}@media(max-width:1280px){.grid-3,.grid-2,.grid-wide-left,.grid-wide-right,.split,.auth-grid{grid-template-columns:minmax(0,1fr)}.table-wrap{max-height:none}textarea{min-height:260px}.kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>RouteCodex Admin</title>
7
+ <script type="module" crossorigin src="/daemon/admin/assets/index-C8vP_c5E.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/daemon/admin/assets/index-DjIoHmNv.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -1489,6 +1489,14 @@
1489
1489
  <select id="routingSourceSelect" style="width: 420px;"></select>
1490
1490
  <button id="refreshRoutingSourcesBtn">Refresh sources</button>
1491
1491
  </div>
1492
+ <div class="row" style="margin-bottom: 10px;">
1493
+ <label for="routingGroupSelect">group</label>
1494
+ <select id="routingGroupSelect" style="width: 240px;"></select>
1495
+ <input id="routingGroupNameInput" type="text" placeholder="new group id" style="width: 200px;" />
1496
+ <button id="createRoutingGroupBtn">Create/Copy group</button>
1497
+ <button id="deleteRoutingGroupBtn" class="danger">Delete group</button>
1498
+ <span id="routingActiveGroupHint" class="mono muted" style="margin-left:auto;"></span>
1499
+ </div>
1492
1500
  <div class="row" style="margin-bottom: 10px;">
1493
1501
  <label for="routingQuotaModeSelect">mode</label>
1494
1502
  <select id="routingQuotaModeSelect" style="width: 160px;">
@@ -1509,9 +1517,9 @@
1509
1517
  </div>
1510
1518
  <div class="row" style="margin-bottom: 10px;">
1511
1519
  <button id="loadRoutingBtn" class="primary">Load</button>
1512
- <button id="saveRoutingBtn" class="primary">Save</button>
1513
- <button id="saveRoutingRestartLocalBtn">Save + Restart local</button>
1514
- <button id="saveRoutingRestartAllBtn">Save + Restart all</button>
1520
+ <button id="saveRoutingBtn" class="primary">Save group</button>
1521
+ <button id="saveRoutingRestartLocalBtn">Activate group</button>
1522
+ <button id="saveRoutingRestartAllBtn">Activate + Restart all</button>
1515
1523
  <button id="refreshRoutingPoolBtn">Refresh pool status</button>
1516
1524
  <button id="routingRegExpandBtn">Expand all</button>
1517
1525
  <button id="routingRegCollapseBtn">Collapse all</button>
@@ -1551,6 +1559,9 @@
1551
1559
  routingTargetsUpdatedAt: 0,
1552
1560
  routingSources: [],
1553
1561
  routingSourcesUpdatedAt: 0,
1562
+ routingGroups: {},
1563
+ routingActiveGroupId: "",
1564
+ routingSelectedGroupId: "",
1554
1565
  routingLocation: "virtualrouter.routing",
1555
1566
  routingHasLoadBalancing: false,
1556
1567
  credentials: [],
@@ -4245,9 +4256,13 @@
4245
4256
  }
4246
4257
 
4247
4258
  function buildRoutingPolicyEditorValue(snapshot) {
4248
- const routingObj = toPlainObjectOrNull(snapshot && snapshot.routing) || {};
4249
- const hasLoadBalancing = Boolean(snapshot && snapshot.hasLoadBalancing === true);
4250
- const loadBalancingObj = toPlainObjectOrNull(snapshot && snapshot.loadBalancing) || {};
4259
+ const source = toPlainObjectOrNull(snapshot) || {};
4260
+ const routingObj = toPlainObjectOrNull(source.routing) || {};
4261
+ const loadBalancingObj = toPlainObjectOrNull(source.loadBalancing) || {};
4262
+ const hasLoadBalancing =
4263
+ typeof source.hasLoadBalancing === "boolean"
4264
+ ? Boolean(source.hasLoadBalancing)
4265
+ : Boolean(loadBalancingObj && Object.keys(loadBalancingObj).length > 0);
4251
4266
  UI.routingHasLoadBalancing = hasLoadBalancing;
4252
4267
  return {
4253
4268
  routing: routingObj,
@@ -4276,30 +4291,114 @@
4276
4291
  };
4277
4292
  }
4278
4293
 
4279
- async function loadRouting() {
4280
- setLog("routingOpLog", "");
4281
- const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
4282
- if (!auth || !auth.authenticated) {
4283
- notifyUnauthorizedOnce("routing");
4284
- return;
4285
- }
4286
- try {
4287
- const selectedPath = textOf($("routingSourceSelect").value || "").trim();
4288
- const query = selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
4289
- const out = await apiFetch(`/config/routing${query}`);
4290
- UI.routingLocation = out.location || "virtualrouter.routing";
4291
- routingEditorSetValue(buildRoutingPolicyEditorValue(out));
4292
- setLog(
4293
- "routingOpLog",
4294
- `Loaded. Path: ${out.path || "—"}\nLocation: ${UI.routingLocation}\nloadBalancing: ${UI.routingHasLoadBalancing ? "present" : "absent"}`
4295
- );
4296
- toast("Routing policy loaded.", "ok");
4297
- } catch (e) {
4298
- if (e && e.status === 401) notifyUnauthorizedOnce("routing");
4299
- setLog("routingOpLog", `Load failed: ${e.message}`);
4300
- toast(`Load failed: ${e.message}`);
4301
- }
4302
- }
4294
+ function normalizeRoutingGroupsMap(value) {
4295
+ const out = {};
4296
+ if (!value || typeof value !== "object" || Array.isArray(value)) return out;
4297
+ for (const [groupId, groupPolicy] of Object.entries(value)) {
4298
+ const policyObj = toPlainObjectOrNull(groupPolicy);
4299
+ if (!groupId || !policyObj) continue;
4300
+ out[groupId] = policyObj;
4301
+ }
4302
+ return out;
4303
+ }
4304
+
4305
+ function currentRoutingSourcePath() {
4306
+ return textOf($("routingSourceSelect").value || "").trim();
4307
+ }
4308
+
4309
+ function currentRoutingSourceQuery() {
4310
+ const selectedPath = currentRoutingSourcePath();
4311
+ return selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
4312
+ }
4313
+
4314
+ function updateRoutingGroupHint() {
4315
+ const hint = $("routingActiveGroupHint");
4316
+ if (!hint) return;
4317
+ const selected = textOf(UI.routingSelectedGroupId || "").trim();
4318
+ const active = textOf(UI.routingActiveGroupId || "").trim();
4319
+ if (!selected && !active) {
4320
+ hint.textContent = "group: —";
4321
+ return;
4322
+ }
4323
+ hint.textContent = `selected=${selected || "—"} · active=${active || "—"}`;
4324
+ }
4325
+
4326
+ function renderRoutingGroupSelect(preferredGroupId) {
4327
+ const select = $("routingGroupSelect");
4328
+ if (!select) return "";
4329
+ const groups = normalizeRoutingGroupsMap(UI.routingGroups);
4330
+ let ids = Object.keys(groups).sort((a, b) => a.localeCompare(b));
4331
+ if (!ids.length) {
4332
+ groups.default = { routing: {} };
4333
+ ids = ["default"];
4334
+ }
4335
+ UI.routingGroups = groups;
4336
+
4337
+ const prev = textOf(select.value || "").trim();
4338
+ select.replaceChildren();
4339
+ for (const id of ids) {
4340
+ const opt = document.createElement("option");
4341
+ opt.value = id;
4342
+ opt.textContent = id === UI.routingActiveGroupId ? `${id} (active)` : id;
4343
+ select.appendChild(opt);
4344
+ }
4345
+ const nextSelected =
4346
+ (preferredGroupId && groups[preferredGroupId] ? preferredGroupId : "")
4347
+ || (prev && groups[prev] ? prev : "")
4348
+ || (UI.routingSelectedGroupId && groups[UI.routingSelectedGroupId] ? UI.routingSelectedGroupId : "")
4349
+ || (UI.routingActiveGroupId && groups[UI.routingActiveGroupId] ? UI.routingActiveGroupId : "")
4350
+ || ids[0];
4351
+ select.value = nextSelected;
4352
+ UI.routingSelectedGroupId = nextSelected;
4353
+ updateRoutingGroupHint();
4354
+ return nextSelected;
4355
+ }
4356
+
4357
+ function getSelectedRoutingGroupId() {
4358
+ const select = $("routingGroupSelect");
4359
+ const selected = textOf((select && select.value) || UI.routingSelectedGroupId || "").trim();
4360
+ if (selected) UI.routingSelectedGroupId = selected;
4361
+ return selected;
4362
+ }
4363
+
4364
+ function loadSelectedRoutingGroupIntoEditor() {
4365
+ const selectedGroupId = getSelectedRoutingGroupId();
4366
+ const groups = normalizeRoutingGroupsMap(UI.routingGroups);
4367
+ const selectedPolicy = toPlainObjectOrNull(groups[selectedGroupId]) || { routing: {} };
4368
+ routingEditorSetValue(buildRoutingPolicyEditorValue(selectedPolicy));
4369
+ updateRoutingGroupHint();
4370
+ }
4371
+
4372
+ async function loadRouting(preferredGroupId) {
4373
+ setLog("routingOpLog", "");
4374
+ const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
4375
+ if (!auth || !auth.authenticated) {
4376
+ notifyUnauthorizedOnce("routing");
4377
+ return;
4378
+ }
4379
+ try {
4380
+ const selectedPath = currentRoutingSourcePath();
4381
+ const query = currentRoutingSourceQuery();
4382
+ const out = await apiFetch(`/config/routing/groups${query}`);
4383
+ UI.routingLocation = out.location || "virtualrouter.routing";
4384
+ UI.routingGroups = normalizeRoutingGroupsMap(out.groups);
4385
+ UI.routingActiveGroupId = textOf(out.activeGroupId || "").trim();
4386
+ const selectedGroupId = renderRoutingGroupSelect(preferredGroupId);
4387
+ if (!UI.routingActiveGroupId && selectedGroupId) {
4388
+ UI.routingActiveGroupId = selectedGroupId;
4389
+ }
4390
+ loadSelectedRoutingGroupIntoEditor();
4391
+ setLog(
4392
+ "routingOpLog",
4393
+ `Loaded groups. Path: ${out.path || selectedPath || "—"}\nLocation: ${UI.routingLocation}\nselected: ${UI.routingSelectedGroupId || "—"}\nactive: ${UI.routingActiveGroupId || "—"}\nloadBalancing: ${UI.routingHasLoadBalancing ? "present" : "absent"}`
4394
+ );
4395
+ toast("Routing groups loaded.", "ok");
4396
+ } catch (e) {
4397
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing");
4398
+ setLog("routingOpLog", `Load failed: ${e.message}`);
4399
+ toast(`Load failed: ${e.message}`);
4400
+ }
4401
+ }
4303
4402
 
4304
4403
  async function saveRouting() {
4305
4404
  setLog("routingOpLog", "");
@@ -4309,30 +4408,41 @@
4309
4408
  return null;
4310
4409
  }
4311
4410
  try {
4312
- const selectedPath = textOf($("routingSourceSelect").value || "").trim();
4313
- const query = selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
4411
+ const selectedPath = currentRoutingSourcePath();
4412
+ const query = currentRoutingSourceQuery();
4413
+ const selectedGroupId = getSelectedRoutingGroupId();
4414
+ if (!selectedGroupId) {
4415
+ throw new Error("No routing group selected");
4416
+ }
4314
4417
  const policy = parseRoutingPolicyEditorValue(routingEditorGetValue() || {});
4315
- const payload = {
4316
- routing: policy.routing,
4317
- location: UI.routingLocation,
4318
- path: selectedPath || undefined
4418
+ const basePolicy = toPlainObjectOrNull((UI.routingGroups || {})[selectedGroupId]) || {};
4419
+ const nextPolicy = {
4420
+ ...basePolicy,
4421
+ routing: policy.routing
4319
4422
  };
4320
4423
  if (policy.shouldWriteLoadBalancing) {
4321
- payload.loadBalancing = policy.loadBalancing || {};
4424
+ nextPolicy.loadBalancing = policy.loadBalancing || {};
4425
+ } else {
4426
+ delete nextPolicy.loadBalancing;
4322
4427
  }
4323
- const out = await apiFetch(`/config/routing${query}`, {
4428
+ const out = await apiFetch(`/config/routing/groups/${encodeURIComponent(selectedGroupId)}${query}`, {
4324
4429
  method: "PUT",
4325
- body: JSON.stringify(payload)
4430
+ body: JSON.stringify({
4431
+ policy: nextPolicy,
4432
+ location: UI.routingLocation,
4433
+ path: selectedPath || undefined
4434
+ })
4326
4435
  });
4327
- UI.routingLocation = out.location || UI.routingLocation;
4328
- if (typeof out.hasLoadBalancing === "boolean") {
4329
- UI.routingHasLoadBalancing = out.hasLoadBalancing;
4330
- }
4436
+ UI.routingLocation = out.location || UI.routingLocation;
4437
+ UI.routingGroups = normalizeRoutingGroupsMap(out.groups);
4438
+ UI.routingActiveGroupId = textOf(out.activeGroupId || UI.routingActiveGroupId || "").trim();
4439
+ renderRoutingGroupSelect(selectedGroupId);
4440
+ loadSelectedRoutingGroupIntoEditor();
4331
4441
  setLog(
4332
4442
  "routingOpLog",
4333
- `Saved. Path: ${out.path || "—"}\nLocation: ${UI.routingLocation}\nloadBalancing: ${UI.routingHasLoadBalancing ? "present" : "absent"}\nRestart required to apply.`
4443
+ `Saved group. Path: ${out.path || selectedPath || "—"}\nLocation: ${UI.routingLocation}\nselected: ${UI.routingSelectedGroupId || ""}\nactive: ${UI.routingActiveGroupId || ""}`
4334
4444
  );
4335
- toast("Routing policy saved.", "ok");
4445
+ toast("Routing group saved.", "ok");
4336
4446
  routingEditorSetDirty(false);
4337
4447
  return out;
4338
4448
  } catch (e) {
@@ -4344,17 +4454,39 @@
4344
4454
  }
4345
4455
 
4346
4456
  async function saveRoutingAndRestartLocal() {
4347
- const saved = await saveRouting();
4348
- if (!saved) return;
4349
- if (!confirm("Restart this RouteCodex runtime now to apply routing policy changes?")) return;
4457
+ const selectedGroupId = getSelectedRoutingGroupId();
4458
+ if (!selectedGroupId) {
4459
+ toast("No routing group selected.");
4460
+ return;
4461
+ }
4462
+ if (routingEditorState.dirty) {
4463
+ const shouldSave = confirm("Current group has unsaved changes. Save now before activation?");
4464
+ if (!shouldSave) return;
4465
+ const saved = await saveRouting();
4466
+ if (!saved) return;
4467
+ }
4468
+ if (!confirm(`Activate routing group \"${selectedGroupId}\" and reload local runtime now?`)) return;
4350
4469
  try {
4351
- const out = await apiFetch("/daemon/restart", { method: "POST" });
4352
- const warnings = Array.isArray(out.warnings) && out.warnings.length ? `\nWarnings:\n- ${out.warnings.join("\n- ")}` : "";
4470
+ const selectedPath = currentRoutingSourcePath();
4471
+ const query = currentRoutingSourceQuery();
4472
+ const out = await apiFetch(`/config/routing/groups/activate${query}`, {
4473
+ method: "POST",
4474
+ body: JSON.stringify({
4475
+ groupId: selectedGroupId,
4476
+ location: UI.routingLocation,
4477
+ path: selectedPath || undefined
4478
+ })
4479
+ });
4480
+ UI.routingLocation = out.location || UI.routingLocation;
4481
+ UI.routingGroups = normalizeRoutingGroupsMap(out.groups);
4482
+ UI.routingActiveGroupId = textOf(out.activeGroupId || selectedGroupId).trim();
4483
+ renderRoutingGroupSelect(selectedGroupId);
4484
+ loadSelectedRoutingGroupIntoEditor();
4353
4485
  setLog(
4354
4486
  "routingOpLog",
4355
- `Saved + restarted local.\nconfigPath: ${out.configPath || "—"}\nreloadedAt: ${out.reloadedAt || "—"}${warnings}`
4487
+ `Activated local.\nPath: ${out.path || selectedPath || "—"}\nselected: ${UI.routingSelectedGroupId || "—"}\nactive: ${UI.routingActiveGroupId || "—"}\nselfReload: ${JSON.stringify(out.selfReload ?? null, null, 2)}`
4356
4488
  );
4357
- toast("Routing saved and local runtime restarted.", "ok");
4489
+ toast("Routing group activated (local).", "ok");
4358
4490
  await refreshStatus();
4359
4491
  await refreshProviders();
4360
4492
  await refreshCredentials();
@@ -4369,13 +4501,37 @@
4369
4501
  }
4370
4502
 
4371
4503
  async function saveRoutingAndRestartAll() {
4372
- const saved = await saveRouting();
4373
- if (!saved) return;
4504
+ const selectedGroupId = getSelectedRoutingGroupId();
4505
+ if (!selectedGroupId) {
4506
+ toast("No routing group selected.");
4507
+ return;
4508
+ }
4509
+ if (routingEditorState.dirty) {
4510
+ const shouldSave = confirm("Current group has unsaved changes. Save now before activation?");
4511
+ if (!shouldSave) return;
4512
+ const saved = await saveRouting();
4513
+ if (!saved) return;
4514
+ }
4374
4515
  if (!confirm("Broadcast restart all local RouteCodex servers now?")) return;
4375
4516
  try {
4517
+ const selectedPath = currentRoutingSourcePath();
4518
+ const query = currentRoutingSourceQuery();
4519
+ const activated = await apiFetch(`/config/routing/groups/activate${query}`, {
4520
+ method: "POST",
4521
+ body: JSON.stringify({
4522
+ groupId: selectedGroupId,
4523
+ location: UI.routingLocation,
4524
+ path: selectedPath || undefined
4525
+ })
4526
+ });
4527
+ UI.routingLocation = activated.location || UI.routingLocation;
4528
+ UI.routingGroups = normalizeRoutingGroupsMap(activated.groups);
4529
+ UI.routingActiveGroupId = textOf(activated.activeGroupId || selectedGroupId).trim();
4530
+ renderRoutingGroupSelect(selectedGroupId);
4531
+ loadSelectedRoutingGroupIntoEditor();
4376
4532
  const out = await controlMutate("servers.restart", {});
4377
- setLog("routingOpLog", `Saved + restart-all requested.\n${JSON.stringify(out, null, 2)}`);
4378
- toast("Routing saved and restart-all requested.", "ok");
4533
+ setLog("routingOpLog", `Activated + restart-all requested.\n${JSON.stringify(out, null, 2)}`);
4534
+ toast("Routing group activated and restart-all requested.", "ok");
4379
4535
  await refreshControl();
4380
4536
  await refreshStatus();
4381
4537
  } catch (e) {
@@ -4384,6 +4540,106 @@
4384
4540
  }
4385
4541
  }
4386
4542
 
4543
+ function buildRoutingTemplatePolicyForNewGroup() {
4544
+ const selectedGroupId = getSelectedRoutingGroupId();
4545
+ const groups = normalizeRoutingGroupsMap(UI.routingGroups);
4546
+ const base = toPlainObjectOrNull(groups[selectedGroupId]) || { routing: {} };
4547
+ if (!routingEditorState.dirty) {
4548
+ return { ...base };
4549
+ }
4550
+ const parsed = parseRoutingPolicyEditorValue(routingEditorGetValue() || {});
4551
+ const next = {
4552
+ ...base,
4553
+ routing: parsed.routing
4554
+ };
4555
+ if (parsed.shouldWriteLoadBalancing) {
4556
+ next.loadBalancing = parsed.loadBalancing || {};
4557
+ } else {
4558
+ delete next.loadBalancing;
4559
+ }
4560
+ return next;
4561
+ }
4562
+
4563
+ async function createRoutingGroup() {
4564
+ setLog("routingOpLog", "");
4565
+ const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
4566
+ if (!auth || !auth.authenticated) {
4567
+ notifyUnauthorizedOnce("routing groups");
4568
+ return;
4569
+ }
4570
+ const input = $("routingGroupNameInput");
4571
+ const groupId = textOf((input && input.value) || "").trim();
4572
+ if (!groupId) {
4573
+ toast("Group id is required.");
4574
+ return;
4575
+ }
4576
+ const selectedPath = currentRoutingSourcePath();
4577
+ const query = currentRoutingSourceQuery();
4578
+ try {
4579
+ const policy = buildRoutingTemplatePolicyForNewGroup();
4580
+ const out = await apiFetch(`/config/routing/groups/${encodeURIComponent(groupId)}${query}`, {
4581
+ method: "PUT",
4582
+ body: JSON.stringify({
4583
+ policy,
4584
+ location: UI.routingLocation,
4585
+ path: selectedPath || undefined
4586
+ })
4587
+ });
4588
+ UI.routingLocation = out.location || UI.routingLocation;
4589
+ UI.routingGroups = normalizeRoutingGroupsMap(out.groups);
4590
+ UI.routingActiveGroupId = textOf(out.activeGroupId || UI.routingActiveGroupId || "").trim();
4591
+ renderRoutingGroupSelect(groupId);
4592
+ loadSelectedRoutingGroupIntoEditor();
4593
+ if (input) input.value = "";
4594
+ setLog(
4595
+ "routingOpLog",
4596
+ `Created/updated group.\nPath: ${out.path || selectedPath || "—"}\nselected: ${UI.routingSelectedGroupId || "—"}\nactive: ${UI.routingActiveGroupId || "—"}`
4597
+ );
4598
+ toast("Routing group created/updated.", "ok");
4599
+ } catch (e) {
4600
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing groups");
4601
+ setLog("routingOpLog", `Create group failed: ${e && e.message ? e.message : String(e)}`);
4602
+ toast(`Create group failed: ${e && e.message ? e.message : String(e)}`);
4603
+ }
4604
+ }
4605
+
4606
+ async function deleteRoutingGroup() {
4607
+ setLog("routingOpLog", "");
4608
+ const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
4609
+ if (!auth || !auth.authenticated) {
4610
+ notifyUnauthorizedOnce("routing groups");
4611
+ return;
4612
+ }
4613
+ const groupId = getSelectedRoutingGroupId();
4614
+ if (!groupId) {
4615
+ toast("No routing group selected.");
4616
+ return;
4617
+ }
4618
+ if (!confirm(`Delete routing group \"${groupId}\"?`)) return;
4619
+ try {
4620
+ const queryBase = currentRoutingSourceQuery();
4621
+ const locationPart = UI.routingLocation ? `${queryBase ? "&" : "?"}location=${encodeURIComponent(UI.routingLocation)}` : "";
4622
+ const query = `${queryBase}${locationPart}`;
4623
+ const out = await apiFetch(`/config/routing/groups/${encodeURIComponent(groupId)}${query}`, {
4624
+ method: "DELETE"
4625
+ });
4626
+ UI.routingLocation = out.location || UI.routingLocation;
4627
+ UI.routingGroups = normalizeRoutingGroupsMap(out.groups);
4628
+ UI.routingActiveGroupId = textOf(out.activeGroupId || UI.routingActiveGroupId || "").trim();
4629
+ renderRoutingGroupSelect();
4630
+ loadSelectedRoutingGroupIntoEditor();
4631
+ setLog(
4632
+ "routingOpLog",
4633
+ `Deleted group \"${groupId}\".\nPath: ${out.path || currentRoutingSourcePath() || "—"}\nselected: ${UI.routingSelectedGroupId || "—"}\nactive: ${UI.routingActiveGroupId || "—"}`
4634
+ );
4635
+ toast("Routing group deleted.", "ok");
4636
+ } catch (e) {
4637
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing groups");
4638
+ setLog("routingOpLog", `Delete group failed: ${e && e.message ? e.message : String(e)}`);
4639
+ toast(`Delete group failed: ${e && e.message ? e.message : String(e)}`);
4640
+ }
4641
+ }
4642
+
4387
4643
  async function refreshRoutingSources() {
4388
4644
  const select = $("routingSourceSelect");
4389
4645
  if (!select) return;
@@ -5642,8 +5898,23 @@
5642
5898
  $("saveRoutingBtn").addEventListener("click", saveRouting);
5643
5899
  $("saveRoutingRestartLocalBtn").addEventListener("click", () => void saveRoutingAndRestartLocal());
5644
5900
  $("saveRoutingRestartAllBtn").addEventListener("click", () => void saveRoutingAndRestartAll());
5901
+ $("createRoutingGroupBtn").addEventListener("click", () => void createRoutingGroup());
5902
+ $("deleteRoutingGroupBtn").addEventListener("click", () => void deleteRoutingGroup());
5645
5903
  $("refreshRoutingSourcesBtn").addEventListener("click", refreshRoutingSources);
5646
- $("routingSourceSelect").addEventListener("change", loadRouting);
5904
+ $("routingSourceSelect").addEventListener("change", async () => {
5905
+ if (routingEditorState.dirty && !confirm("Discard unsaved routing editor changes and switch source?")) {
5906
+ return;
5907
+ }
5908
+ await loadRouting();
5909
+ });
5910
+ $("routingGroupSelect").addEventListener("change", () => {
5911
+ if (routingEditorState.dirty && !confirm("Discard unsaved routing editor changes and switch group?")) {
5912
+ renderRoutingGroupSelect(UI.routingSelectedGroupId || UI.routingActiveGroupId || "");
5913
+ return;
5914
+ }
5915
+ UI.routingSelectedGroupId = textOf($("routingGroupSelect").value || "").trim();
5916
+ loadSelectedRoutingGroupIntoEditor();
5917
+ });
5647
5918
  $("routingRegExpandBtn").addEventListener("click", () => setAllDetailsOpen($("routingKvEditor"), true));
5648
5919
  $("routingRegCollapseBtn").addEventListener("click", () => setAllDetailsOpen($("routingKvEditor"), false));
5649
5920
  $("refreshRoutingPoolBtn").addEventListener("click", async () => {
package/dist/index.d.ts CHANGED
@@ -25,6 +25,15 @@ declare class RouteCodexApp {
25
25
  * Get server status
26
26
  */
27
27
  getStatus(): unknown;
28
+ getServerConfig(): {
29
+ host: string;
30
+ port: number;
31
+ } | null;
32
+ restartRuntimeFromDisk(): Promise<{
33
+ reloadedAt: number;
34
+ configPath: string;
35
+ warnings?: string[];
36
+ }>;
28
37
  /**
29
38
  * Detect server port from user configuration
30
39
  */