@jsonstudio/rcc 0.89.1205 → 0.89.1457

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 (391) hide show
  1. package/README.md +53 -1412
  2. package/configsamples/config.json +426 -0
  3. package/configsamples/config.reference.json +58 -0
  4. package/configsamples/provider/crs/config.v1.json +46 -0
  5. package/configsamples/provider/glm/config.v1.json +81 -0
  6. package/configsamples/provider/glm-anthropic/config.v1.json +45 -0
  7. package/configsamples/provider/iflow/config.v1.json +74 -0
  8. package/configsamples/provider/kimi/config.v1.json +41 -0
  9. package/configsamples/provider/lmstudio/config.v1.json +101 -0
  10. package/configsamples/provider/mimo/config.v1.json +35 -0
  11. package/configsamples/provider/modelscope/config.v1.json +96 -0
  12. package/configsamples/provider/qwen/config.v1.json +38 -0
  13. package/configsamples/provider/tab/config.v1.json +50 -0
  14. package/configsamples/provider/tabglm/config.v1.json +49 -0
  15. package/dist/build-info.js +2 -2
  16. package/dist/cli/commands/code.js +12 -6
  17. package/dist/cli/commands/code.js.map +1 -1
  18. package/dist/cli/commands/config.d.ts +2 -1
  19. package/dist/cli/commands/config.js +77 -103
  20. package/dist/cli/commands/config.js.map +1 -1
  21. package/dist/cli/commands/examples.js +6 -6
  22. package/dist/cli/commands/examples.js.map +1 -1
  23. package/dist/cli/commands/init.d.ts +28 -0
  24. package/dist/cli/commands/init.js +94 -0
  25. package/dist/cli/commands/init.js.map +1 -0
  26. package/dist/cli/commands/port.js +10 -2
  27. package/dist/cli/commands/port.js.map +1 -1
  28. package/dist/cli/commands/restart.js +5 -2
  29. package/dist/cli/commands/restart.js.map +1 -1
  30. package/dist/cli/commands/start.js +25 -22
  31. package/dist/cli/commands/start.js.map +1 -1
  32. package/dist/cli/commands/status.js +1 -0
  33. package/dist/cli/commands/status.js.map +1 -1
  34. package/dist/cli/commands/stop.js +1 -0
  35. package/dist/cli/commands/stop.js.map +1 -1
  36. package/dist/cli/config/bundled-docs.d.ts +20 -0
  37. package/dist/cli/config/bundled-docs.js +91 -0
  38. package/dist/cli/config/bundled-docs.js.map +1 -0
  39. package/dist/cli/config/init-config.d.ts +37 -0
  40. package/dist/cli/config/init-config.js +212 -0
  41. package/dist/cli/config/init-config.js.map +1 -0
  42. package/dist/cli/config/init-provider-catalog.d.ts +8 -0
  43. package/dist/cli/config/init-provider-catalog.js +187 -0
  44. package/dist/cli/config/init-provider-catalog.js.map +1 -0
  45. package/dist/cli/register/init-command.d.ts +3 -0
  46. package/dist/cli/register/init-command.js +5 -0
  47. package/dist/cli/register/init-command.js.map +1 -0
  48. package/dist/cli.js +28 -3
  49. package/dist/cli.js.map +1 -1
  50. package/dist/client/gemini/gemini-protocol-client.js +2 -1
  51. package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
  52. package/dist/client/gemini-cli/gemini-cli-protocol-client.js +40 -16
  53. package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
  54. package/dist/client/openai/chat-protocol-client.js +2 -1
  55. package/dist/client/openai/chat-protocol-client.js.map +1 -1
  56. package/dist/client/responses/responses-protocol-client.js +2 -1
  57. package/dist/client/responses/responses-protocol-client.js.map +1 -1
  58. package/dist/config/risk-control-config.d.ts +94 -0
  59. package/dist/config/risk-control-config.js +196 -0
  60. package/dist/config/risk-control-config.js.map +1 -0
  61. package/dist/constants/index.d.ts +6 -0
  62. package/dist/constants/index.js +13 -0
  63. package/dist/constants/index.js.map +1 -1
  64. package/dist/docs/daemon-admin-ui.html +2113 -190
  65. package/dist/error-handling/quiet-error-handling-center.js +46 -8
  66. package/dist/error-handling/quiet-error-handling-center.js.map +1 -1
  67. package/dist/index.js +0 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/manager/modules/health/index.d.ts +1 -1
  70. package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +70 -0
  71. package/dist/manager/modules/quota/antigravity-quota-manager.js +442 -0
  72. package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -0
  73. package/dist/manager/modules/quota/index.d.ts +3 -127
  74. package/dist/manager/modules/quota/index.js +2 -1093
  75. package/dist/manager/modules/quota/index.js.map +1 -1
  76. package/dist/manager/modules/quota/provider-key-normalization.d.ts +3 -0
  77. package/dist/manager/modules/quota/provider-key-normalization.js +155 -0
  78. package/dist/manager/modules/quota/provider-key-normalization.js.map +1 -0
  79. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +9 -0
  80. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +115 -0
  81. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -0
  82. package/dist/manager/modules/quota/provider-quota-daemon.d.ts +77 -0
  83. package/dist/manager/modules/quota/provider-quota-daemon.events.d.ts +12 -0
  84. package/dist/manager/modules/quota/provider-quota-daemon.events.js +239 -0
  85. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -0
  86. package/dist/manager/modules/quota/provider-quota-daemon.js +404 -0
  87. package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -0
  88. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +11 -0
  89. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +192 -0
  90. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -0
  91. package/dist/manager/modules/quota/provider-quota-daemon.snapshot.d.ts +8 -0
  92. package/dist/manager/modules/quota/provider-quota-daemon.snapshot.js +96 -0
  93. package/dist/manager/modules/quota/provider-quota-daemon.snapshot.js.map +1 -0
  94. package/dist/manager/modules/quota/provider-quota-daemon.view.d.ts +19 -0
  95. package/dist/manager/modules/quota/provider-quota-daemon.view.js +37 -0
  96. package/dist/manager/modules/quota/provider-quota-daemon.view.js.map +1 -0
  97. package/dist/manager/modules/routing/index.d.ts +1 -0
  98. package/dist/manager/modules/routing/index.js +11 -25
  99. package/dist/manager/modules/routing/index.js.map +1 -1
  100. package/dist/manager/quota/provider-quota-center.d.ts +2 -0
  101. package/dist/manager/quota/provider-quota-center.js +80 -82
  102. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  103. package/dist/modules/llmswitch/bridge.d.ts +16 -18
  104. package/dist/modules/llmswitch/bridge.js +293 -94
  105. package/dist/modules/llmswitch/bridge.js.map +1 -1
  106. package/dist/modules/llmswitch/core-loader.d.ts +4 -2
  107. package/dist/modules/llmswitch/core-loader.js +32 -20
  108. package/dist/modules/llmswitch/core-loader.js.map +1 -1
  109. package/dist/modules/pipeline/utils/colored-logger.js +3 -2
  110. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
  111. package/dist/modules/pipeline/utils/debug-logger.js +1 -1
  112. package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
  113. package/dist/providers/auth/antigravity-userinfo-helper.d.ts +2 -1
  114. package/dist/providers/auth/antigravity-userinfo-helper.js +25 -4
  115. package/dist/providers/auth/antigravity-userinfo-helper.js.map +1 -1
  116. package/dist/providers/auth/iflow-cookie-auth.js +0 -2
  117. package/dist/providers/auth/iflow-cookie-auth.js.map +1 -1
  118. package/dist/providers/auth/oauth-lifecycle.js +2 -23
  119. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  120. package/dist/providers/auth/tokenfile-auth.d.ts +2 -0
  121. package/dist/providers/auth/tokenfile-auth.js +33 -1
  122. package/dist/providers/auth/tokenfile-auth.js.map +1 -1
  123. package/dist/providers/core/config/camoufox-launcher.d.ts +5 -0
  124. package/dist/providers/core/config/camoufox-launcher.js +40 -4
  125. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  126. package/dist/providers/core/config/service-profiles.js +7 -18
  127. package/dist/providers/core/config/service-profiles.js.map +1 -1
  128. package/dist/providers/core/runtime/antigravity-quota-client.js +6 -3
  129. package/dist/providers/core/runtime/antigravity-quota-client.js.map +1 -1
  130. package/dist/providers/core/runtime/base-provider.d.ts +2 -7
  131. package/dist/providers/core/runtime/base-provider.js +84 -165
  132. package/dist/providers/core/runtime/base-provider.js.map +1 -1
  133. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +7 -0
  134. package/dist/providers/core/runtime/gemini-cli-http-provider.js +368 -97
  135. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  136. package/dist/providers/core/runtime/http-request-executor.d.ts +3 -0
  137. package/dist/providers/core/runtime/http-request-executor.js +110 -38
  138. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  139. package/dist/providers/core/runtime/http-transport-provider.d.ts +17 -0
  140. package/dist/providers/core/runtime/http-transport-provider.js +165 -16
  141. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  142. package/dist/providers/core/runtime/provider-error-classifier.js +10 -0
  143. package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
  144. package/dist/providers/core/runtime/provider-factory.js +7 -5
  145. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  146. package/dist/providers/core/runtime/provider-runtime-metadata.d.ts +6 -0
  147. package/dist/providers/core/runtime/provider-runtime-metadata.js.map +1 -1
  148. package/dist/providers/core/runtime/rate-limit-manager.d.ts +1 -12
  149. package/dist/providers/core/runtime/rate-limit-manager.js +4 -77
  150. package/dist/providers/core/runtime/rate-limit-manager.js.map +1 -1
  151. package/dist/providers/core/runtime/responses-provider.d.ts +1 -7
  152. package/dist/providers/core/runtime/responses-provider.js +12 -93
  153. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  154. package/dist/providers/core/strategies/oauth-auth-code-flow.js +12 -8
  155. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  156. package/dist/providers/core/utils/http-client.js +36 -46
  157. package/dist/providers/core/utils/http-client.js.map +1 -1
  158. package/dist/providers/core/utils/provider-error-logger.d.ts +1 -1
  159. package/dist/providers/core/utils/provider-error-reporter.d.ts +3 -1
  160. package/dist/providers/core/utils/provider-error-reporter.js +3 -0
  161. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  162. package/dist/providers/core/utils/snapshot-writer.js +1 -4
  163. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  164. package/dist/providers/mock/mock-provider-runtime.js +57 -27
  165. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  166. package/dist/scripts/camoufox/launch-auth.mjs +193 -58
  167. package/dist/server/handlers/handler-utils.js +8 -3
  168. package/dist/server/handlers/handler-utils.js.map +1 -1
  169. package/dist/server/handlers/responses-handler.js +1 -1
  170. package/dist/server/handlers/responses-handler.js.map +1 -1
  171. package/dist/server/runtime/http-server/daemon-admin/auth-handler.d.ts +2 -0
  172. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +103 -0
  173. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -0
  174. package/dist/server/runtime/http-server/daemon-admin/auth-session.d.ts +5 -0
  175. package/dist/server/runtime/http-server/daemon-admin/auth-session.js +77 -0
  176. package/dist/server/runtime/http-server/daemon-admin/auth-session.js.map +1 -0
  177. package/dist/server/runtime/http-server/daemon-admin/auth-store.d.ts +18 -0
  178. package/dist/server/runtime/http-server/daemon-admin/auth-store.js +89 -0
  179. package/dist/server/runtime/http-server/daemon-admin/auth-store.js.map +1 -0
  180. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +1 -2
  181. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  182. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +226 -24
  183. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  184. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +47 -8
  185. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  186. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +1 -1
  187. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -1
  188. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +1 -1
  189. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
  190. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +68 -4
  191. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  192. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +3 -4
  193. package/dist/server/runtime/http-server/daemon-admin-routes.js +9 -14
  194. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  195. package/dist/server/runtime/http-server/executor-metadata.js +1 -1
  196. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  197. package/dist/server/runtime/http-server/executor-response.js +0 -16
  198. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  199. package/dist/server/runtime/http-server/hub-shadow-compare.js +110 -34
  200. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  201. package/dist/server/runtime/http-server/index.d.ts +5 -3
  202. package/dist/server/runtime/http-server/index.js +281 -136
  203. package/dist/server/runtime/http-server/index.js.map +1 -1
  204. package/dist/server/runtime/http-server/middleware.js +19 -1
  205. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  206. package/dist/server/runtime/http-server/request-executor.js +59 -24
  207. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  208. package/dist/server/runtime/http-server/routes.js +12 -3
  209. package/dist/server/runtime/http-server/routes.js.map +1 -1
  210. package/dist/server/runtime/http-server/session-dir.d.ts +2 -0
  211. package/dist/server/runtime/http-server/session-dir.js +59 -0
  212. package/dist/server/runtime/http-server/session-dir.js.map +1 -0
  213. package/dist/server/runtime/http-server/types.d.ts +0 -4
  214. package/dist/server/utils/utf8-chunk-buffer.js +6 -3
  215. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -1
  216. package/dist/server/utils/warmup-storm-tracker.js +1 -1
  217. package/dist/server/utils/warmup-storm-tracker.js.map +1 -1
  218. package/dist/server-factory.d.ts +6 -28
  219. package/dist/server-factory.js +8 -93
  220. package/dist/server-factory.js.map +1 -1
  221. package/dist/token-daemon/index.js +2 -2
  222. package/dist/token-daemon/index.js.map +1 -1
  223. package/dist/token-daemon/provider-registry.js +0 -1
  224. package/dist/token-daemon/provider-registry.js.map +1 -1
  225. package/dist/token-daemon/server-utils.js +8 -9
  226. package/dist/token-daemon/server-utils.js.map +1 -1
  227. package/dist/token-daemon/token-utils.js +1 -1
  228. package/dist/token-daemon/token-utils.js.map +1 -1
  229. package/dist/tools/semantic-replay.js +2 -2
  230. package/dist/tools/semantic-replay.js.map +1 -1
  231. package/dist/tools/stats-request-events.d.ts +1 -1
  232. package/dist/tools/stats-usage.js +6 -3
  233. package/dist/tools/stats-usage.js.map +1 -1
  234. package/dist/utils/llms-engine-shadow.d.ts +19 -0
  235. package/dist/utils/llms-engine-shadow.js +209 -0
  236. package/dist/utils/llms-engine-shadow.js.map +1 -0
  237. package/dist/utils/runtime-versions.js +2 -1
  238. package/dist/utils/runtime-versions.js.map +1 -1
  239. package/dist/utils/strip-internal-keys.d.ts +12 -0
  240. package/dist/utils/strip-internal-keys.js +28 -0
  241. package/dist/utils/strip-internal-keys.js.map +1 -0
  242. package/docs/ARCHITECTURE.md +402 -0
  243. package/docs/CHAT_PROCESS_PROTOCOL_AND_PIPELINE.md +221 -0
  244. package/docs/CODEX_AND_CLAUDE_CODE.md +69 -0
  245. package/docs/CONFIG_ARCHITECTURE.md +517 -0
  246. package/docs/ERROR_HANDLING_AUDIT.md +0 -0
  247. package/docs/GCLI2API_PARITY_GAPS.md +98 -0
  248. package/docs/INSTALLATION_AND_QUICKSTART.md +74 -0
  249. package/docs/INSTRUCTION_MARKUP.md +89 -0
  250. package/docs/MODULE_ENHANCEMENT_SYSTEM.md +666 -0
  251. package/docs/PORTS.md +36 -0
  252. package/docs/PROVIDERS_BUILTIN.md +111 -0
  253. package/docs/PROVIDER_TYPES.md +55 -0
  254. package/docs/SERVERTOOL_CLOCK_DESIGN.md +233 -0
  255. package/docs/USAGE_HANDLING_ANALYSIS.md +335 -0
  256. package/docs/USER_CONFIG_PARSER_CHANGES.md +175 -0
  257. package/docs/V3_INBOUND_OUTBOUND_DESIGN.md +86 -0
  258. package/docs/VIRTUAL_ROUTER_PRIORITY_AND_HEALTH.md +125 -0
  259. package/docs/anthropic-request-golden-samples.md +50 -0
  260. package/docs/antigravity-gemini-format-cleanup.md +102 -0
  261. package/docs/antigravity-routing-contract.md +31 -0
  262. package/docs/ccr-alignment-enhancetool.md +105 -0
  263. package/docs/chat-glm-500-analysis.md +79 -0
  264. package/docs/chat-request-golden-samples.md +42 -0
  265. package/docs/chat-semantic-expansion-plan.md +84 -0
  266. package/docs/cli-command-inventory.md +76 -0
  267. package/docs/codex-samples-replay.md +50 -0
  268. package/docs/daemon-admin-api-design.md +350 -0
  269. package/docs/daemon-admin-module-structure.md +169 -0
  270. package/docs/daemon-admin-ui.html +3394 -0
  271. package/docs/debug-system-design.md +734 -0
  272. package/docs/debugging/gemini-sse-root-cause.md +52 -0
  273. package/docs/debugging/sse_encoding_failure_analysis.md +53 -0
  274. package/docs/dry-run/README.md +721 -0
  275. package/docs/error-handling-v2.md +92 -0
  276. package/docs/exec-command-guard-policy.example.v1.json +42 -0
  277. package/docs/fixes/gemini-protocol-mapping.md +57 -0
  278. package/docs/fixes/oauth-portal-timing-fix.md +202 -0
  279. package/docs/fixes/web-search-hop3-fix.md +265 -0
  280. package/docs/glm-api-reference.md +390 -0
  281. package/docs/glm-chat-completions.md +1779 -0
  282. package/docs/glm-history-inline-images.md +44 -0
  283. package/docs/golden-ci-library.md +66 -0
  284. package/docs/lmstudio-dry-run-summary.md +203 -0
  285. package/docs/lmstudio-tool-calling.md +214 -0
  286. package/docs/mapping-tables/anthropic-to-openai.json +290 -0
  287. package/docs/mapping-tables/iflow-to-openai.json +215 -0
  288. package/docs/mapping-tables/openai-passthrough.json +190 -0
  289. package/docs/mapping-tables/openai-to-iflow.json +227 -0
  290. package/docs/monitoring/Design.md +61 -0
  291. package/docs/multi-token-auth-guide.md +66 -0
  292. package/docs/oauth-authentication-guide.md +168 -0
  293. package/docs/oauth-iflow-implementation.md +153 -0
  294. package/docs/pipeline-routing-report.md +209 -0
  295. package/docs/plans/manager-daemon/PLAN.md +86 -0
  296. package/docs/plans/provider-config-v2-plan.md +176 -0
  297. package/docs/plans/provider-runtime-manager-plan.md +209 -0
  298. package/docs/plans/transparent-429-failover.md +89 -0
  299. package/docs/plans/unified-hub-framework-v1.md +245 -0
  300. package/docs/provider-config-v2-ui-design.md +181 -0
  301. package/docs/provider-quota-design.md +129 -0
  302. package/docs/providers/gemini-provider.md +62 -0
  303. package/docs/providers/lmstudio-v2-migration-report.md +102 -0
  304. package/docs/providers/provider-composite-design.md +142 -0
  305. package/docs/providers/provider-composite-testing.md +98 -0
  306. package/docs/providers/provider-type-only-migration.md +111 -0
  307. package/docs/rccx-wasm-migration.md +74 -0
  308. package/docs/refactoring/architecture-comparison-diagram.md +140 -0
  309. package/docs/refactoring/compatibility-v2-architecture-design.md +738 -0
  310. package/docs/refactoring/workflow-compatibility-refactoring-design.md +361 -0
  311. package/docs/reports/routing-classification-report.json +24 -0
  312. package/docs/reports/routing-classification-report.md +18 -0
  313. package/docs/reports/thinking-keywords-report.json +19 -0
  314. package/docs/responses/README.md +156 -0
  315. package/docs/responses-generic-provider.md +86 -0
  316. package/docs/responses-passthrough-provider-design.md +202 -0
  317. package/docs/routing-awrr-health-weighted-round-robin.md +179 -0
  318. package/docs/routing-instructions.md +393 -0
  319. package/docs/servertool-framework.md +65 -0
  320. package/docs/stop-message-auto.md +225 -0
  321. package/docs/streaming-flow.html +30 -0
  322. package/docs/streaming-flow.md +182 -0
  323. package/docs/token-daemon-preview.html +490 -0
  324. package/docs/token-refresh-daemon-plan.md +269 -0
  325. package/docs/transformation-tables/Gemini-FinishReason/345/256/214/346/225/264/350/275/254/346/215/242/350/241/250.json +233 -0
  326. package/docs/transformation-tables/README.md +225 -0
  327. package/docs/transformation-tables/claude-code-router-anthropic-to-gemini.json +283 -0
  328. package/docs/transformation-tables/claude-code-router-anthropic-to-openai.json +208 -0
  329. package/docs/transformation-tables/claude-code-router-openai-to-anthropic.json +261 -0
  330. package/docs/transformation-tables/claude-code-router-openai-to-gemini.json +208 -0
  331. package/docs/transformation-tables/claude-code-router-openai-to-lmstudio.json +182 -0
  332. package/docs/transformation-tables/claude-code-router-openai-to-ollama.json +250 -0
  333. package/docs/transformation-tables/claude-code-router-openai-to-textgenwebui.json +295 -0
  334. package/docs/transformation-tables/claude-code-router-provider-conversions.json +193 -0
  335. package/docs/transformation-tables//345/256/214/346/225/264/347/232/204/345/267/245/345/205/267/346/211/247/350/241/214/346/265/201/347/250/213/350/275/254/346/215/242/350/241/250.json +299 -0
  336. package/docs/transformation-tables//345/257/271/350/257/235/345/216/206/345/217/262/347/273/264/346/212/244/345/210/206/346/236/220.md +134 -0
  337. package/docs/transformation-tables//345/267/245/345/205/267/350/260/203/347/224/250/346/250/241/345/274/217/345/210/206/346/236/220.md +158 -0
  338. package/docs/transformation-tables//347/212/266/346/200/201/347/256/241/347/220/206/351/234/200/346/261/202/345/210/206/346/236/220.md +175 -0
  339. package/docs/transformation-tables//351/235/231/346/200/201/350/241/250vs/345/212/250/346/200/201/345/210/206/346/236/220.md +189 -0
  340. package/docs/transformation-tables//351/235/231/346/200/201/350/241/250/345/207/206/347/241/256/346/200/247/350/257/204/344/274/260.md +179 -0
  341. package/docs/transformation-tables//351/235/236/346/265/201/345/274/217/345/234/272/346/231/257/345/210/206/346/236/220.md +189 -0
  342. package/docs/v2-architecture/IMPLEMENTATION-ROADMAP.md +367 -0
  343. package/docs/v2-architecture/OPTIMIZED-DESIGN.md +827 -0
  344. package/docs/v2-architecture/PRERUN-CONNECTION-DESIGN.md +716 -0
  345. package/docs/v2-architecture/README.md +549 -0
  346. package/docs/verification/modelscope-verify.md +59 -0
  347. package/docs/verified-configs/README.md +60 -0
  348. package/docs/verified-configs/v0.45.0/README.md +244 -0
  349. package/docs/verified-configs/v0.45.0/lmstudio-5521-gpt-oss-20b-mlx.json +135 -0
  350. package/docs/verified-configs/v0.45.0/merged-config.5521.json +1205 -0
  351. package/docs/verified-configs/v0.45.0/merged-config.qwen-5522.json +1559 -0
  352. package/docs/verified-configs/v0.45.0/qwen-5522-qwen3-coder-plus-final.json +221 -0
  353. package/docs/verified-configs/v0.45.0/qwen-5522-qwen3-coder-plus-fixed.json +242 -0
  354. package/docs/verified-configs/v0.45.0/qwen-5522-qwen3-coder-plus.json +242 -0
  355. package/docs/web-search-service-design.md +322 -0
  356. package/package.json +26 -15
  357. package/scripts/build-core.mjs +3 -1
  358. package/scripts/camoufox/launch-auth.mjs +193 -58
  359. package/scripts/ci/repo-sanity.mjs +138 -0
  360. package/scripts/mock-provider/run-regressions.mjs +157 -1
  361. package/scripts/monitor-diff.mjs +126 -0
  362. package/scripts/pack-mode.mjs +19 -1
  363. package/scripts/pack-rcc.mjs +63 -0
  364. package/scripts/run-bg.sh +0 -14
  365. package/scripts/tests/ci-jest.mjs +119 -0
  366. package/scripts/tools-dev/responses-debug-client/README.md +23 -0
  367. package/scripts/tools-dev/responses-debug-client/payloads/poem.json +13 -0
  368. package/scripts/tools-dev/responses-debug-client/payloads/sample-no-tools.json +98 -0
  369. package/scripts/tools-dev/responses-debug-client/payloads/text.json +13 -0
  370. package/scripts/tools-dev/responses-debug-client/payloads/tool.json +27 -0
  371. package/scripts/tools-dev/responses-debug-client/run.mjs +65 -0
  372. package/scripts/tools-dev/responses-debug-client/src/index.ts +281 -0
  373. package/scripts/tools-dev/run-llmswitch-chat.mjs +53 -0
  374. package/scripts/tools-dev/server-tools-dev/run-web-fetch.mjs +65 -0
  375. package/scripts/unified-hub-shadow-compare.mjs +33 -13
  376. package/scripts/vendor-core.mjs +13 -3
  377. package/scripts/verify-e2e-toolcall.mjs +115 -26
  378. package/dist/modules/llmswitch/pipeline-registry.d.ts +0 -57
  379. package/dist/modules/llmswitch/pipeline-registry.js +0 -229
  380. package/dist/modules/llmswitch/pipeline-registry.js.map +0 -1
  381. package/dist/server/RouteCodexServer.d.ts +0 -13
  382. package/dist/server/RouteCodexServer.js +0 -25
  383. package/dist/server/RouteCodexServer.js.map +0 -1
  384. package/dist/v2/conversion/hub/snapshot-recorder.d.ts +0 -12
  385. package/dist/v2/conversion/hub/snapshot-recorder.js +0 -22
  386. package/dist/v2/conversion/hub/snapshot-recorder.js.map +0 -1
  387. package/scripts/test-fc-responses.mjs +0 -66
  388. package/scripts/test-guidance.mjs +0 -100
  389. package/scripts/test-iflow-web-search.mjs +0 -141
  390. package/scripts/test-iflow.mjs +0 -379
  391. package/scripts/test-tool-exec.mjs +0 -26
@@ -85,6 +85,194 @@
85
85
  color: var(--muted);
86
86
  }
87
87
 
88
+ .toast {
89
+ position: fixed;
90
+ right: 14px;
91
+ bottom: 14px;
92
+ max-width: 520px;
93
+ padding: 10px 12px;
94
+ border-radius: 10px;
95
+ border: 1px solid var(--border);
96
+ background: rgba(20, 20, 20, 0.92);
97
+ color: var(--fg);
98
+ box-shadow: 0 12px 34px rgba(0,0,0,0.45);
99
+ z-index: 99999;
100
+ font-size: 13px;
101
+ line-height: 1.35;
102
+ display: none;
103
+ white-space: pre-wrap;
104
+ word-break: break-word;
105
+ }
106
+ .toast.show { display: block; }
107
+ .toast.ok { border-color: rgba(38, 200, 120, 0.45); }
108
+ .toast.err { border-color: rgba(255, 90, 90, 0.55); }
109
+
110
+ .kv {
111
+ border: 1px solid rgba(255, 255, 255, 0.10);
112
+ border-radius: 12px;
113
+ padding: 8px 10px;
114
+ background: rgba(0, 0, 0, 0.18);
115
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
116
+ font-size: 12px;
117
+ line-height: 1.45;
118
+ color: rgba(255, 255, 255, 0.86);
119
+ overflow: visible;
120
+ max-height: none;
121
+ }
122
+
123
+ .kv details {
124
+ border-left: 1px solid rgba(255, 255, 255, 0.10);
125
+ margin-left: 10px;
126
+ padding-left: 10px;
127
+ }
128
+
129
+ .kv summary {
130
+ cursor: pointer;
131
+ list-style: none;
132
+ user-select: none;
133
+ display: flex;
134
+ gap: 10px;
135
+ align-items: baseline;
136
+ padding: 2px 0;
137
+ }
138
+
139
+ .kv summary::-webkit-details-marker { display: none; }
140
+
141
+ .kv .kv-key {
142
+ color: rgba(255, 255, 255, 0.92);
143
+ min-width: 180px;
144
+ max-width: 520px;
145
+ overflow: hidden;
146
+ text-overflow: ellipsis;
147
+ white-space: nowrap;
148
+ }
149
+
150
+ .kv .kv-meta {
151
+ color: rgba(255, 255, 255, 0.55);
152
+ flex: 1;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ white-space: nowrap;
156
+ }
157
+
158
+ .kv .kv-leaf {
159
+ display: flex;
160
+ gap: 10px;
161
+ padding: 2px 0;
162
+ }
163
+
164
+ .kv .kv-val {
165
+ color: rgba(255, 255, 255, 0.78);
166
+ flex: 1;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .kv-tools {
173
+ display: flex;
174
+ gap: 8px;
175
+ flex-wrap: wrap;
176
+ margin: 0 0 8px;
177
+ }
178
+
179
+ .kv-editor .kv-leaf,
180
+ .kv-editor summary {
181
+ align-items: center;
182
+ }
183
+
184
+ .kv-editor .kv-type {
185
+ color: rgba(255, 255, 255, 0.55);
186
+ min-width: 92px;
187
+ max-width: 140px;
188
+ overflow: hidden;
189
+ text-overflow: ellipsis;
190
+ white-space: nowrap;
191
+ }
192
+
193
+ .kv-editor .kv-actions {
194
+ display: inline-flex;
195
+ gap: 6px;
196
+ margin-left: auto;
197
+ flex: 0 0 auto;
198
+ }
199
+
200
+ .kv-editor .kv-actions button {
201
+ padding: 4px 8px;
202
+ border-radius: 8px;
203
+ font-size: 12px;
204
+ }
205
+
206
+ .kv-editor input[type="text"],
207
+ .kv-editor input[type="number"],
208
+ .kv-editor select {
209
+ padding: 4px 6px;
210
+ border-radius: 8px;
211
+ font-size: 12px;
212
+ }
213
+
214
+ .kv-editor .kv-path {
215
+ color: rgba(255, 255, 255, 0.42);
216
+ margin-left: 6px;
217
+ }
218
+
219
+ #routingKvEditor.kv-editor .kv-val {
220
+ white-space: normal;
221
+ overflow: visible;
222
+ text-overflow: unset;
223
+ }
224
+
225
+ #routingKvEditor.kv-editor .kv-actions .kv-path {
226
+ display: none;
227
+ }
228
+
229
+ body.show-routing-paths #routingKvEditor.kv-editor .kv-actions .kv-path {
230
+ display: inline-flex;
231
+ }
232
+
233
+ #routingKvEditor.kv-editor .kv-actions button.danger {
234
+ display: none;
235
+ }
236
+
237
+ #routingKvEditor.kv-editor .kv-leaf:hover .kv-actions button.danger,
238
+ #routingKvEditor.kv-editor summary:hover .kv-actions button.danger {
239
+ display: inline-flex;
240
+ }
241
+
242
+ #routingKvEditor .routing-target-actions {
243
+ margin-top: 6px;
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: 6px;
247
+ }
248
+
249
+ #routingKvEditor .routing-target-row {
250
+ display: flex;
251
+ gap: 8px;
252
+ flex-wrap: wrap;
253
+ align-items: center;
254
+ }
255
+
256
+ #routingKvEditor .routing-target-key {
257
+ padding: 2px 6px;
258
+ border: 1px solid rgba(255, 255, 255, 0.12);
259
+ border-radius: 999px;
260
+ background: rgba(255, 255, 255, 0.04);
261
+ max-width: 520px;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap;
265
+ }
266
+
267
+ #routingKvEditor .routing-target-meta {
268
+ color: rgba(255, 255, 255, 0.55);
269
+ }
270
+
271
+ #routingKvEditor .routing-target-row button {
272
+ padding: 4px 8px;
273
+ border-radius: 8px;
274
+ font-size: 12px;
275
+ }
88
276
  .dot {
89
277
  width: 8px;
90
278
  height: 8px;
@@ -184,6 +372,14 @@
184
372
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
185
373
  }
186
374
 
375
+ .grid.grid-wide-right {
376
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
377
+ }
378
+
379
+ .grid.grid-one {
380
+ grid-template-columns: minmax(0, 1fr);
381
+ }
382
+
187
383
  /* Ensure grid children can shrink without overflowing into the next column */
188
384
  .grid > .card {
189
385
  min-width: 0;
@@ -230,12 +426,53 @@
230
426
  background: rgba(255, 255, 255, 0.03);
231
427
  }
232
428
 
429
+ .table.table-runtime th:nth-child(1),
430
+ .table.table-runtime td:nth-child(1) {
431
+ width: 22%;
432
+ }
433
+ .table.table-runtime th:nth-child(2),
434
+ .table.table-runtime td:nth-child(2) {
435
+ width: 19%;
436
+ }
437
+ .table.table-runtime th:nth-child(3),
438
+ .table.table-runtime td:nth-child(3) {
439
+ width: 9%;
440
+ }
441
+ .table.table-runtime th:nth-child(4),
442
+ .table.table-runtime td:nth-child(4) {
443
+ width: 12%;
444
+ }
445
+ .table.table-runtime th:nth-child(5),
446
+ .table.table-runtime td:nth-child(5) {
447
+ width: 12%;
448
+ }
449
+ .table.table-runtime th:nth-child(6),
450
+ .table.table-runtime td:nth-child(6) {
451
+ width: 7%;
452
+ }
453
+ .table.table-runtime th:nth-child(7),
454
+ .table.table-runtime td:nth-child(7) {
455
+ width: 11%;
456
+ }
457
+ .table.table-runtime th:nth-child(8),
458
+ .table.table-runtime td:nth-child(8) {
459
+ width: 8%;
460
+ }
461
+
233
462
  .table tr.group-row td {
234
463
  background: rgba(255, 255, 255, 0.02);
235
464
  color: rgba(255, 255, 255, 0.86);
236
465
  font-weight: 650;
237
466
  }
238
467
 
468
+ .table tr.provider-row:hover td {
469
+ background: rgba(78, 161, 255, 0.06);
470
+ }
471
+
472
+ .table tr.provider-row.selected td {
473
+ background: rgba(78, 161, 255, 0.12);
474
+ }
475
+
239
476
  .indent {
240
477
  padding-left: 22px !important;
241
478
  }
@@ -243,14 +480,14 @@
243
480
  .table-wrap {
244
481
  width: 100%;
245
482
  max-width: 100%;
246
- overflow: auto;
483
+ overflow: visible;
247
484
  border-radius: 12px;
248
485
  border: 1px solid var(--border);
249
486
  }
250
487
 
251
488
  .table-wrap .table {
252
489
  border: 0;
253
- min-width: 860px;
490
+ min-width: 0;
254
491
  }
255
492
 
256
493
  .table td.actions-cell {
@@ -278,6 +515,39 @@
278
515
  word-break: break-all;
279
516
  }
280
517
 
518
+ .pill {
519
+ display: inline-block;
520
+ padding: 2px 8px;
521
+ border-radius: 999px;
522
+ border: 1px solid rgba(255, 255, 255, 0.12);
523
+ background: rgba(255, 255, 255, 0.04);
524
+ font-size: 11px;
525
+ line-height: 1.4;
526
+ white-space: nowrap;
527
+ }
528
+
529
+ .pill.ok {
530
+ border-color: rgba(52, 211, 153, 0.35);
531
+ background: rgba(52, 211, 153, 0.10);
532
+ color: rgba(210, 255, 236, 0.92);
533
+ }
534
+
535
+ .pill.warn {
536
+ border-color: rgba(255, 181, 71, 0.35);
537
+ background: rgba(255, 181, 71, 0.10);
538
+ color: rgba(255, 236, 210, 0.92);
539
+ }
540
+
541
+ .pill.bad {
542
+ border-color: rgba(239, 68, 68, 0.35);
543
+ background: rgba(239, 68, 68, 0.10);
544
+ color: rgba(255, 220, 220, 0.92);
545
+ }
546
+
547
+ .muted {
548
+ color: var(--muted);
549
+ }
550
+
281
551
  .notice {
282
552
  border: 1px solid rgba(255, 181, 71, 0.35);
283
553
  background: rgba(255, 181, 71, 0.08);
@@ -327,9 +597,8 @@
327
597
  <div class="title">
328
598
  <h1>RouteCodex Daemon Admin</h1>
329
599
  <p>
330
- Writes to <span class="mono">~/.routecodex/config.json</span>. If
331
- <span class="mono">httpserver.apikey</span> is configured, remote access is allowed
332
- with the key; otherwise this page is localhost-only.
600
+ Writes to <span class="mono">~/.routecodex/config.json</span>. This UI requires an admin password:
601
+ first visit will ask you to set one (stored at <span class="mono">~/.routecodex/login</span>), then you login with it.
333
602
  </p>
334
603
  </div>
335
604
  <div class="statusline">
@@ -340,7 +609,45 @@
340
609
  </header>
341
610
 
342
611
  <div class="row" style="margin-bottom: 10px;">
343
- <label for="apiKeyInput">Server API Key (optional)</label>
612
+ <label for="adminPasswordInput">Admin password</label>
613
+ <input
614
+ id="adminPasswordInput"
615
+ type="password"
616
+ placeholder="set (first time) / login"
617
+ style="flex: 1; min-width: 260px;"
618
+ />
619
+ <button id="setupPasswordBtn" class="primary">Set</button>
620
+ <button id="loginPasswordBtn" class="primary">Login</button>
621
+ <button id="logoutPasswordBtn">Logout</button>
622
+ <span class="mono" id="adminAuthHint"></span>
623
+ </div>
624
+
625
+ <details id="changePasswordDetails" style="margin: 0 0 10px; display:none;">
626
+ <summary class="muted" style="font-size:12px; cursor:pointer; user-select:none;">Change admin password</summary>
627
+ <div class="row" style="margin-top: 10px;">
628
+ <label for="oldAdminPasswordInput">old</label>
629
+ <input
630
+ id="oldAdminPasswordInput"
631
+ type="password"
632
+ placeholder="old password"
633
+ style="flex: 1; min-width: 240px;"
634
+ />
635
+ <label for="newAdminPasswordInput">new</label>
636
+ <input
637
+ id="newAdminPasswordInput"
638
+ type="password"
639
+ placeholder="new password (8+ chars)"
640
+ style="flex: 1; min-width: 240px;"
641
+ />
642
+ <button id="changePasswordBtn" class="primary">Change</button>
643
+ </div>
644
+ <div class="muted" style="font-size:12px; margin-top: 6px;">
645
+ Localhost-only. Requires current authenticated session.
646
+ </div>
647
+ </details>
648
+
649
+ <div class="row" style="margin-bottom: 10px;">
650
+ <label for="apiKeyInput">Server API key (optional, for /v1/* tests)</label>
344
651
  <input
345
652
  id="apiKeyInput"
346
653
  type="password"
@@ -393,11 +700,11 @@
393
700
  <div class="card" style="box-shadow: none;">
394
701
  <p class="section-title" id="providerEditorTitle">Provider editor</p>
395
702
  <p class="section-sub">
396
- Edit provider JSON (secrets are not allowed inline). Save writes to disk and creates a backup.
703
+ Edit providers as key/value entries to avoid JSON syntax errors. Save writes to disk and creates a backup.
397
704
  </p>
398
705
 
399
706
  <div class="notice" style="margin-bottom: 10px;">
400
- If you use <span class="mono">httpserver.apikey</span>, set it above so API calls don’t return 401. Without apikey configured, this page is localhost-only.
707
+ Admin API calls use the password login above (cookie session). The optional server API key is only needed for testing proxy endpoints like <span class="mono">/v1/responses</span>.
401
708
  </div>
402
709
 
403
710
  <div class="row" style="margin-bottom: 10px;">
@@ -473,11 +780,14 @@
473
780
  </div>
474
781
  </div>
475
782
 
476
- <textarea
477
- id="providerJsonEditor"
478
- spellcheck="false"
479
- placeholder="{\n \"enabled\": true,\n \"type\": \"responses\",\n \"baseURL\": \"https://...\",\n \"auth\": { \"type\": \"apikey\", \"apiKey\": \"authfile-...\" }\n}"
480
- ></textarea>
783
+ <p class="section-title" style="margin-top: 10px;">Provider config</p>
784
+ <p class="section-sub">Tree editor. Use “Apply preset” to populate common templates.</p>
785
+ <div class="kv-tools">
786
+ <button id="providerEditorExpandBtn">Expand all</button>
787
+ <button id="providerEditorCollapseBtn">Collapse all</button>
788
+ <span class="mono" id="providerEditorDirtyHint"></span>
789
+ </div>
790
+ <div id="providerKvEditor" class="kv kv-editor"></div>
481
791
 
482
792
  <div class="row" style="margin-top: 10px;">
483
793
  <button id="loadProviderBtn">Load</button>
@@ -593,21 +903,55 @@
593
903
  <p class="section-sub">
594
904
  VirtualRouter consumes this via <span class="mono">quotaView</span>. When
595
905
  <span class="mono">inPool=false</span>, the provider is treated as removed from the route pool.
596
- </p>
597
- <div class="row" style="margin-bottom: 10px;">
598
- <button id="refreshQuotaBtn" class="primary">Refresh</button>
599
- <button id="resetQuotaBtn" class="danger">Reset quota module</button>
906
+ </p>
907
+ <div class="row" style="margin-bottom: 10px;">
908
+ <label for="quotaFilterInput">filter</label>
909
+ <input id="quotaFilterInput" type="text" placeholder="providerKey contains…" style="width: 320px;" />
910
+ <label><input id="quotaHideOkToggle" type="checkbox" /> hide ok</label>
911
+ <label><input id="quotaOnlyRoutedTargetsToggle" type="checkbox" checked /> only routed targets</label>
912
+ <span class="muted" style="font-size:12px;">Tip: click a row to fill the offline box.</span>
913
+ </div>
914
+ <div class="row" style="margin-bottom: 10px;">
915
+ <label for="quotaKeyInput">providerKey</label>
916
+ <input id="quotaKeyInput" type="text" placeholder="tab.key1.gpt-5.2-codex" style="width: 420px;" />
917
+ <label for="quotaModeSelect">offline mode</label>
918
+ <select id="quotaModeSelect" style="width: 140px;">
919
+ <option value="cooldown">cooldown</option>
920
+ <option value="blacklist">blacklist</option>
921
+ </select>
922
+ <label for="quotaDurationSelect">offline time</label>
923
+ <select id="quotaDurationSelect" style="width: 160px;">
924
+ <option value="5">5m</option>
925
+ <option value="15">15m</option>
926
+ <option value="30">30m</option>
927
+ <option value="60" selected>1h</option>
928
+ <option value="180">3h</option>
929
+ <option value="360">6h</option>
930
+ <option value="720">12h</option>
931
+ <option value="1440">24h</option>
932
+ </select>
933
+ <button id="quotaApplyDisableBtn" class="danger">Offline</button>
934
+ <button id="quotaApplyRecoverBtn">Recover</button>
935
+ <button id="quotaApplyResetBtn">Reset</button>
936
+ </div>
937
+ <div class="muted" style="font-size:12px; margin: -6px 0 10px;">
938
+ Offline removes the provider from the route pool for the selected minutes. Recover brings it back online immediately.
939
+ </div>
940
+ <div class="row" style="margin-bottom: 10px;">
941
+ <button id="refreshQuotaBtn" class="primary">Refresh provider pool</button>
942
+ <button id="refreshQuotaSnapshotBtn" class="primary">Refresh antigravity snapshot</button>
943
+ <button id="resetQuotaBtn" class="danger">Reset provider-quota module</button>
600
944
  </div>
601
945
  <div class="table-wrap">
602
946
  <table class="table">
603
947
  <thead>
604
948
  <tr>
605
- <th>providerKey</th>
606
- <th>auth</th>
949
+ <th>provider</th>
950
+ <th>key</th>
951
+ <th>model</th>
607
952
  <th>inPool</th>
608
953
  <th>reason</th>
609
- <th>cooldownUntil</th>
610
- <th>blacklistUntil</th>
954
+ <th>until</th>
611
955
  <th>errCount</th>
612
956
  <th></th>
613
957
  </tr>
@@ -615,66 +959,148 @@
615
959
  <tbody id="quotaTbody"></tbody>
616
960
  </table>
617
961
  </div>
618
- <div id="quotaOpLog" class="log" style="margin-top: 10px; display:none;"></div>
619
- </div>
620
-
621
- <div class="card" style="box-shadow:none;">
622
- <p class="section-title">Notes</p>
623
- <div class="notice">
962
+ <div id="quotaOpLog" class="log" style="margin-top: 10px; display:none;"></div>
963
+ </div>
964
+
965
+ <div class="card" style="box-shadow:none;">
966
+ <p class="section-title">Antigravity quota snapshot</p>
967
+ <p class="section-sub">
968
+ Snapshot fetched by <span class="mono">QuotaManagerModule</span> (antigravity quota API). Used to gate antigravity providers entering the route pool.
969
+ </p>
970
+ <div class="row" style="margin-bottom: 10px;">
971
+ <label><input id="quotaSnapshotOnlyRoutedToggle" type="checkbox" checked /> only routed models</label>
972
+ <span class="muted" style="font-size:12px;">(filters by current provider pool keys)</span>
973
+ </div>
974
+ <div class="table-wrap">
975
+ <table class="table">
976
+ <thead>
977
+ <tr>
978
+ <th>alias</th>
979
+ <th>model</th>
980
+ <th>remaining</th>
981
+ <th>resetAt</th>
982
+ <th>fetchedAt</th>
983
+ </tr>
984
+ </thead>
985
+ <tbody id="quotaSnapshotTbody"></tbody>
986
+ </table>
987
+ </div>
988
+ <div id="quotaSnapshotLog" class="log" style="margin-top: 10px; display:none;"></div>
989
+ </div>
990
+
991
+ <div class="card" style="box-shadow:none;">
992
+ <p class="section-title">Notes</p>
993
+ <div class="notice">
624
994
  <div style="margin-bottom: 6px;">
625
995
  Use this view to confirm 429/backoff/blacklist decisions and whether a provider is currently eligible.
626
- </div>
627
- <div>
628
- If a provider looks stuck, try <span class="mono">Reset quota module</span>, then <span class="mono">Restart runtime</span>.
629
- </div>
630
- </div>
631
- </div>
996
+ </div>
997
+ <div>
998
+ If a provider looks stuck, try <span class="mono">Reset provider-quota module</span>, then <span class="mono">Restart runtime</span>.
999
+ </div>
1000
+ </div>
1001
+ </div>
632
1002
  </div>
633
1003
  </section>
634
1004
 
635
- <section id="panelRouting" data-panel="routing" style="display:none;">
636
- <div class="grid">
637
- <div class="card" style="box-shadow:none;">
638
- <p class="section-title">Routing editor</p>
639
- <p class="section-sub">Edits <span class="mono">virtualrouter.routing</span> in user config.</p>
640
- <div class="row" style="margin-bottom: 10px;">
641
- <button id="loadRoutingBtn" class="primary">Load</button>
642
- <button id="saveRoutingBtn" class="primary">Save</button>
643
- </div>
644
- <textarea id="routingEditor" spellcheck="false" placeholder="{\n \"default\": [...]\n}"></textarea>
645
- <div id="routingOpLog" class="log" style="margin-top: 10px; display:none;"></div>
646
- </div>
647
-
648
- <div class="card" style="box-shadow:none;">
649
- <p class="section-title">Runtime providers</p>
650
- <p class="section-sub">
651
- What the running process currently has loaded (restart required after edits).
652
- </p>
653
- <div class="row" style="margin-bottom: 10px;">
654
- <button id="refreshRuntimesBtn" class="primary">Refresh</button>
655
- </div>
656
- <div class="table-wrap">
657
- <table class="table">
658
- <thead>
659
- <tr>
660
- <th>providerKey</th>
661
- <th>runtimeKey</th>
662
- <th>family</th>
663
- <th>protocol</th>
664
- <th>series</th>
665
- </tr>
666
- </thead>
667
- <tbody id="runtimesTbody"></tbody>
668
- </table>
669
- </div>
670
- </div>
1005
+ <section id="panelRouting" data-panel="routing" style="display:none;">
1006
+ <div class="grid grid-one">
1007
+ <div class="card" style="box-shadow:none;">
1008
+ <p class="section-title">Routing editor</p>
1009
+ <p class="section-sub">
1010
+ Edits routing in a selected config file (auto-detects <span class="mono">virtualrouter.routing</span> or <span class="mono">routing</span>).
1011
+ </p>
1012
+ <div class="row" style="margin-bottom: 10px;">
1013
+ <label for="routingSourceSelect">source</label>
1014
+ <select id="routingSourceSelect" style="width: 420px;"></select>
1015
+ <button id="refreshRoutingSourcesBtn">Refresh sources</button>
1016
+ </div>
1017
+ <div class="row" style="margin-bottom: 10px;">
1018
+ <label for="routingQuotaModeSelect">mode</label>
1019
+ <select id="routingQuotaModeSelect" style="width: 160px;">
1020
+ <option value="cooldown" selected>cooldown</option>
1021
+ <option value="blacklist">blacklist</option>
1022
+ </select>
1023
+ <label for="routingQuotaDurationSelect">offline time</label>
1024
+ <select id="routingQuotaDurationSelect" style="width: 160px;">
1025
+ <option value="5">5m</option>
1026
+ <option value="15">15m</option>
1027
+ <option value="30">30m</option>
1028
+ <option value="60" selected>1h</option>
1029
+ <option value="180">3h</option>
1030
+ <option value="360">6h</option>
1031
+ <option value="720">12h</option>
1032
+ <option value="1440">24h</option>
1033
+ </select>
1034
+ </div>
1035
+ <div class="row" style="margin-bottom: 10px;">
1036
+ <button id="loadRoutingBtn" class="primary">Load</button>
1037
+ <button id="saveRoutingBtn" class="primary">Save</button>
1038
+ <button id="refreshRoutingPoolBtn">Refresh pool status</button>
1039
+ <button id="routingRegExpandBtn">Expand all</button>
1040
+ <button id="routingRegCollapseBtn">Collapse all</button>
1041
+ <label style="margin-left:auto;"><input id="routingShowPathsToggle" type="checkbox" /> show debug paths</label>
1042
+ </div>
1043
+ <div id="routingOpLog" class="log" style="margin-top: 10px; display:none;"></div>
1044
+ <div class="muted" style="font-size:12px; margin: -4px 0 10px;">
1045
+ Tip: edit routing here (CRUD). For tool targets, use the inline <span class="mono">Offline/Recover</span> controls to manage runtime provider keys.
1046
+ </div>
1047
+ <div id="routingKvEditor" class="kv kv-editor"></div>
1048
+ </div>
671
1049
  </div>
672
1050
  </section>
673
1051
  </div>
674
1052
  </div>
675
1053
 
1054
+ <div id="toast" class="toast" role="status" aria-live="polite"></div>
1055
+
676
1056
  <script>
677
1057
  const $ = (id) => document.getElementById(id);
1058
+ const UI = {
1059
+ selectedProviderId: "",
1060
+ lastUnauthorizedToastAt: 0,
1061
+ adminAuth: null,
1062
+ quotaProviders: [],
1063
+ quotaProvidersUpdatedAt: 0,
1064
+ quotaProviderMap: null,
1065
+ routingTargets: null,
1066
+ routingTargetsUpdatedAt: 0,
1067
+ routingSources: [],
1068
+ routingSourcesUpdatedAt: 0,
1069
+ routingLocation: "virtualrouter.routing"
1070
+ };
1071
+ let toastTimer = null;
1072
+
1073
+ function toast(msg, kind = "err") {
1074
+ const el = $("toast");
1075
+ if (!el) return;
1076
+ el.classList.remove("ok", "err", "show");
1077
+ el.classList.add(kind === "ok" ? "ok" : "err");
1078
+ el.textContent = String(msg || "");
1079
+ el.classList.add("show");
1080
+ if (toastTimer) clearTimeout(toastTimer);
1081
+ toastTimer = setTimeout(() => {
1082
+ try { el.classList.remove("show"); } catch {}
1083
+ }, 4200);
1084
+ }
1085
+
1086
+ function onClick(id, handler) {
1087
+ const el = $(id);
1088
+ if (!el) {
1089
+ console.warn(`[daemon-admin-ui] missing #${id}`);
1090
+ return false;
1091
+ }
1092
+ el.addEventListener("click", handler);
1093
+ return true;
1094
+ }
1095
+
1096
+ function notifyUnauthorizedOnce(context) {
1097
+ const now = Date.now();
1098
+ if (now - UI.lastUnauthorizedToastAt < 1800) return;
1099
+ UI.lastUnauthorizedToastAt = now;
1100
+ const label = context ? ` (${context})` : "";
1101
+ toast(`Unauthorized${label}. Login required.`);
1102
+ try { void refreshAdminAuthStatus(); } catch {}
1103
+ }
678
1104
 
679
1105
  function setLog(id, value) {
680
1106
  const el = $(id);
@@ -703,10 +1129,8 @@
703
1129
 
704
1130
  async function apiFetch(path, opts = {}) {
705
1131
  const headers = new Headers(opts.headers || {});
706
- const apiKey = getApiKey();
707
- if (apiKey) headers.set("x-api-key", apiKey);
708
1132
  if (!headers.has("content-type") && opts.body) headers.set("content-type", "application/json");
709
- const res = await fetch(path, { ...opts, headers });
1133
+ const res = await fetch(path, { ...opts, headers, credentials: "same-origin" });
710
1134
  const text = await res.text();
711
1135
  let json = null;
712
1136
  try {
@@ -720,6 +1144,7 @@
720
1144
  `HTTP ${res.status} ${res.statusText}`;
721
1145
  const err = new Error(msg);
722
1146
  err.status = res.status;
1147
+ err.path = path;
723
1148
  err.payload = json;
724
1149
  throw err;
725
1150
  }
@@ -739,7 +1164,7 @@
739
1164
  ];
740
1165
  for (const p of panels) p.el.style.display = p.name === name ? "block" : "none";
741
1166
 
742
- // Light auto-refresh on tab switch to avoid showing stale "Unauthorized" after setting apikey.
1167
+ // Light auto-refresh on tab switch to avoid showing stale "Unauthorized" after login.
743
1168
  void maybeRefreshTab(name);
744
1169
  }
745
1170
 
@@ -768,8 +1193,14 @@
768
1193
  if (key === "providers") await refreshProviders();
769
1194
  else if (key === "tokens") await refreshTokens();
770
1195
  else if (key === "credentials") await refreshCredentials();
771
- else if (key === "quota") await refreshQuota();
772
- else if (key === "routing") await refreshRuntimes();
1196
+ else if (key === "quota") {
1197
+ await refreshQuota();
1198
+ await refreshQuotaSnapshot();
1199
+ }
1200
+ else if (key === "routing") {
1201
+ await refreshRuntimes();
1202
+ await refreshRoutingSources();
1203
+ }
773
1204
  } catch {
774
1205
  // ignore refresh failures on tab switch
775
1206
  }
@@ -799,6 +1230,476 @@
799
1230
  return tr;
800
1231
  }
801
1232
 
1233
+ function setSelectedProviderId(id) {
1234
+ UI.selectedProviderId = textOf(id || "");
1235
+ const tbody = $("providersTbody");
1236
+ if (!tbody) return;
1237
+ tbody.querySelectorAll("tr.provider-row").forEach((tr) => {
1238
+ const pid = tr.getAttribute("data-provider-id") || "";
1239
+ tr.classList.toggle("selected", pid && pid === UI.selectedProviderId);
1240
+ });
1241
+ }
1242
+
1243
+ function kvTypeMeta(value) {
1244
+ if (value === null) return "null";
1245
+ if (value === undefined) return "undefined";
1246
+ if (Array.isArray(value)) return `array(${value.length})`;
1247
+ const t = typeof value;
1248
+ if (t === "string") return `string(${value.length})`;
1249
+ if (t === "number") return "number";
1250
+ if (t === "boolean") return "boolean";
1251
+ if (t === "bigint") return "bigint";
1252
+ if (t === "function") return "function";
1253
+ if (t === "symbol") return "symbol";
1254
+ if (t === "object") {
1255
+ try {
1256
+ const keys = Object.keys(value);
1257
+ return `object(${keys.length})`;
1258
+ } catch {
1259
+ return "object";
1260
+ }
1261
+ }
1262
+ return t;
1263
+ }
1264
+
1265
+ const providerEditorState = {
1266
+ value: {},
1267
+ dirty: false
1268
+ };
1269
+
1270
+ function providerEditorClone(value) {
1271
+ try {
1272
+ return value == null ? value : JSON.parse(JSON.stringify(value));
1273
+ } catch {
1274
+ return value;
1275
+ }
1276
+ }
1277
+
1278
+ function providerEditorSetDirty(dirty) {
1279
+ providerEditorState.dirty = Boolean(dirty);
1280
+ const hint = $("providerEditorDirtyHint");
1281
+ if (hint) hint.textContent = providerEditorState.dirty ? "unsaved changes" : "";
1282
+ }
1283
+
1284
+ function providerEditorSetValue(value) {
1285
+ providerEditorState.value = providerEditorClone(value) || {};
1286
+ providerEditorSetDirty(false);
1287
+ providerEditorRender();
1288
+ }
1289
+
1290
+ function providerEditorGetValue() {
1291
+ return providerEditorClone(providerEditorState.value) || {};
1292
+ }
1293
+
1294
+ function providerEditorPathToString(path) {
1295
+ if (!path || !path.length) return "";
1296
+ return path.map((p) => String(p)).join(".");
1297
+ }
1298
+
1299
+ function providerEditorGetByPath(root, path) {
1300
+ let cur = root;
1301
+ for (const part of path || []) {
1302
+ if (cur == null) return undefined;
1303
+ cur = cur[part];
1304
+ }
1305
+ return cur;
1306
+ }
1307
+
1308
+ function providerEditorSetByPath(root, path, nextValue) {
1309
+ if (!root || typeof root !== "object") return;
1310
+ if (!path || !path.length) return;
1311
+ const last = path[path.length - 1];
1312
+ let cur = root;
1313
+ for (let i = 0; i < path.length - 1; i += 1) {
1314
+ const part = path[i];
1315
+ const existing = cur[part];
1316
+ if (existing == null || typeof existing !== "object") cur[part] = {};
1317
+ cur = cur[part];
1318
+ }
1319
+ cur[last] = nextValue;
1320
+ }
1321
+
1322
+ function providerEditorDeleteByPath(root, path) {
1323
+ if (!root || typeof root !== "object") return;
1324
+ if (!path || !path.length) return;
1325
+ const last = path[path.length - 1];
1326
+ let cur = root;
1327
+ for (let i = 0; i < path.length - 1; i += 1) {
1328
+ const part = path[i];
1329
+ if (cur == null) return;
1330
+ cur = cur[part];
1331
+ }
1332
+ if (Array.isArray(cur)) {
1333
+ const idx = Number(last);
1334
+ if (Number.isFinite(idx) && idx >= 0 && idx < cur.length) cur.splice(idx, 1);
1335
+ return;
1336
+ }
1337
+ try { delete cur[last]; } catch {}
1338
+ }
1339
+
1340
+ function providerEditorKind(value) {
1341
+ if (value === null) return "null";
1342
+ if (Array.isArray(value)) return "array";
1343
+ const t = typeof value;
1344
+ if (t === "string") return "string";
1345
+ if (t === "number") return "number";
1346
+ if (t === "boolean") return "boolean";
1347
+ if (t === "object") return "object";
1348
+ return "string";
1349
+ }
1350
+
1351
+ function providerEditorCoerce(kind, raw) {
1352
+ if (kind === "null") return null;
1353
+ if (kind === "boolean") return raw === true || raw === "true";
1354
+ if (kind === "number") {
1355
+ const n = typeof raw === "number" ? raw : Number.parseFloat(String(raw));
1356
+ return Number.isFinite(n) ? n : 0;
1357
+ }
1358
+ if (kind === "array") return [];
1359
+ if (kind === "object") return {};
1360
+ return String(raw ?? "");
1361
+ }
1362
+
1363
+ function providerEditorRender() {
1364
+ const container = $("providerKvEditor");
1365
+ if (!container) return;
1366
+ try {
1367
+ container.replaceChildren();
1368
+ } catch {
1369
+ try { container.innerHTML = ""; } catch {}
1370
+ }
1371
+ try {
1372
+ container.appendChild(providerEditorRenderNode("provider", providerEditorState.value || {}, [], 0, true));
1373
+ } catch (e) {
1374
+ const msg = e && e.message ? e.message : String(e);
1375
+ try { container.textContent = `Render failed: ${msg}`; } catch {}
1376
+ }
1377
+ }
1378
+
1379
+ function providerEditorRenderNode(label, value, path, depth, isRoot = false) {
1380
+ const isObj = value !== null && typeof value === "object";
1381
+ const isArr = Array.isArray(value);
1382
+ const kind = providerEditorKind(value);
1383
+
1384
+ if (!isObj) {
1385
+ const row = document.createElement("div");
1386
+ row.className = "kv-leaf";
1387
+
1388
+ const keyEl = document.createElement("div");
1389
+ keyEl.className = "kv-key";
1390
+ keyEl.textContent = label;
1391
+
1392
+ const typeSel = document.createElement("select");
1393
+ typeSel.className = "kv-type";
1394
+ typeSel.innerHTML = [
1395
+ "<option value=\"string\">string</option>",
1396
+ "<option value=\"number\">number</option>",
1397
+ "<option value=\"boolean\">boolean</option>",
1398
+ "<option value=\"null\">null</option>",
1399
+ "<option value=\"object\">object</option>",
1400
+ "<option value=\"array\">array</option>"
1401
+ ].join("");
1402
+ typeSel.value = kind;
1403
+ typeSel.addEventListener("click", (e) => e.stopPropagation());
1404
+ typeSel.addEventListener("change", () => {
1405
+ const root = providerEditorState.value || {};
1406
+ providerEditorSetByPath(root, path, providerEditorCoerce(typeSel.value, ""));
1407
+ providerEditorSetDirty(true);
1408
+ providerEditorRender();
1409
+ });
1410
+
1411
+ const valWrap = document.createElement("div");
1412
+ valWrap.className = "kv-val";
1413
+
1414
+ if (kind === "boolean") {
1415
+ const sel = document.createElement("select");
1416
+ sel.innerHTML = `<option value="true">true</option><option value="false">false</option>`;
1417
+ sel.value = value ? "true" : "false";
1418
+ sel.addEventListener("click", (e) => e.stopPropagation());
1419
+ sel.addEventListener("change", () => {
1420
+ const root = providerEditorState.value || {};
1421
+ providerEditorSetByPath(root, path, sel.value === "true");
1422
+ providerEditorSetDirty(true);
1423
+ providerEditorRender();
1424
+ });
1425
+ valWrap.appendChild(sel);
1426
+ } else if (kind === "number") {
1427
+ const inp = document.createElement("input");
1428
+ inp.type = "number";
1429
+ inp.value = String(value);
1430
+ inp.addEventListener("click", (e) => e.stopPropagation());
1431
+ inp.addEventListener("change", () => {
1432
+ const root = providerEditorState.value || {};
1433
+ const n = Number.parseFloat(inp.value);
1434
+ providerEditorSetByPath(root, path, Number.isFinite(n) ? n : 0);
1435
+ providerEditorSetDirty(true);
1436
+ providerEditorRender();
1437
+ });
1438
+ valWrap.appendChild(inp);
1439
+ } else if (kind === "null") {
1440
+ const span = document.createElement("span");
1441
+ span.className = "mono";
1442
+ span.textContent = "null";
1443
+ valWrap.appendChild(span);
1444
+ } else {
1445
+ const inp = document.createElement("input");
1446
+ inp.type = "text";
1447
+ inp.value = value == null ? "" : String(value);
1448
+ inp.addEventListener("click", (e) => e.stopPropagation());
1449
+ inp.addEventListener("change", () => {
1450
+ const root = providerEditorState.value || {};
1451
+ providerEditorSetByPath(root, path, String(inp.value));
1452
+ providerEditorSetDirty(true);
1453
+ providerEditorRender();
1454
+ });
1455
+ valWrap.appendChild(inp);
1456
+ }
1457
+
1458
+ const actions = document.createElement("div");
1459
+ actions.className = "kv-actions";
1460
+ if (!isRoot) {
1461
+ const del = document.createElement("button");
1462
+ del.textContent = "Del";
1463
+ del.className = "danger";
1464
+ del.addEventListener("click", (e) => {
1465
+ e.preventDefault();
1466
+ e.stopPropagation();
1467
+ const root = providerEditorState.value || {};
1468
+ providerEditorDeleteByPath(root, path);
1469
+ providerEditorSetDirty(true);
1470
+ providerEditorRender();
1471
+ });
1472
+ actions.appendChild(del);
1473
+ }
1474
+ const pathEl = document.createElement("span");
1475
+ pathEl.className = "kv-path mono";
1476
+ pathEl.textContent = providerEditorPathToString(path);
1477
+ actions.appendChild(pathEl);
1478
+
1479
+ row.appendChild(keyEl);
1480
+ row.appendChild(typeSel);
1481
+ row.appendChild(valWrap);
1482
+ row.appendChild(actions);
1483
+ return row;
1484
+ }
1485
+
1486
+ const details = document.createElement("details");
1487
+ details.open = isRoot || depth < 1;
1488
+
1489
+ const summary = document.createElement("summary");
1490
+
1491
+ const keyEl = document.createElement("div");
1492
+ keyEl.className = "kv-key";
1493
+ keyEl.textContent = label;
1494
+
1495
+ const metaEl = document.createElement("div");
1496
+ metaEl.className = "kv-meta";
1497
+ metaEl.textContent = kvTypeMeta(value);
1498
+
1499
+ const actions = document.createElement("div");
1500
+ actions.className = "kv-actions";
1501
+
1502
+ if (isArr) {
1503
+ const add = document.createElement("button");
1504
+ add.textContent = "+Item";
1505
+ add.addEventListener("click", (e) => {
1506
+ e.preventDefault();
1507
+ e.stopPropagation();
1508
+ const t = (prompt("Item type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
1509
+ const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
1510
+ let init = "";
1511
+ if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
1512
+ else if (k === "number") init = prompt("Value (number)", "0") || "0";
1513
+ else if (k === "string") init = prompt("Value (string)", "") || "";
1514
+ const v = providerEditorCoerce(k, k === "boolean" ? init === "true" : init);
1515
+ const root = providerEditorState.value || {};
1516
+ const arr = providerEditorGetByPath(root, path);
1517
+ if (Array.isArray(arr)) {
1518
+ arr.push(v);
1519
+ providerEditorSetDirty(true);
1520
+ providerEditorRender();
1521
+ }
1522
+ });
1523
+ actions.appendChild(add);
1524
+ } else {
1525
+ const add = document.createElement("button");
1526
+ add.textContent = "+Key";
1527
+ add.addEventListener("click", (e) => {
1528
+ e.preventDefault();
1529
+ e.stopPropagation();
1530
+ const name = (prompt("Key name", "") || "").trim();
1531
+ if (!name) return;
1532
+ const t = (prompt("Value type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
1533
+ const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
1534
+ let init = "";
1535
+ if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
1536
+ else if (k === "number") init = prompt("Value (number)", "0") || "0";
1537
+ else if (k === "string") init = prompt("Value (string)", "") || "";
1538
+ const v = providerEditorCoerce(k, k === "boolean" ? init === "true" : init);
1539
+ const root = providerEditorState.value || {};
1540
+ const obj = providerEditorGetByPath(root, path);
1541
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1542
+ obj[name] = v;
1543
+ providerEditorSetDirty(true);
1544
+ providerEditorRender();
1545
+ }
1546
+ });
1547
+ actions.appendChild(add);
1548
+ }
1549
+
1550
+ if (!isRoot) {
1551
+ const del = document.createElement("button");
1552
+ del.textContent = "Del";
1553
+ del.className = "danger";
1554
+ del.addEventListener("click", (e) => {
1555
+ e.preventDefault();
1556
+ e.stopPropagation();
1557
+ const root = providerEditorState.value || {};
1558
+ providerEditorDeleteByPath(root, path);
1559
+ providerEditorSetDirty(true);
1560
+ providerEditorRender();
1561
+ });
1562
+ actions.appendChild(del);
1563
+ }
1564
+
1565
+ const pathEl = document.createElement("span");
1566
+ pathEl.className = "kv-path mono";
1567
+ pathEl.textContent = providerEditorPathToString(path);
1568
+ actions.appendChild(pathEl);
1569
+
1570
+ summary.appendChild(keyEl);
1571
+ summary.appendChild(metaEl);
1572
+ summary.appendChild(actions);
1573
+ details.appendChild(summary);
1574
+
1575
+ if (isArr) {
1576
+ for (let i = 0; i < value.length; i += 1) {
1577
+ details.appendChild(providerEditorRenderNode(String(i), value[i], path.concat([i]), depth + 1));
1578
+ }
1579
+ return details;
1580
+ }
1581
+
1582
+ let keys = [];
1583
+ try {
1584
+ keys = Object.keys(value);
1585
+ keys.sort((a, b) => a.localeCompare(b));
1586
+ } catch {
1587
+ keys = [];
1588
+ }
1589
+ for (const childKey of keys) {
1590
+ details.appendChild(providerEditorRenderNode(childKey, value[childKey], path.concat([childKey]), depth + 1));
1591
+ }
1592
+
1593
+ return details;
1594
+ }
1595
+
1596
+ function providerEditorSetAuthApiKey(secretRef) {
1597
+ const ref = textOf(secretRef).trim();
1598
+ if (!ref) return;
1599
+ const root = providerEditorState.value || {};
1600
+ if (!root.auth || typeof root.auth !== "object" || Array.isArray(root.auth)) root.auth = {};
1601
+ root.auth.type = "apikey";
1602
+ root.auth.apiKey = ref;
1603
+ providerEditorSetDirty(true);
1604
+ providerEditorRender();
1605
+ }
1606
+
1607
+ function kvPreview(value) {
1608
+ if (value === null || value === undefined) return String(value);
1609
+ const t = typeof value;
1610
+ if (t === "string") {
1611
+ const max = 140;
1612
+ const s = value.length > max ? value.slice(0, max) + "…" : value;
1613
+ return JSON.stringify(s);
1614
+ }
1615
+ if (t === "number" || t === "boolean" || t === "bigint") return String(value);
1616
+ if (Array.isArray(value)) return "";
1617
+ if (t === "object") return "";
1618
+ return String(value);
1619
+ }
1620
+
1621
+ function renderRegistry(container, value, opts = {}) {
1622
+ if (!container) return;
1623
+ container.replaceChildren();
1624
+ const rootLabel = (opts.rootLabel || "root").trim() || "root";
1625
+ try {
1626
+ container.appendChild(renderRegistryNode(rootLabel, value, 0, true));
1627
+ } catch (e) {
1628
+ const box = document.createElement("div");
1629
+ box.className = "mono";
1630
+ box.textContent = `Render failed: ${e && e.message ? e.message : String(e)}`;
1631
+ container.appendChild(box);
1632
+ }
1633
+ }
1634
+
1635
+ function renderRegistryNode(key, value, depth, isRoot = false) {
1636
+ const isObj = value !== null && typeof value === "object";
1637
+ const isArr = Array.isArray(value);
1638
+
1639
+ if (!isObj) {
1640
+ const row = document.createElement("div");
1641
+ row.className = "kv-leaf";
1642
+ const k = document.createElement("div");
1643
+ k.className = "kv-key";
1644
+ k.textContent = key;
1645
+ const v = document.createElement("div");
1646
+ v.className = "kv-val";
1647
+ v.textContent = kvPreview(value);
1648
+ row.appendChild(k);
1649
+ row.appendChild(v);
1650
+ return row;
1651
+ }
1652
+
1653
+ const details = document.createElement("details");
1654
+ details.open = isRoot || depth < 1;
1655
+
1656
+ const summary = document.createElement("summary");
1657
+ const k = document.createElement("div");
1658
+ k.className = "kv-key";
1659
+ k.textContent = key;
1660
+ const m = document.createElement("div");
1661
+ m.className = "kv-meta";
1662
+ const meta = kvTypeMeta(value);
1663
+ const preview = kvPreview(value);
1664
+ m.textContent = preview ? `${meta} ${preview}` : meta;
1665
+ summary.appendChild(k);
1666
+ summary.appendChild(m);
1667
+ details.appendChild(summary);
1668
+
1669
+ if (isArr) {
1670
+ for (let i = 0; i < value.length; i += 1) {
1671
+ details.appendChild(renderRegistryNode(String(i), value[i], depth + 1));
1672
+ }
1673
+ return details;
1674
+ }
1675
+
1676
+ let keys = [];
1677
+ try {
1678
+ keys = Object.keys(value);
1679
+ keys.sort((a, b) => a.localeCompare(b));
1680
+ } catch {
1681
+ keys = [];
1682
+ }
1683
+ for (const childKey of keys) {
1684
+ let childValue;
1685
+ try {
1686
+ childValue = value[childKey];
1687
+ } catch (e) {
1688
+ childValue = `[[unreadable: ${e && e.message ? e.message : String(e)}]]`;
1689
+ }
1690
+ details.appendChild(renderRegistryNode(childKey, childValue, depth + 1));
1691
+ }
1692
+ return details;
1693
+ }
1694
+
1695
+ function setAllDetailsOpen(container, open, keepRootOpen = true) {
1696
+ if (!container) return;
1697
+ const nodes = Array.from(container.querySelectorAll("details"));
1698
+ nodes.forEach((d, idx) => {
1699
+ d.open = open || (keepRootOpen && idx === 0);
1700
+ });
1701
+ }
1702
+
802
1703
  function presetFor(type) {
803
1704
  if (type === "responses") {
804
1705
  return {
@@ -887,8 +1788,12 @@
887
1788
  const items = grouped.get(type);
888
1789
  items.sort((a, b) => textOf(a.id).localeCompare(textOf(b.id)));
889
1790
  for (const p of items) {
1791
+ const pid = textOf(p.id);
890
1792
  const tr = document.createElement("tr");
891
- tr.appendChild(createCell("td", p.id || "", "mono indent"));
1793
+ tr.className = "provider-row";
1794
+ tr.setAttribute("data-provider-id", pid);
1795
+ if (pid && pid === UI.selectedProviderId) tr.classList.add("selected");
1796
+ tr.appendChild(createCell("td", pid || "", "mono indent"));
892
1797
  tr.appendChild(createCell("td", p.type || "", ""));
893
1798
  tr.appendChild(createCell("td", String(Boolean(p.enabled)), ""));
894
1799
  tr.appendChild(createCell("td", p.baseURL || "", "mono truncate", { title: true }));
@@ -924,19 +1829,23 @@
924
1829
  }
925
1830
  }
926
1831
  } catch (e) {
1832
+ if (e && e.status === 401) notifyUnauthorizedOnce("providers");
927
1833
  body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
928
1834
  }
929
1835
  }
930
1836
 
931
1837
  async function loadProvider(id) {
932
1838
  setLog("providerOpLog", "");
1839
+ setSelectedProviderId(id);
933
1840
  try {
934
1841
  const data = await apiFetch(`/config/providers/${encodeURIComponent(id)}`);
935
1842
  $("providerIdInput").value = id;
936
- $("providerJsonEditor").value = JSON.stringify(data.provider || {}, null, 2);
1843
+ providerEditorSetValue(data.provider || {});
937
1844
  $("providerEditorTitle").textContent = `Provider editor: ${id}`;
938
1845
  } catch (e) {
1846
+ if (e && e.status === 401) notifyUnauthorizedOnce("provider load");
939
1847
  setLog("providerOpLog", `Load failed: ${e.message}`);
1848
+ toast(`Load failed: ${e.message}`);
940
1849
  }
941
1850
  }
942
1851
 
@@ -945,18 +1854,24 @@
945
1854
  const id = ($("providerIdInput").value || "").trim();
946
1855
  if (!id) {
947
1856
  setLog("providerOpLog", "provider id is required");
1857
+ toast("provider id is required");
948
1858
  return;
949
1859
  }
950
1860
  try {
951
- const provider = JSON.parse($("providerJsonEditor").value || "{}");
1861
+ const provider = providerEditorGetValue() || {};
1862
+ if (provider && typeof provider === "object") provider.id = id;
952
1863
  const result = await apiFetch(`/config/providers/${encodeURIComponent(id)}`, {
953
1864
  method: "PUT",
954
1865
  body: JSON.stringify({ provider })
955
1866
  });
956
1867
  setLog("providerOpLog", `Saved. Path: ${result.path || "—"}\nRestart required to apply.`);
1868
+ toast("Provider saved.", "ok");
1869
+ providerEditorSetDirty(false);
957
1870
  await refreshProviders();
958
1871
  } catch (e) {
1872
+ if (e && e.status === 401) notifyUnauthorizedOnce("provider save");
959
1873
  setLog("providerOpLog", `Save failed: ${e.message}`);
1874
+ toast(`Save failed: ${e.message}`);
960
1875
  }
961
1876
  }
962
1877
 
@@ -964,15 +1879,19 @@
964
1879
  setLog("providerOpLog", "");
965
1880
  if (!id) {
966
1881
  setLog("providerOpLog", "provider id is required");
1882
+ toast("provider id is required");
967
1883
  return;
968
1884
  }
969
1885
  if (!confirm(`Delete provider "${id}" from user config?`)) return;
970
1886
  try {
971
1887
  const result = await apiFetch(`/config/providers/${encodeURIComponent(id)}`, { method: "DELETE" });
972
1888
  setLog("providerOpLog", `Deleted. Path: ${result.path || "—"}\nRestart required to apply.`);
1889
+ toast("Provider deleted.", "ok");
973
1890
  await refreshProviders();
974
1891
  } catch (e) {
1892
+ if (e && e.status === 401) notifyUnauthorizedOnce("provider delete");
975
1893
  setLog("providerOpLog", `Delete failed: ${e.message}`);
1894
+ toast(`Delete failed: ${e.message}`);
976
1895
  }
977
1896
  }
978
1897
 
@@ -998,7 +1917,9 @@
998
1917
  $("apikeySecretRefOut").textContent = `secretRef: ${out.secretRef}`;
999
1918
  return out.secretRef;
1000
1919
  } catch (e) {
1920
+ if (e && e.status === 401) notifyUnauthorizedOnce("authfile create");
1001
1921
  setLog("providerOpLog", `Create authfile failed: ${e.message}`);
1922
+ toast(`Create authfile failed: ${e.message}`);
1002
1923
  return null;
1003
1924
  }
1004
1925
  }
@@ -1025,7 +1946,8 @@
1025
1946
  const secretRef = ($("apikeySecretRefOut").textContent || "").replace(/^secretRef:\\s*/i, "").trim();
1026
1947
  base.auth = { type: "apikey", apiKey: secretRef || "authfile-REPLACE_ME" };
1027
1948
  }
1028
- $("providerJsonEditor").value = JSON.stringify(base, null, 2);
1949
+ providerEditorSetValue(base);
1950
+ providerEditorSetDirty(true);
1029
1951
  }
1030
1952
 
1031
1953
  function updateAuthModeUi() {
@@ -1078,74 +2000,678 @@
1078
2000
  }
1079
2001
  }
1080
2002
 
1081
- async function authorizeOauth() {
1082
- setLog("credentialOpLog", "");
1083
- const provider = $("oauthProviderSelect").value;
1084
- const alias = ($("oauthAuthAliasInput").value || "default").trim() || "default";
1085
- const openBrowser = $("oauthOpenBrowser").checked;
1086
- const forceReauthorize = $("oauthForceReauth").checked;
2003
+ async function authorizeOauth() {
2004
+ setLog("credentialOpLog", "");
2005
+ const provider = $("oauthProviderSelect").value;
2006
+ const alias = ($("oauthAuthAliasInput").value || "default").trim() || "default";
2007
+ const openBrowser = $("oauthOpenBrowser").checked;
2008
+ const forceReauthorize = $("oauthForceReauth").checked;
2009
+ try {
2010
+ const out = await apiFetch("/daemon/oauth/authorize", {
2011
+ method: "POST",
2012
+ body: JSON.stringify({ provider, alias, openBrowser, forceReauthorize })
2013
+ });
2014
+ setLog("credentialOpLog", `OK. tokenFile: ${out.tokenFile || "—"}`);
2015
+ await refreshCredentials();
2016
+ } catch (e) {
2017
+ setLog("credentialOpLog", `Authorize failed: ${e.message}`);
2018
+ }
2019
+ }
2020
+
2021
+ const routingEditorState = {
2022
+ value: {},
2023
+ dirty: false
2024
+ };
2025
+
2026
+ function routingEditorClone(value) {
2027
+ try {
2028
+ return value == null ? value : JSON.parse(JSON.stringify(value));
2029
+ } catch {
2030
+ return value;
2031
+ }
2032
+ }
2033
+
2034
+ function routingEditorSetDirty(dirty) {
2035
+ routingEditorState.dirty = Boolean(dirty);
2036
+ }
2037
+
2038
+ function routingEditorSetValue(value) {
2039
+ routingEditorState.value = routingEditorClone(value) || {};
2040
+ routingEditorSetDirty(false);
2041
+ routingEditorRender();
2042
+ }
2043
+
2044
+ function routingEditorGetValue() {
2045
+ return routingEditorClone(routingEditorState.value) || {};
2046
+ }
2047
+
2048
+ function routingEditorPathToString(path) {
2049
+ if (!path || !path.length) return "";
2050
+ return path.map((p) => String(p)).join(".");
2051
+ }
2052
+
2053
+ function routingEditorGetByPath(root, path) {
2054
+ let cur = root;
2055
+ for (const part of path || []) {
2056
+ if (cur == null) return undefined;
2057
+ cur = cur[part];
2058
+ }
2059
+ return cur;
2060
+ }
2061
+
2062
+ function routingEditorSetByPath(root, path, nextValue) {
2063
+ if (!root || typeof root !== "object") return;
2064
+ if (!path || !path.length) return;
2065
+ const last = path[path.length - 1];
2066
+ let cur = root;
2067
+ for (let i = 0; i < path.length - 1; i += 1) {
2068
+ const part = path[i];
2069
+ const existing = cur[part];
2070
+ if (existing == null || typeof existing !== "object") cur[part] = {};
2071
+ cur = cur[part];
2072
+ }
2073
+ cur[last] = nextValue;
2074
+ }
2075
+
2076
+ function routingEditorDeleteByPath(root, path) {
2077
+ if (!root || typeof root !== "object") return;
2078
+ if (!path || !path.length) return;
2079
+ const last = path[path.length - 1];
2080
+ let cur = root;
2081
+ for (let i = 0; i < path.length - 1; i += 1) {
2082
+ const part = path[i];
2083
+ if (cur == null) return;
2084
+ cur = cur[part];
2085
+ }
2086
+ if (Array.isArray(cur)) {
2087
+ const idx = Number(last);
2088
+ if (Number.isFinite(idx) && idx >= 0 && idx < cur.length) cur.splice(idx, 1);
2089
+ return;
2090
+ }
2091
+ try { delete cur[last]; } catch {}
2092
+ }
2093
+
2094
+ function routingEditorKind(value) {
2095
+ if (value === null) return "null";
2096
+ if (Array.isArray(value)) return "array";
2097
+ const t = typeof value;
2098
+ if (t === "string") return "string";
2099
+ if (t === "number") return "number";
2100
+ if (t === "boolean") return "boolean";
2101
+ if (t === "object") return "object";
2102
+ return "string";
2103
+ }
2104
+
2105
+ function routingEditorCoerce(kind, raw) {
2106
+ if (kind === "null") return null;
2107
+ if (kind === "boolean") return raw === true || raw === "true";
2108
+ if (kind === "number") {
2109
+ const n = typeof raw === "number" ? raw : Number.parseFloat(String(raw));
2110
+ return Number.isFinite(n) ? n : 0;
2111
+ }
2112
+ if (kind === "array") return [];
2113
+ if (kind === "object") return {};
2114
+ return String(raw ?? "");
2115
+ }
2116
+
2117
+ function looksLikeRoutingTargetString(path, value) {
2118
+ const s = textOf(value).trim();
2119
+ if (!s) return false;
2120
+ if (s.length > 320) return false;
2121
+ if (s.includes(" ")) return false;
2122
+ if (!s.includes(".")) return false;
2123
+ const root = routingEditorState.value || {};
2124
+ const parentPath = path.slice(0, -1);
2125
+ const parent = routingEditorGetByPath(root, parentPath);
2126
+ const parentKey = path.length >= 2 ? path[path.length - 2] : null;
2127
+ if (parentKey === "targets") return true;
2128
+ if (Array.isArray(parent) && path.length === 2 && typeof path[0] === "string") return true;
2129
+ return false;
2130
+ }
2131
+
2132
+ function resolveTargetToProviderKeys(target) {
2133
+ const raw = textOf(target).trim();
2134
+ if (!raw) return [];
2135
+ const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
2136
+ const keys = list.map((p) => textOf(p && p.providerKey ? p.providerKey : "")).filter(Boolean);
2137
+ const known = new Set(keys);
2138
+ if (known.has(raw)) return [raw];
2139
+ const dot = raw.indexOf(".");
2140
+ if (dot <= 0) return [];
2141
+ const providerId = raw.slice(0, dot);
2142
+ const modelId = raw.slice(dot + 1);
2143
+ if (!providerId || !modelId) return [];
2144
+ const prefix = `${providerId}.`;
2145
+ const suffix = `.${modelId}`;
2146
+ const out = [];
2147
+ for (const k of keys) {
2148
+ if (k.startsWith(prefix) && k.endsWith(suffix)) out.push(k);
2149
+ }
2150
+ out.sort((a, b) => a.localeCompare(b));
2151
+ return out;
2152
+ }
2153
+
2154
+ function getQuotaStateByProviderKey(providerKey) {
2155
+ const map = UI.quotaProviderMap instanceof Map ? UI.quotaProviderMap : null;
2156
+ if (map && map.has(providerKey)) return map.get(providerKey);
2157
+ const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
2158
+ return list.find((p) => textOf(p && p.providerKey ? p.providerKey : "") === providerKey) || null;
2159
+ }
2160
+
2161
+ async function routingQuotaAction(kind, providerKey) {
2162
+ if (!providerKey) return;
2163
+ if (!UI.adminAuth || !UI.adminAuth.authenticated) {
2164
+ notifyUnauthorizedOnce("routing quota action");
2165
+ return;
2166
+ }
2167
+ try {
2168
+ if (kind === "recover") {
2169
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/recover`, { method: "POST" });
2170
+ } else if (kind === "disable") {
2171
+ const minutes = Number.parseFloat(textOf($("routingQuotaDurationSelect").value || "60"));
2172
+ if (!Number.isFinite(minutes) || minutes <= 0) throw new Error("Invalid minutes");
2173
+ const modeRaw = (textOf($("routingQuotaModeSelect").value) || "cooldown").trim().toLowerCase();
2174
+ const mode = modeRaw === "blacklist" ? "blacklist" : "cooldown";
2175
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/disable`, {
2176
+ method: "POST",
2177
+ body: JSON.stringify({ mode, durationMinutes: minutes })
2178
+ });
2179
+ } else {
2180
+ return;
2181
+ }
2182
+ await refreshRuntimes();
2183
+ routingEditorRender();
2184
+ } catch (e) {
2185
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing quota action");
2186
+ toast(`Action failed: ${e && e.message ? e.message : String(e)}`);
2187
+ }
2188
+ }
2189
+
2190
+ function routingEditorRenderTargetActions(target) {
2191
+ const providerKeys = resolveTargetToProviderKeys(target);
2192
+ if (!providerKeys.length) return null;
2193
+
2194
+ const box = document.createElement("div");
2195
+ box.className = "routing-target-actions";
2196
+
2197
+ for (const providerKey of providerKeys) {
2198
+ const row = document.createElement("div");
2199
+ row.className = "routing-target-row";
2200
+
2201
+ const key = document.createElement("span");
2202
+ key.className = "routing-target-key mono";
2203
+ key.textContent = providerKey;
2204
+
2205
+ const state = getQuotaStateByProviderKey(providerKey);
2206
+ const inPool = state ? Boolean(state.inPool) : null;
2207
+ const untilMs = state ? Math.max(Number(state.blacklistUntil || 0), Number(state.cooldownUntil || 0)) : 0;
2208
+ const meta = document.createElement("span");
2209
+ meta.className = "routing-target-meta";
2210
+ meta.textContent =
2211
+ inPool === null
2212
+ ? "unknown"
2213
+ : inPool
2214
+ ? "online"
2215
+ : untilMs
2216
+ ? `offline ${formatEpochWithDelta(untilMs)}`
2217
+ : "offline";
2218
+
2219
+ const offBtn = document.createElement("button");
2220
+ offBtn.textContent = "Offline";
2221
+ offBtn.className = "danger";
2222
+ offBtn.addEventListener("click", (e) => {
2223
+ e.preventDefault();
2224
+ e.stopPropagation();
2225
+ void routingQuotaAction("disable", providerKey);
2226
+ });
2227
+
2228
+ const recBtn = document.createElement("button");
2229
+ recBtn.textContent = "Recover";
2230
+ recBtn.addEventListener("click", (e) => {
2231
+ e.preventDefault();
2232
+ e.stopPropagation();
2233
+ void routingQuotaAction("recover", providerKey);
2234
+ });
2235
+
2236
+ row.appendChild(key);
2237
+ row.appendChild(meta);
2238
+ row.appendChild(offBtn);
2239
+ row.appendChild(recBtn);
2240
+ box.appendChild(row);
2241
+ }
2242
+
2243
+ return box;
2244
+ }
2245
+
2246
+ function routingEditorRender() {
2247
+ const container = $("routingKvEditor");
2248
+ if (!container) return;
2249
+ try {
2250
+ container.replaceChildren();
2251
+ } catch {
2252
+ try { container.innerHTML = ""; } catch {}
2253
+ }
2254
+ try {
2255
+ container.appendChild(routingEditorRenderNode("routing", routingEditorState.value || {}, [], 0, true));
2256
+ } catch (e) {
2257
+ const msg = e && e.message ? e.message : String(e);
2258
+ try { container.textContent = `Render failed: ${msg}`; } catch {}
2259
+ }
2260
+ }
2261
+
2262
+ function routingEditorRenderNode(label, value, path, depth, isRoot = false) {
2263
+ const isObj = value !== null && typeof value === "object";
2264
+ const isArr = Array.isArray(value);
2265
+ const kind = routingEditorKind(value);
2266
+
2267
+ if (!isObj) {
2268
+ const row = document.createElement("div");
2269
+ row.className = "kv-leaf";
2270
+
2271
+ const keyEl = document.createElement("div");
2272
+ keyEl.className = "kv-key";
2273
+ keyEl.textContent = label;
2274
+
2275
+ const typeSel = document.createElement("select");
2276
+ typeSel.className = "kv-type";
2277
+ typeSel.innerHTML = [
2278
+ "<option value=\"string\">string</option>",
2279
+ "<option value=\"number\">number</option>",
2280
+ "<option value=\"boolean\">boolean</option>",
2281
+ "<option value=\"null\">null</option>",
2282
+ "<option value=\"object\">object</option>",
2283
+ "<option value=\"array\">array</option>"
2284
+ ].join("");
2285
+ typeSel.value = kind;
2286
+ typeSel.addEventListener("click", (e) => e.stopPropagation());
2287
+ typeSel.addEventListener("change", () => {
2288
+ const root = routingEditorState.value || {};
2289
+ routingEditorSetByPath(root, path, routingEditorCoerce(typeSel.value, ""));
2290
+ routingEditorSetDirty(true);
2291
+ routingEditorRender();
2292
+ });
2293
+
2294
+ const valWrap = document.createElement("div");
2295
+ valWrap.className = "kv-val";
2296
+
2297
+ if (kind === "boolean") {
2298
+ const sel = document.createElement("select");
2299
+ sel.innerHTML = `<option value="true">true</option><option value="false">false</option>`;
2300
+ sel.value = value ? "true" : "false";
2301
+ sel.addEventListener("click", (e) => e.stopPropagation());
2302
+ sel.addEventListener("change", () => {
2303
+ const root = routingEditorState.value || {};
2304
+ routingEditorSetByPath(root, path, sel.value === "true");
2305
+ routingEditorSetDirty(true);
2306
+ routingEditorRender();
2307
+ });
2308
+ valWrap.appendChild(sel);
2309
+ } else if (kind === "number") {
2310
+ const inp = document.createElement("input");
2311
+ inp.type = "number";
2312
+ inp.value = String(value);
2313
+ inp.addEventListener("click", (e) => e.stopPropagation());
2314
+ inp.addEventListener("change", () => {
2315
+ const root = routingEditorState.value || {};
2316
+ const n = Number.parseFloat(inp.value);
2317
+ routingEditorSetByPath(root, path, Number.isFinite(n) ? n : 0);
2318
+ routingEditorSetDirty(true);
2319
+ routingEditorRender();
2320
+ });
2321
+ valWrap.appendChild(inp);
2322
+ } else if (kind === "null") {
2323
+ const span = document.createElement("span");
2324
+ span.className = "mono";
2325
+ span.textContent = "null";
2326
+ valWrap.appendChild(span);
2327
+ } else {
2328
+ const inp = document.createElement("input");
2329
+ inp.type = "text";
2330
+ inp.value = value == null ? "" : String(value);
2331
+ inp.addEventListener("click", (e) => e.stopPropagation());
2332
+ inp.addEventListener("change", () => {
2333
+ const root = routingEditorState.value || {};
2334
+ routingEditorSetByPath(root, path, String(inp.value));
2335
+ routingEditorSetDirty(true);
2336
+ routingEditorRender();
2337
+ });
2338
+ valWrap.appendChild(inp);
2339
+
2340
+ if (looksLikeRoutingTargetString(path, value)) {
2341
+ const widget = routingEditorRenderTargetActions(inp.value);
2342
+ if (widget) valWrap.appendChild(widget);
2343
+ }
2344
+ }
2345
+
2346
+ const actions = document.createElement("div");
2347
+ actions.className = "kv-actions";
2348
+ if (!isRoot) {
2349
+ const del = document.createElement("button");
2350
+ del.textContent = "Del";
2351
+ del.className = "danger";
2352
+ del.addEventListener("click", (e) => {
2353
+ e.preventDefault();
2354
+ e.stopPropagation();
2355
+ const root = routingEditorState.value || {};
2356
+ routingEditorDeleteByPath(root, path);
2357
+ routingEditorSetDirty(true);
2358
+ routingEditorRender();
2359
+ });
2360
+ actions.appendChild(del);
2361
+ }
2362
+ const pathEl = document.createElement("span");
2363
+ pathEl.className = "kv-path mono";
2364
+ pathEl.textContent = routingEditorPathToString(path);
2365
+ actions.appendChild(pathEl);
2366
+
2367
+ row.appendChild(keyEl);
2368
+ row.appendChild(typeSel);
2369
+ row.appendChild(valWrap);
2370
+ row.appendChild(actions);
2371
+ return row;
2372
+ }
2373
+
2374
+ const details = document.createElement("details");
2375
+ details.open = isRoot || depth < 1;
2376
+
2377
+ const summary = document.createElement("summary");
2378
+
2379
+ const keyEl = document.createElement("div");
2380
+ keyEl.className = "kv-key";
2381
+ keyEl.textContent = label;
2382
+
2383
+ const metaEl = document.createElement("div");
2384
+ metaEl.className = "kv-meta";
2385
+ metaEl.textContent = kvTypeMeta(value);
2386
+
2387
+ const actions = document.createElement("div");
2388
+ actions.className = "kv-actions";
2389
+
2390
+ if (isArr) {
2391
+ const add = document.createElement("button");
2392
+ add.textContent = "+Item";
2393
+ add.addEventListener("click", (e) => {
2394
+ e.preventDefault();
2395
+ e.stopPropagation();
2396
+ const t = (prompt("Item type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
2397
+ const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
2398
+ let init = "";
2399
+ if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
2400
+ else if (k === "number") init = prompt("Value (number)", "0") || "0";
2401
+ else if (k === "string") init = prompt("Value (string)", "") || "";
2402
+ const v = routingEditorCoerce(k, k === "boolean" ? init === "true" : init);
2403
+ const root = routingEditorState.value || {};
2404
+ const arr = routingEditorGetByPath(root, path);
2405
+ if (Array.isArray(arr)) {
2406
+ arr.push(v);
2407
+ routingEditorSetDirty(true);
2408
+ routingEditorRender();
2409
+ }
2410
+ });
2411
+ actions.appendChild(add);
2412
+ } else {
2413
+ const add = document.createElement("button");
2414
+ add.textContent = "+Key";
2415
+ add.addEventListener("click", (e) => {
2416
+ e.preventDefault();
2417
+ e.stopPropagation();
2418
+ const name = (prompt("Key name", "") || "").trim();
2419
+ if (!name) return;
2420
+ const t = (prompt("Value type: string/number/boolean/null/object/array", "string") || "string").trim().toLowerCase();
2421
+ const k = ["string","number","boolean","null","object","array"].includes(t) ? t : "string";
2422
+ let init = "";
2423
+ if (k === "boolean") init = prompt("Value: true/false", "false") || "false";
2424
+ else if (k === "number") init = prompt("Value (number)", "0") || "0";
2425
+ else if (k === "string") init = prompt("Value (string)", "") || "";
2426
+ const v = routingEditorCoerce(k, k === "boolean" ? init === "true" : init);
2427
+ const root = routingEditorState.value || {};
2428
+ const obj = routingEditorGetByPath(root, path);
2429
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
2430
+ obj[name] = v;
2431
+ routingEditorSetDirty(true);
2432
+ routingEditorRender();
2433
+ }
2434
+ });
2435
+ actions.appendChild(add);
2436
+ }
2437
+
2438
+ if (!isRoot) {
2439
+ const del = document.createElement("button");
2440
+ del.textContent = "Del";
2441
+ del.className = "danger";
2442
+ del.addEventListener("click", (e) => {
2443
+ e.preventDefault();
2444
+ e.stopPropagation();
2445
+ const root = routingEditorState.value || {};
2446
+ routingEditorDeleteByPath(root, path);
2447
+ routingEditorSetDirty(true);
2448
+ routingEditorRender();
2449
+ });
2450
+ actions.appendChild(del);
2451
+ }
2452
+
2453
+ const pathEl = document.createElement("span");
2454
+ pathEl.className = "kv-path mono";
2455
+ pathEl.textContent = routingEditorPathToString(path);
2456
+ actions.appendChild(pathEl);
2457
+
2458
+ summary.appendChild(keyEl);
2459
+ summary.appendChild(metaEl);
2460
+ summary.appendChild(actions);
2461
+ details.appendChild(summary);
2462
+
2463
+ if (isArr) {
2464
+ for (let i = 0; i < value.length; i += 1) {
2465
+ details.appendChild(routingEditorRenderNode(String(i), value[i], path.concat([i]), depth + 1));
2466
+ }
2467
+ return details;
2468
+ }
2469
+
2470
+ let keys = [];
2471
+ try {
2472
+ keys = Object.keys(value);
2473
+ keys.sort((a, b) => a.localeCompare(b));
2474
+ } catch {
2475
+ keys = [];
2476
+ }
2477
+ for (const childKey of keys) {
2478
+ details.appendChild(routingEditorRenderNode(childKey, value[childKey], path.concat([childKey]), depth + 1));
2479
+ }
2480
+
2481
+ return details;
2482
+ }
2483
+
2484
+ async function loadRouting() {
2485
+ setLog("routingOpLog", "");
2486
+ const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
2487
+ if (!auth || !auth.authenticated) {
2488
+ notifyUnauthorizedOnce("routing");
2489
+ return;
2490
+ }
2491
+ try {
2492
+ const selectedPath = textOf($("routingSourceSelect").value || "").trim();
2493
+ const query = selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
2494
+ const out = await apiFetch(`/config/routing${query}`);
2495
+ UI.routingLocation = out.location || "virtualrouter.routing";
2496
+ routingEditorSetValue(out.routing || {});
2497
+ setLog("routingOpLog", `Loaded. Path: ${out.path || "—"}\nLocation: ${UI.routingLocation}`);
2498
+ toast("Routing loaded.", "ok");
2499
+ } catch (e) {
2500
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing");
2501
+ setLog("routingOpLog", `Load failed: ${e.message}`);
2502
+ toast(`Load failed: ${e.message}`);
2503
+ }
2504
+ }
2505
+
2506
+ async function saveRouting() {
2507
+ setLog("routingOpLog", "");
2508
+ const auth = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
2509
+ if (!auth || !auth.authenticated) {
2510
+ notifyUnauthorizedOnce("routing save");
2511
+ return;
2512
+ }
2513
+ try {
2514
+ const selectedPath = textOf($("routingSourceSelect").value || "").trim();
2515
+ const query = selectedPath ? `?path=${encodeURIComponent(selectedPath)}` : "";
2516
+ const routing = routingEditorGetValue() || {};
2517
+ const out = await apiFetch(`/config/routing${query}`, {
2518
+ method: "PUT",
2519
+ body: JSON.stringify({ routing, location: UI.routingLocation, path: selectedPath || undefined })
2520
+ });
2521
+ UI.routingLocation = out.location || UI.routingLocation;
2522
+ setLog("routingOpLog", `Saved. Path: ${out.path || "—"}\nLocation: ${UI.routingLocation}\nRestart required to apply.`);
2523
+ toast("Routing saved.", "ok");
2524
+ routingEditorSetDirty(false);
2525
+ } catch (e) {
2526
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing save");
2527
+ setLog("routingOpLog", `Save failed: ${e.message}`);
2528
+ toast(`Save failed: ${e.message}`);
2529
+ }
2530
+ }
2531
+
2532
+ async function refreshRoutingSources() {
2533
+ const select = $("routingSourceSelect");
2534
+ if (!select) return;
2535
+ const prev = textOf(select.value || "");
2536
+ try {
2537
+ const out = await apiFetch("/config/routing/sources");
2538
+ const sources = Array.isArray(out && out.sources) ? out.sources : [];
2539
+ UI.routingSources = sources;
2540
+ UI.routingSourcesUpdatedAt = Date.now();
2541
+
2542
+ select.replaceChildren();
2543
+ for (const s of sources) {
2544
+ const opt = document.createElement("option");
2545
+ opt.value = textOf(s.path || "");
2546
+ const version = s.version ? ` v=${s.version}` : "";
2547
+ const loc = s.location ? ` (${s.location})` : "";
2548
+ const kind = s.kind ? `[${s.kind}] ` : "";
2549
+ opt.textContent = `${kind}${textOf(s.label || s.path || "")}${version}${loc}`;
2550
+ select.appendChild(opt);
2551
+ }
2552
+
2553
+ const active = textOf(out && out.activePath ? out.activePath : "");
2554
+ const hasPrev = sources.some((s) => textOf(s.path) === prev);
2555
+ const hasActive = sources.some((s) => textOf(s.path) === active);
2556
+ if (hasPrev) select.value = prev;
2557
+ else if (hasActive) select.value = active;
2558
+
2559
+ toast("Routing sources refreshed.", "ok");
2560
+ } catch (e) {
2561
+ if (e && e.status === 401) notifyUnauthorizedOnce("routing sources");
2562
+ toast(`Routing sources failed: ${e && e.message ? e.message : String(e)}`);
2563
+ }
2564
+ }
2565
+
2566
+ async function refreshRuntimes() {
2567
+ try {
2568
+ const quota = await apiFetch("/quota/providers");
2569
+ const quotaProviders = quota && Array.isArray(quota.providers) ? quota.providers : [];
2570
+ UI.quotaProviders = quotaProviders;
2571
+ UI.quotaProvidersUpdatedAt = Date.now();
2572
+ UI.quotaProviderMap = new Map(quotaProviders.map((q) => [textOf(q.providerKey), q]));
2573
+ } catch (e) {
2574
+ if (e && e.status === 401) notifyUnauthorizedOnce("pool status");
2575
+ throw e;
2576
+ }
2577
+ }
2578
+
2579
+ function formatEpochMs(ms) {
2580
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "—";
1087
2581
  try {
1088
- const out = await apiFetch("/daemon/oauth/authorize", {
1089
- method: "POST",
1090
- body: JSON.stringify({ provider, alias, openBrowser, forceReauthorize })
1091
- });
1092
- setLog("credentialOpLog", `OK. tokenFile: ${out.tokenFile || "—"}`);
1093
- await refreshCredentials();
1094
- } catch (e) {
1095
- setLog("credentialOpLog", `Authorize failed: ${e.message}`);
2582
+ return new Date(ms).toLocaleString();
2583
+ } catch {
2584
+ return String(ms);
1096
2585
  }
1097
2586
  }
1098
2587
 
1099
- async function loadRouting() {
1100
- setLog("routingOpLog", "");
1101
- try {
1102
- const out = await apiFetch("/config/routing");
1103
- $("routingEditor").value = JSON.stringify(out.routing || {}, null, 2);
1104
- setLog("routingOpLog", `Loaded. Path: ${out.path || "—"}`);
1105
- } catch (e) {
1106
- setLog("routingOpLog", `Load failed: ${e.message}`);
1107
- }
2588
+ function formatDurationMs(ms) {
2589
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
2590
+ const abs = Math.abs(ms);
2591
+ const sign = ms < 0 ? "-" : "";
2592
+ const s = Math.round(abs / 1000);
2593
+ if (s < 60) return `${sign}${s}s`;
2594
+ const m = Math.round(s / 60);
2595
+ if (m < 60) return `${sign}${m}m`;
2596
+ const h = Math.round(m / 60);
2597
+ if (h < 48) return `${sign}${h}h`;
2598
+ const d = Math.round(h / 24);
2599
+ return `${sign}${d}d`;
1108
2600
  }
1109
2601
 
1110
- async function saveRouting() {
1111
- setLog("routingOpLog", "");
1112
- try {
1113
- const routing = JSON.parse($("routingEditor").value || "{}");
1114
- const out = await apiFetch("/config/routing", {
1115
- method: "PUT",
1116
- body: JSON.stringify({ routing })
1117
- });
1118
- setLog("routingOpLog", `Saved. Path: ${out.path || ""}\nRestart required to apply.`);
1119
- } catch (e) {
1120
- setLog("routingOpLog", `Save failed: ${e.message}`);
2602
+ function formatEpochWithDelta(ms) {
2603
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "";
2604
+ const delta = ms - Date.now();
2605
+ const tail = delta >= 0 ? `in ${formatDurationMs(delta)}` : `${formatDurationMs(delta)} ago`;
2606
+ return `${formatEpochMs(ms)} (${tail})`;
2607
+ }
2608
+
2609
+ function pill(text, kind) {
2610
+ const span = document.createElement("span");
2611
+ span.className = `pill ${kind || ""}`.trim();
2612
+ span.textContent = textOf(text);
2613
+ return span;
2614
+ }
2615
+
2616
+ function extractRoutingTargets(routing) {
2617
+ const out = new Set();
2618
+ if (!routing || typeof routing !== "object") return out;
2619
+ for (const routeName of Object.keys(routing)) {
2620
+ const pools = routing[routeName];
2621
+ if (!Array.isArray(pools)) continue;
2622
+ for (const pool of pools) {
2623
+ if (!pool || typeof pool !== "object") continue;
2624
+ const targets = pool.targets;
2625
+ if (!Array.isArray(targets)) continue;
2626
+ for (const t of targets) {
2627
+ if (typeof t === "string" && t.trim()) out.add(t.trim());
2628
+ }
2629
+ }
1121
2630
  }
2631
+ return out;
1122
2632
  }
1123
2633
 
1124
- async function refreshRuntimes() {
1125
- const body = $("runtimesTbody");
1126
- body.replaceChildren();
1127
- try {
1128
- const items = await apiFetch("/providers/runtimes");
1129
- for (const r of items || []) {
1130
- const tr = document.createElement("tr");
1131
- tr.appendChild(createCell("td", r.providerKey || "", "mono truncate", { title: true }));
1132
- tr.appendChild(createCell("td", r.runtimeKey || "", "mono truncate", { title: true }));
1133
- tr.appendChild(createCell("td", r.family || "", ""));
1134
- tr.appendChild(createCell("td", r.protocol || "", ""));
1135
- tr.appendChild(createCell("td", r.series || "", ""));
1136
- body.appendChild(tr);
2634
+ function resolveRoutedProviderKeys(routingTargets, providers) {
2635
+ const targets = routingTargets instanceof Set ? Array.from(routingTargets) : [];
2636
+ const list = Array.isArray(providers) ? providers : [];
2637
+ const keys = [];
2638
+ for (const p of list) {
2639
+ const k = textOf(p && p.providerKey ? p.providerKey : "");
2640
+ if (k) keys.push(k);
2641
+ }
2642
+ const known = new Set(keys);
2643
+ const resolved = new Set();
2644
+ if (!targets.length || !known.size) return resolved;
2645
+
2646
+ for (const t of targets) {
2647
+ const target = textOf(t);
2648
+ if (!target) continue;
2649
+ if (known.has(target)) {
2650
+ resolved.add(target);
2651
+ continue;
2652
+ }
2653
+ const dot = target.indexOf(".");
2654
+ if (dot <= 0) continue;
2655
+ const providerId = target.slice(0, dot);
2656
+ const modelId = target.slice(dot + 1);
2657
+ if (!providerId || !modelId) continue;
2658
+ const prefix = `${providerId}.`;
2659
+ const suffix = `.${modelId}`;
2660
+ for (const k of known) {
2661
+ if (k.startsWith(prefix) && k.endsWith(suffix)) resolved.add(k);
1137
2662
  }
1138
- } catch (e) {
1139
- body.appendChild(createErrorRow(5, e && e.message ? e.message : e));
1140
2663
  }
2664
+ return resolved;
1141
2665
  }
1142
2666
 
1143
- function formatEpochMs(ms) {
1144
- if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "—";
2667
+ async function refreshRoutingTargets() {
1145
2668
  try {
1146
- return new Date(ms).toLocaleString();
2669
+ const routingOut = await apiFetch("/config/routing");
2670
+ UI.routingTargets = extractRoutingTargets(routingOut && routingOut.routing ? routingOut.routing : {});
2671
+ UI.routingTargetsUpdatedAt = Date.now();
1147
2672
  } catch {
1148
- return String(ms);
2673
+ UI.routingTargets = null;
2674
+ UI.routingTargetsUpdatedAt = Date.now();
1149
2675
  }
1150
2676
  }
1151
2677
 
@@ -1154,43 +2680,257 @@
1154
2680
  body.replaceChildren();
1155
2681
  setLog("quotaOpLog", "");
1156
2682
  try {
2683
+ // Load routing targets so we can filter out providers not referenced by routing pools.
2684
+ await refreshRoutingTargets();
2685
+ // Force refresh quota first so UI doesn't show stale pool state.
2686
+ try {
2687
+ await apiFetch("/daemon/modules/quota/refresh", { method: "POST" });
2688
+ } catch (e) {
2689
+ // Backwards compatibility: older servers may only support reset.
2690
+ if (e && e.status === 404) {
2691
+ try {
2692
+ await apiFetch("/daemon/modules/quota/reset", { method: "POST" });
2693
+ } catch (e2) {
2694
+ throw e2;
2695
+ }
2696
+ } else {
2697
+ throw e;
2698
+ }
2699
+ }
1157
2700
  const out = await apiFetch("/quota/providers");
1158
- const list = Array.isArray(out.providers) ? out.providers : [];
1159
- for (const q of list) {
2701
+ UI.quotaProviders = Array.isArray(out.providers) ? out.providers : [];
2702
+ UI.quotaProvidersUpdatedAt = Date.now();
2703
+ renderQuotaProviders();
2704
+ } catch (e) {
2705
+ if (e && e.status === 401) notifyUnauthorizedOnce("quota");
2706
+ else toast(e && e.message ? e.message : String(e || "quota refresh failed"));
2707
+ body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
2708
+ }
2709
+ }
2710
+
2711
+ function renderQuotaProviders() {
2712
+ const body = $("quotaTbody");
2713
+ body.replaceChildren();
2714
+
2715
+ const filter = textOf($("quotaFilterInput").value || "").trim().toLowerCase();
2716
+ const hideOk = Boolean($("quotaHideOkToggle").checked);
2717
+ const onlyRoutedTargets = Boolean($("quotaOnlyRoutedTargetsToggle").checked);
2718
+ const routedProviderKeys = onlyRoutedTargets ? resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders) : null;
2719
+
2720
+ const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
2721
+ const next = list
2722
+ .filter((q) => {
2723
+ const key = textOf(q && q.providerKey ? q.providerKey : "").toLowerCase();
2724
+ if (filter && !key.includes(filter)) return false;
2725
+ if (hideOk && q && q.inPool === true) return false;
2726
+ if (routedProviderKeys && !routedProviderKeys.has(textOf(q && q.providerKey ? q.providerKey : ""))) return false;
2727
+ return true;
2728
+ })
2729
+ .slice();
2730
+
2731
+ next.sort((a, b) => {
2732
+ const aIn = a && a.inPool === true ? 1 : 0;
2733
+ const bIn = b && b.inPool === true ? 1 : 0;
2734
+ if (aIn !== bIn) return aIn - bIn; // inPool=false first
2735
+ const aUntil = Math.max(Number(a?.blacklistUntil || 0), Number(a?.cooldownUntil || 0));
2736
+ const bUntil = Math.max(Number(b?.blacklistUntil || 0), Number(b?.cooldownUntil || 0));
2737
+ if (aUntil !== bUntil) return bUntil - aUntil;
2738
+ return textOf(a?.providerKey).localeCompare(textOf(b?.providerKey));
2739
+ });
2740
+
2741
+ if (!next.length) {
2742
+ body.appendChild(createErrorRow(8, "No providers matched filter."));
2743
+ return;
2744
+ }
2745
+
2746
+ function splitProviderKey(providerKey) {
2747
+ const raw = textOf(providerKey || "");
2748
+ const parts = raw.split(".");
2749
+ if (parts.length >= 3) {
2750
+ return { providerId: parts[0], authAlias: parts[1], model: parts.slice(2).join(".") };
2751
+ }
2752
+ if (parts.length === 2) {
2753
+ return { providerId: parts[0], authAlias: "", model: parts[1] };
2754
+ }
2755
+ return { providerId: raw, authAlias: "", model: "" };
2756
+ }
2757
+
2758
+ const byProvider = new Map();
2759
+ for (const q of next) {
2760
+ const pk = textOf(q && q.providerKey ? q.providerKey : "");
2761
+ const { providerId, authAlias, model } = splitProviderKey(pk);
2762
+ if (!byProvider.has(providerId)) byProvider.set(providerId, new Map());
2763
+ const byAlias = byProvider.get(providerId);
2764
+ if (!byAlias.has(authAlias)) byAlias.set(authAlias, []);
2765
+ byAlias.get(authAlias).push({ q, pk, providerId, authAlias, model });
2766
+ }
2767
+
2768
+ const providerIds = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
2769
+ const groupCols = 8;
2770
+
2771
+ const appendGroupRow = (label, indent = false) => {
2772
+ const tr = document.createElement("tr");
2773
+ tr.className = "group-row";
2774
+ const td = document.createElement("td");
2775
+ td.colSpan = groupCols;
2776
+ td.textContent = label;
2777
+ if (indent) td.className = "indent";
2778
+ tr.appendChild(td);
2779
+ body.appendChild(tr);
2780
+ };
2781
+
2782
+ for (const providerId of providerIds) {
2783
+ const byAlias = byProvider.get(providerId);
2784
+ let modelsCount = 0;
2785
+ for (const v of byAlias.values()) modelsCount += v.length;
2786
+ appendGroupRow(`${providerId} (${modelsCount})`);
2787
+
2788
+ const aliases = Array.from(byAlias.keys()).sort((a, b) => a.localeCompare(b));
2789
+ for (const alias of aliases) {
2790
+ const items = byAlias.get(alias);
2791
+ appendGroupRow(`${alias || "(no-key)"} (${items.length})`, true);
2792
+
2793
+ for (const item of items) {
2794
+ const q = item.q;
2795
+ const tr = document.createElement("tr");
2796
+ tr.className = "provider-row";
2797
+ tr.setAttribute("data-provider-key", item.pk);
2798
+
2799
+ tr.appendChild(createCell("td", "", "mono"));
2800
+ tr.appendChild(createCell("td", "", "mono"));
2801
+ tr.appendChild(createCell("td", item.model || item.pk, "mono indent", { title: true }));
2802
+
2803
+ const inPool = Boolean(q.inPool);
2804
+ const inTd = document.createElement("td");
2805
+ inTd.appendChild(pill(inPool ? "true" : "false", inPool ? "ok" : "bad"));
2806
+ tr.appendChild(inTd);
2807
+
2808
+ const reason = textOf(q.reason || "");
2809
+ const reasonKind =
2810
+ reason === "ok" ? "ok" :
2811
+ reason === "cooldown" ? "warn" :
2812
+ reason === "blacklist" || reason === "fatal" || reason === "quotaDepleted" ? "bad" : "warn";
2813
+ const reasonTd = document.createElement("td");
2814
+ reasonTd.appendChild(pill(reason || "—", reasonKind));
2815
+ tr.appendChild(reasonTd);
2816
+
2817
+ const cooldown = formatEpochWithDelta(q.cooldownUntil);
2818
+ const blacklist = formatEpochWithDelta(q.blacklistUntil);
2819
+ const until = `cooldown=${cooldown || "—"} blacklist=${blacklist || "—"}`;
2820
+ tr.appendChild(createCell("td", until, "mono", { title: true }));
2821
+
2822
+ tr.appendChild(createCell("td", q.consecutiveErrorCount ?? 0, "mono"));
2823
+
2824
+ const actionsTd = document.createElement("td");
2825
+ actionsTd.className = "actions-cell";
2826
+ const box = document.createElement("div");
2827
+ box.className = "actions";
2828
+ const recover = document.createElement("button");
2829
+ recover.textContent = "Recover";
2830
+ recover.setAttribute("data-action", "quota-recover");
2831
+ recover.setAttribute("data-key", item.pk);
2832
+ const reset = document.createElement("button");
2833
+ reset.textContent = "Reset";
2834
+ reset.setAttribute("data-action", "quota-reset");
2835
+ reset.setAttribute("data-key", item.pk);
2836
+ const disable = document.createElement("button");
2837
+ disable.textContent = "Offline…";
2838
+ disable.className = "danger";
2839
+ disable.setAttribute("data-action", "quota-disable");
2840
+ disable.setAttribute("data-key", item.pk);
2841
+ box.appendChild(recover);
2842
+ box.appendChild(reset);
2843
+ box.appendChild(disable);
2844
+ actionsTd.appendChild(box);
2845
+ tr.appendChild(actionsTd);
2846
+
2847
+ body.appendChild(tr);
2848
+ }
2849
+ }
2850
+ }
2851
+ }
2852
+
2853
+ function formatRemainingFraction(v) {
2854
+ if (typeof v !== "number" || !Number.isFinite(v)) return "—";
2855
+ const pct = Math.max(0, Math.min(1, v)) * 100;
2856
+ return `${pct.toFixed(1)}%`;
2857
+ }
2858
+
2859
+ async function refreshQuotaSnapshot() {
2860
+ const body = $("quotaSnapshotTbody");
2861
+ body.replaceChildren();
2862
+ setLog("quotaSnapshotLog", "");
2863
+ try {
2864
+ // Ensure routing + provider pool snapshots exist so we can filter routed models.
2865
+ await refreshRoutingTargets();
2866
+ if (!Array.isArray(UI.quotaProviders) || UI.quotaProviders.length === 0) {
2867
+ try {
2868
+ const outProviders = await apiFetch("/quota/providers");
2869
+ UI.quotaProviders = Array.isArray(outProviders.providers) ? outProviders.providers : [];
2870
+ UI.quotaProvidersUpdatedAt = Date.now();
2871
+ } catch {
2872
+ // ignore: snapshot view can still render unfiltered
2873
+ }
2874
+ }
2875
+ const out = await apiFetch("/quota/summary");
2876
+ let list = Array.isArray(out.records) ? out.records : [];
2877
+
2878
+ const onlyRouted = Boolean($("quotaSnapshotOnlyRoutedToggle").checked);
2879
+ if (onlyRouted) {
2880
+ const routedProviderKeys = resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders);
2881
+ const allowedSnapshotKeys = new Set();
2882
+ for (const providerKey of routedProviderKeys) {
2883
+ if (!providerKey.toLowerCase().startsWith("antigravity.")) continue;
2884
+ const parts = providerKey.split(".");
2885
+ if (parts.length < 3) continue;
2886
+ const alias = parts[1];
2887
+ const modelId = parts.slice(2).join(".");
2888
+ if (!alias || !modelId) continue;
2889
+ allowedSnapshotKeys.add(`antigravity://${alias}/${modelId}`);
2890
+ }
2891
+ list = list.filter((r) => allowedSnapshotKeys.has(textOf(r && r.key ? r.key : "")));
2892
+ }
2893
+
2894
+ list.sort((a, b) => String(a.key || "").localeCompare(String(b.key || "")));
2895
+ for (const r of list) {
2896
+ const raw = textOf(r && r.key ? r.key : "");
2897
+ const prefix = "antigravity://";
2898
+ const rest = raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
2899
+ const parts = rest.split("/");
2900
+ const alias = parts.length >= 2 ? parts[0] : "";
2901
+ const model = parts.length >= 2 ? parts.slice(1).join("/") : rest;
1160
2902
  const tr = document.createElement("tr");
1161
- tr.appendChild(createCell("td", q.providerKey || "", "mono truncate", { title: true }));
1162
- tr.appendChild(createCell("td", q.authType || "", ""));
1163
- tr.appendChild(createCell("td", String(Boolean(q.inPool)), ""));
1164
- tr.appendChild(createCell("td", q.reason || "", ""));
1165
- tr.appendChild(createCell("td", formatEpochMs(q.cooldownUntil), "mono"));
1166
- tr.appendChild(createCell("td", formatEpochMs(q.blacklistUntil), "mono"));
1167
- tr.appendChild(createCell("td", q.consecutiveErrorCount ?? 0, "mono"));
1168
- const actionsTd = document.createElement("td");
1169
- actionsTd.className = "actions-cell";
1170
- const box = document.createElement("div");
1171
- box.className = "actions";
1172
- const recover = document.createElement("button");
1173
- recover.textContent = "Recover";
1174
- recover.setAttribute("data-action", "quota-recover");
1175
- recover.setAttribute("data-key", textOf(q.providerKey));
1176
- const reset = document.createElement("button");
1177
- reset.textContent = "Reset";
1178
- reset.setAttribute("data-action", "quota-reset");
1179
- reset.setAttribute("data-key", textOf(q.providerKey));
1180
- const disable = document.createElement("button");
1181
- disable.textContent = "Disable…";
1182
- disable.className = "danger";
1183
- disable.setAttribute("data-action", "quota-disable");
1184
- disable.setAttribute("data-key", textOf(q.providerKey));
1185
- box.appendChild(recover);
1186
- box.appendChild(reset);
1187
- box.appendChild(disable);
1188
- actionsTd.appendChild(box);
1189
- tr.appendChild(actionsTd);
2903
+ tr.appendChild(createCell("td", alias || "", "mono", { title: true }));
2904
+ tr.appendChild(createCell("td", model || "", "mono", { title: true }));
2905
+ tr.appendChild(createCell("td", formatRemainingFraction(r.remainingFraction), "mono"));
2906
+ tr.appendChild(createCell("td", formatEpochWithDelta(r.resetAt), "mono", { title: true }));
2907
+ tr.appendChild(createCell("td", formatEpochMs(r.fetchedAt), "mono", { title: true }));
1190
2908
  body.appendChild(tr);
1191
2909
  }
2910
+ if (!list.length) {
2911
+ body.appendChild(createErrorRow(5, "No quota records to show (check filter or click Refresh antigravity snapshot)."));
2912
+ }
1192
2913
  } catch (e) {
1193
- body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
2914
+ if (e && e.status === 401) notifyUnauthorizedOnce("quota snapshot");
2915
+ body.appendChild(createErrorRow(5, e && e.message ? e.message : e));
2916
+ }
2917
+ }
2918
+
2919
+ async function refreshQuotaSnapshotNow() {
2920
+ setLog("quotaSnapshotLog", "");
2921
+ try {
2922
+ const out = await apiFetch("/quota/refresh", { method: "POST" });
2923
+ const result = out && out.result ? out.result : null;
2924
+ setLog(
2925
+ "quotaSnapshotLog",
2926
+ `OK. refreshedAt=${result && result.refreshedAt ? formatEpochMs(result.refreshedAt) : "—"} tokenCount=${result && typeof result.tokenCount === "number" ? result.tokenCount : "—"} records=${result && typeof result.recordCount === "number" ? result.recordCount : "—"}`
2927
+ );
2928
+ await refreshQuotaSnapshot();
2929
+ toast("Quota snapshot refreshed.", "ok");
2930
+ } catch (e) {
2931
+ if (e && e.status === 401) notifyUnauthorizedOnce("quota refresh");
2932
+ setLog("quotaSnapshotLog", `Refresh failed: ${e && e.message ? e.message : e}`);
2933
+ toast(`Refresh failed: ${e && e.message ? e.message : String(e)}`);
1194
2934
  }
1195
2935
  }
1196
2936
 
@@ -1311,36 +3051,43 @@
1311
3051
 
1312
3052
  async function quotaAction(kind, providerKey) {
1313
3053
  setLog("quotaOpLog", "");
1314
- if (!providerKey) return;
3054
+ if (!providerKey) {
3055
+ setLog("quotaOpLog", "providerKey required");
3056
+ toast("providerKey required");
3057
+ return;
3058
+ }
1315
3059
  try {
1316
3060
  if (kind === "recover") {
1317
3061
  await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/recover`, { method: "POST" });
1318
3062
  await refreshQuota();
3063
+ toast("Recovered.", "ok");
1319
3064
  return;
1320
3065
  }
1321
3066
  if (kind === "reset") {
1322
3067
  await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/reset`, { method: "POST" });
1323
3068
  await refreshQuota();
3069
+ toast("Reset.", "ok");
1324
3070
  return;
1325
3071
  }
1326
3072
  if (kind === "disable") {
1327
- const minutesRaw = prompt("Disable duration (minutes)", "60");
1328
- if (!minutesRaw) return;
1329
- const minutes = Number.parseFloat(minutesRaw);
3073
+ const minutes = Number.parseFloat(textOf($("quotaDurationSelect").value || "60"));
1330
3074
  if (!Number.isFinite(minutes) || minutes <= 0) {
1331
3075
  throw new Error("Invalid minutes");
1332
3076
  }
1333
- const modeRaw = prompt("Mode: cooldown or blacklist", "cooldown");
1334
- const mode = (modeRaw || "cooldown").trim().toLowerCase() === "blacklist" ? "blacklist" : "cooldown";
3077
+ const modeRaw = (textOf($("quotaModeSelect").value) || "cooldown").trim().toLowerCase();
3078
+ const mode = modeRaw === "blacklist" ? "blacklist" : "cooldown";
1335
3079
  await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/disable`, {
1336
3080
  method: "POST",
1337
3081
  body: JSON.stringify({ mode, durationMinutes: minutes })
1338
3082
  });
1339
3083
  await refreshQuota();
3084
+ toast("Applied.", "ok");
1340
3085
  return;
1341
3086
  }
1342
3087
  } catch (e) {
3088
+ if (e && e.status === 401) notifyUnauthorizedOnce("quota action");
1343
3089
  setLog("quotaOpLog", `Action failed: ${e && e.message ? e.message : e}`);
3090
+ toast(`Action failed: ${e && e.message ? e.message : String(e)}`);
1344
3091
  }
1345
3092
  }
1346
3093
 
@@ -1351,8 +3098,11 @@
1351
3098
  const out = await apiFetch("/daemon/modules/provider-quota/reset", { method: "POST" });
1352
3099
  setLog("quotaOpLog", `OK. resetAt=${out.resetAt || "—"}`);
1353
3100
  await refreshQuota();
3101
+ toast("Quota module reset.", "ok");
1354
3102
  } catch (e) {
3103
+ if (e && e.status === 401) notifyUnauthorizedOnce("quota reset");
1355
3104
  setLog("quotaOpLog", `Reset failed: ${e.message}`);
3105
+ toast(`Reset failed: ${e.message}`);
1356
3106
  }
1357
3107
  }
1358
3108
 
@@ -1361,6 +3111,119 @@
1361
3111
  btn.addEventListener("click", () => selectTab(btn.getAttribute("data-tab")));
1362
3112
  });
1363
3113
 
3114
+ async function refreshAdminAuthStatus() {
3115
+ try {
3116
+ const status = await apiFetch("/daemon/auth/status", { method: "GET" });
3117
+ if (status && status.ok) {
3118
+ const hasPassword = Boolean(status.hasPassword);
3119
+ const authed = Boolean(status.authenticated);
3120
+ UI.adminAuth = { hasPassword, authenticated: authed };
3121
+ $("adminAuthHint").textContent = authed ? "authenticated" : hasPassword ? "login required" : "setup required (localhost only)";
3122
+ $("setupPasswordBtn").style.display = hasPassword ? "none" : "inline-block";
3123
+ $("loginPasswordBtn").style.display = hasPassword ? "inline-block" : "none";
3124
+ $("changePasswordDetails").style.display = hasPassword && authed ? "block" : "none";
3125
+ return { hasPassword, authenticated: authed };
3126
+ }
3127
+ } catch (e) {
3128
+ const msg = e && e.message ? e.message : String(e);
3129
+ $("adminAuthHint").textContent = msg;
3130
+ toast(`Admin auth status failed: ${msg}`);
3131
+ }
3132
+ UI.adminAuth = { hasPassword: false, authenticated: false };
3133
+ $("changePasswordDetails").style.display = "none";
3134
+ return { hasPassword: false, authenticated: false };
3135
+ }
3136
+
3137
+ $("adminPasswordInput").addEventListener("keydown", async (ev) => {
3138
+ if (ev.key !== "Enter") return;
3139
+ ev.preventDefault();
3140
+ const status = UI.adminAuth ? UI.adminAuth : await refreshAdminAuthStatus();
3141
+ if (status.hasPassword) $("loginPasswordBtn").click();
3142
+ else $("setupPasswordBtn").click();
3143
+ });
3144
+
3145
+ $("setupPasswordBtn").addEventListener("click", async () => {
3146
+ const pw = ($("adminPasswordInput").value || "");
3147
+ try {
3148
+ $("adminAuthHint").textContent = "setting password…";
3149
+ await apiFetch("/daemon/auth/setup", { method: "POST", body: JSON.stringify({ password: pw }) });
3150
+ $("adminPasswordInput").value = "";
3151
+ $("oldAdminPasswordInput").value = "";
3152
+ $("newAdminPasswordInput").value = "";
3153
+ toast("Password set. You're now logged in.", "ok");
3154
+ await refreshAdminAuthStatus();
3155
+ await refreshStatus();
3156
+ await refreshProviders();
3157
+ await refreshCredentials();
3158
+ await refreshQuota();
3159
+ await refreshQuotaSnapshot();
3160
+ await refreshRuntimes();
3161
+ await refreshRoutingSources();
3162
+ await loadRouting();
3163
+ } catch (e) {
3164
+ const msg = e && e.message ? e.message : String(e);
3165
+ $("adminAuthHint").textContent = msg;
3166
+ toast(`Set password failed: ${msg}`);
3167
+ }
3168
+ });
3169
+
3170
+ $("loginPasswordBtn").addEventListener("click", async () => {
3171
+ const pw = ($("adminPasswordInput").value || "");
3172
+ try {
3173
+ $("adminAuthHint").textContent = "logging in…";
3174
+ await apiFetch("/daemon/auth/login", { method: "POST", body: JSON.stringify({ password: pw }) });
3175
+ $("adminPasswordInput").value = "";
3176
+ $("oldAdminPasswordInput").value = "";
3177
+ $("newAdminPasswordInput").value = "";
3178
+ toast("Logged in.", "ok");
3179
+ await refreshAdminAuthStatus();
3180
+ await refreshStatus();
3181
+ await refreshProviders();
3182
+ await refreshCredentials();
3183
+ await refreshQuota();
3184
+ await refreshRuntimes();
3185
+ await refreshRoutingSources();
3186
+ await loadRouting();
3187
+ } catch (e) {
3188
+ const msg = e && e.message ? e.message : String(e);
3189
+ $("adminAuthHint").textContent = msg;
3190
+ toast(`Login failed: ${msg}`);
3191
+ }
3192
+ });
3193
+
3194
+ $("logoutPasswordBtn").addEventListener("click", async () => {
3195
+ try {
3196
+ $("adminAuthHint").textContent = "logging out…";
3197
+ await apiFetch("/daemon/auth/logout", { method: "POST" });
3198
+ toast("Logged out.", "ok");
3199
+ await refreshAdminAuthStatus();
3200
+ } catch (e) {
3201
+ const msg = e && e.message ? e.message : String(e);
3202
+ $("adminAuthHint").textContent = msg;
3203
+ toast(`Logout failed: ${msg}`);
3204
+ }
3205
+ });
3206
+
3207
+ $("changePasswordBtn").addEventListener("click", async () => {
3208
+ const oldPassword = ($("oldAdminPasswordInput").value || "");
3209
+ const newPassword = ($("newAdminPasswordInput").value || "");
3210
+ try {
3211
+ $("adminAuthHint").textContent = "changing password…";
3212
+ await apiFetch("/daemon/auth/change", {
3213
+ method: "POST",
3214
+ body: JSON.stringify({ oldPassword, newPassword })
3215
+ });
3216
+ $("oldAdminPasswordInput").value = "";
3217
+ $("newAdminPasswordInput").value = "";
3218
+ toast("Password changed.", "ok");
3219
+ await refreshAdminAuthStatus();
3220
+ } catch (e) {
3221
+ const msg = e && e.message ? e.message : String(e);
3222
+ $("adminAuthHint").textContent = msg;
3223
+ toast(`Change password failed: ${msg}`);
3224
+ }
3225
+ });
3226
+
1364
3227
  $("saveApiKeyBtn").addEventListener("click", () => {
1365
3228
  const value = ($("apiKeyInput").value || "").trim();
1366
3229
  setApiKey(value);
@@ -1387,14 +3250,15 @@
1387
3250
  const out = await apiFetch("/daemon/restart", { method: "POST" });
1388
3251
  const warnings = Array.isArray(out.warnings) && out.warnings.length ? `\nWarnings:\n- ${out.warnings.join("\n- ")}` : "";
1389
3252
  setLog("providerOpLog", `Restarted.\nconfigPath: ${out.configPath || "—"}\nreloadedAt: ${out.reloadedAt || "—"}${warnings}`);
1390
- await refreshStatus();
1391
- await refreshProviders();
1392
- await refreshCredentials();
1393
- await refreshQuota();
1394
- await refreshRuntimes();
1395
- } catch (e) {
1396
- setLog("providerOpLog", `Restart failed: ${e.message}`);
1397
- }
3253
+ await refreshStatus();
3254
+ await refreshProviders();
3255
+ await refreshCredentials();
3256
+ await refreshQuota();
3257
+ await refreshQuotaSnapshot();
3258
+ await refreshRuntimes();
3259
+ } catch (e) {
3260
+ setLog("providerOpLog", `Restart failed: ${e.message}`);
3261
+ }
1398
3262
  });
1399
3263
 
1400
3264
  $("refreshProvidersBtn").addEventListener("click", refreshProviders);
@@ -1402,20 +3266,36 @@
1402
3266
  $("newProviderBtn").addEventListener("click", () => {
1403
3267
  $("providerEditorTitle").textContent = "Provider editor (new)";
1404
3268
  $("providerIdInput").value = "";
1405
- $("providerJsonEditor").value = JSON.stringify(presetFor($("providerPreset").value), null, 2);
3269
+ setSelectedProviderId("");
3270
+ providerEditorSetValue(presetFor($("providerPreset").value));
1406
3271
  setLog("providerOpLog", "");
1407
3272
  });
1408
3273
  $("providersTbody").addEventListener("click", async (ev) => {
1409
3274
  const btn = ev.target.closest("button");
1410
- if (!btn) return;
1411
- const id = btn.getAttribute("data-id");
1412
- const action = btn.getAttribute("data-action");
1413
- if (action === "test" && id) {
1414
- await testProviderFromPool(id);
3275
+ if (btn) {
3276
+ const id = btn.getAttribute("data-id");
3277
+ const action = btn.getAttribute("data-action");
3278
+ if (action === "test" && id) {
3279
+ setSelectedProviderId(id);
3280
+ await testProviderFromPool(id);
3281
+ return;
3282
+ }
3283
+ if (action === "edit" && id) {
3284
+ await loadProvider(id);
3285
+ return;
3286
+ }
3287
+ if (action === "delete" && id) {
3288
+ await deleteProvider(id);
3289
+ return;
3290
+ }
1415
3291
  return;
1416
3292
  }
1417
- if (action === "edit") await loadProvider(id);
1418
- if (action === "delete") await deleteProvider(id);
3293
+
3294
+ const row = ev.target.closest("tr.provider-row");
3295
+ if (!row) return;
3296
+ const id = row.getAttribute("data-provider-id");
3297
+ if (!id) return;
3298
+ await loadProvider(id);
1419
3299
  });
1420
3300
  $("loadProviderBtn").addEventListener("click", async () => {
1421
3301
  const id = ($("providerIdInput").value || "").trim();
@@ -1427,7 +3307,16 @@
1427
3307
  const id = ($("providerIdInput").value || "").trim();
1428
3308
  await deleteProvider(id);
1429
3309
  });
1430
- $("createApiKeyCredentialBtn").addEventListener("click", createApiKeyCredential);
3310
+ $("createApiKeyCredentialBtn").addEventListener("click", async () => {
3311
+ const secretRef = await createApiKeyCredential();
3312
+ if (secretRef) {
3313
+ providerEditorSetAuthApiKey(secretRef);
3314
+ toast("Authfile created and applied to provider editor.", "ok");
3315
+ }
3316
+ });
3317
+
3318
+ $("providerEditorExpandBtn").addEventListener("click", () => setAllDetailsOpen($("providerKvEditor"), true, true));
3319
+ $("providerEditorCollapseBtn").addEventListener("click", () => setAllDetailsOpen($("providerKvEditor"), false, true));
1431
3320
 
1432
3321
  $("authMode").addEventListener("change", updateAuthModeUi);
1433
3322
  updateAuthModeUi();
@@ -1437,11 +3326,24 @@
1437
3326
  $("oauthAuthorizeBtn").addEventListener("click", authorizeOauth);
1438
3327
 
1439
3328
  $("refreshQuotaBtn").addEventListener("click", refreshQuota);
3329
+ $("refreshQuotaSnapshotBtn").addEventListener("click", refreshQuotaSnapshotNow);
3330
+ $("quotaFilterInput").addEventListener("input", renderQuotaProviders);
3331
+ $("quotaHideOkToggle").addEventListener("change", renderQuotaProviders);
3332
+ $("quotaOnlyRoutedTargetsToggle").addEventListener("change", renderQuotaProviders);
3333
+ $("quotaApplyDisableBtn").addEventListener("click", () => void quotaAction("disable", $("quotaKeyInput").value));
3334
+ $("quotaApplyRecoverBtn").addEventListener("click", () => void quotaAction("recover", $("quotaKeyInput").value));
3335
+ $("quotaApplyResetBtn").addEventListener("click", () => void quotaAction("reset", $("quotaKeyInput").value));
1440
3336
  $("quotaTbody").addEventListener("click", (ev) => {
1441
- const el = ev.target;
1442
- if (!el || el.tagName !== "BUTTON") return;
3337
+ const tr = ev.target.closest("tr");
3338
+ if (tr && tr.getAttribute) {
3339
+ const pk = tr.getAttribute("data-provider-key");
3340
+ if (pk) $("quotaKeyInput").value = pk;
3341
+ }
3342
+ const el = ev.target.closest("button");
3343
+ if (!el) return;
1443
3344
  const action = el.getAttribute("data-action");
1444
3345
  const key = el.getAttribute("data-key");
3346
+ if (key) $("quotaKeyInput").value = key;
1445
3347
  if (action === "quota-recover") void quotaAction("recover", key);
1446
3348
  else if (action === "quota-reset") void quotaAction("reset", key);
1447
3349
  else if (action === "quota-disable") void quotaAction("disable", key);
@@ -1450,10 +3352,27 @@
1450
3352
 
1451
3353
  $("loadRoutingBtn").addEventListener("click", loadRouting);
1452
3354
  $("saveRoutingBtn").addEventListener("click", saveRouting);
1453
- $("refreshRuntimesBtn").addEventListener("click", refreshRuntimes);
3355
+ $("refreshRoutingSourcesBtn").addEventListener("click", refreshRoutingSources);
3356
+ $("routingSourceSelect").addEventListener("change", loadRouting);
3357
+ $("routingRegExpandBtn").addEventListener("click", () => setAllDetailsOpen($("routingKvEditor"), true));
3358
+ $("routingRegCollapseBtn").addEventListener("click", () => setAllDetailsOpen($("routingKvEditor"), false));
3359
+ $("refreshRoutingPoolBtn").addEventListener("click", async () => {
3360
+ try {
3361
+ await refreshRuntimes();
3362
+ toast("Pool status refreshed.", "ok");
3363
+ } catch (e) {
3364
+ toast(`Refresh failed: ${e && e.message ? e.message : String(e)}`);
3365
+ }
3366
+ });
3367
+ $("routingShowPathsToggle").addEventListener("change", () => {
3368
+ try {
3369
+ document.body.classList.toggle("show-routing-paths", Boolean($("routingShowPathsToggle").checked));
3370
+ } catch {}
3371
+ });
1454
3372
 
1455
3373
  // Init
1456
3374
  (async () => {
3375
+ const auth = await refreshAdminAuthStatus();
1457
3376
  const savedKey = getApiKey();
1458
3377
  if (savedKey) {
1459
3378
  $("apiKeyHint").textContent = "saved (session only)";
@@ -1464,6 +3383,10 @@
1464
3383
  await refreshCredentials();
1465
3384
  await refreshQuota();
1466
3385
  await refreshRuntimes();
3386
+ if (auth && auth.authenticated) {
3387
+ await refreshRoutingSources();
3388
+ await loadRouting();
3389
+ }
1467
3390
  await loadSettings();
1468
3391
  })();
1469
3392
  </script>