@jsonstudio/rcc 0.89.1803 → 0.89.1959

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 (283) hide show
  1. package/configsamples/config.json +19 -0
  2. package/configsamples/provider/deepseek/config.v1.json +59 -0
  3. package/dist/build-info.js +2 -2
  4. package/dist/cli/commands/claude.d.ts +4 -0
  5. package/dist/cli/commands/claude.js +56 -0
  6. package/dist/cli/commands/claude.js.map +1 -0
  7. package/dist/cli/commands/clock-admin.d.ts +20 -0
  8. package/dist/cli/commands/clock-admin.js +234 -0
  9. package/dist/cli/commands/clock-admin.js.map +1 -0
  10. package/dist/cli/commands/code.d.ts +0 -42
  11. package/dist/cli/commands/code.js +4 -414
  12. package/dist/cli/commands/code.js.map +1 -1
  13. package/dist/cli/commands/codex.d.ts +4 -0
  14. package/dist/cli/commands/codex.js +43 -0
  15. package/dist/cli/commands/codex.js.map +1 -0
  16. package/dist/cli/commands/examples.js +13 -16
  17. package/dist/cli/commands/examples.js.map +1 -1
  18. package/dist/cli/commands/init/basic.d.ts +40 -0
  19. package/dist/cli/commands/init/basic.js +482 -0
  20. package/dist/cli/commands/init/basic.js.map +1 -0
  21. package/dist/cli/commands/init/camoufox.d.ts +7 -0
  22. package/dist/cli/commands/init/camoufox.js +59 -0
  23. package/dist/cli/commands/init/camoufox.js.map +1 -0
  24. package/dist/cli/commands/init/interactive.d.ts +18 -0
  25. package/dist/cli/commands/init/interactive.js +223 -0
  26. package/dist/cli/commands/init/interactive.js.map +1 -0
  27. package/dist/cli/commands/init/shared.d.ts +66 -0
  28. package/dist/cli/commands/init/shared.js +9 -0
  29. package/dist/cli/commands/init/shared.js.map +1 -0
  30. package/dist/cli/commands/init/workflows.d.ts +29 -0
  31. package/dist/cli/commands/init/workflows.js +341 -0
  32. package/dist/cli/commands/init/workflows.js.map +1 -0
  33. package/dist/cli/commands/init.d.ts +2 -26
  34. package/dist/cli/commands/init.js +220 -53
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/launcher-kernel.d.ts +78 -0
  37. package/dist/cli/commands/launcher-kernel.js +1194 -0
  38. package/dist/cli/commands/launcher-kernel.js.map +1 -0
  39. package/dist/cli/commands/start.js +27 -1
  40. package/dist/cli/commands/start.js.map +1 -1
  41. package/dist/cli/commands/status.d.ts +2 -0
  42. package/dist/cli/commands/status.js +24 -1
  43. package/dist/cli/commands/status.js.map +1 -1
  44. package/dist/cli/commands/stop.d.ts +1 -0
  45. package/dist/cli/commands/stop.js +201 -4
  46. package/dist/cli/commands/stop.js.map +1 -1
  47. package/dist/cli/commands/tmux-inject.d.ts +20 -0
  48. package/dist/cli/commands/tmux-inject.js +212 -0
  49. package/dist/cli/commands/tmux-inject.js.map +1 -0
  50. package/dist/cli/config/init-provider-catalog.js +34 -0
  51. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  52. package/dist/cli/register/claude-command.d.ts +3 -0
  53. package/dist/cli/register/claude-command.js +5 -0
  54. package/dist/cli/register/claude-command.js.map +1 -0
  55. package/dist/cli/register/clock-admin-command.d.ts +3 -0
  56. package/dist/cli/register/clock-admin-command.js +5 -0
  57. package/dist/cli/register/clock-admin-command.js.map +1 -0
  58. package/dist/cli/register/codex-command.d.ts +3 -0
  59. package/dist/cli/register/codex-command.js +5 -0
  60. package/dist/cli/register/codex-command.js.map +1 -0
  61. package/dist/cli/register/status-config-commands.d.ts +2 -0
  62. package/dist/cli/register/status-config-commands.js.map +1 -1
  63. package/dist/cli/register/tmux-inject-command.d.ts +3 -0
  64. package/dist/cli/register/tmux-inject-command.js +5 -0
  65. package/dist/cli/register/tmux-inject-command.js.map +1 -0
  66. package/dist/cli/server/port-utils.d.ts +3 -2
  67. package/dist/cli/server/port-utils.js +171 -32
  68. package/dist/cli/server/port-utils.js.map +1 -1
  69. package/dist/cli.js +45 -6
  70. package/dist/cli.js.map +1 -1
  71. package/dist/client/gemini/gemini-protocol-client.js +56 -5
  72. package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
  73. package/dist/commands/token-daemon.js +59 -7
  74. package/dist/commands/token-daemon.js.map +1 -1
  75. package/dist/commands/validate.js +87 -15
  76. package/dist/commands/validate.js.map +1 -1
  77. package/dist/config/routecodex-config-loader.js +31 -2
  78. package/dist/config/routecodex-config-loader.js.map +1 -1
  79. package/dist/docs/daemon-admin-ui.html +948 -74
  80. package/dist/index.d.ts +1 -0
  81. package/dist/index.js +325 -37
  82. package/dist/index.js.map +1 -1
  83. package/dist/manager/quota/provider-quota-center.js +8 -14
  84. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  85. package/dist/modules/llmswitch/bridge.d.ts +39 -0
  86. package/dist/modules/llmswitch/bridge.js +169 -0
  87. package/dist/modules/llmswitch/bridge.js.map +1 -1
  88. package/dist/modules/pipeline/utils/colored-logger.js +1 -1
  89. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
  90. package/dist/providers/auth/deepseek-account-auth.d.ts +39 -0
  91. package/dist/providers/auth/deepseek-account-auth.js +329 -0
  92. package/dist/providers/auth/deepseek-account-auth.js.map +1 -0
  93. package/dist/providers/auth/deepseek-account-token-acquirer.d.ts +15 -0
  94. package/dist/providers/auth/deepseek-account-token-acquirer.js +644 -0
  95. package/dist/providers/auth/deepseek-account-token-acquirer.js.map +1 -0
  96. package/dist/providers/auth/oauth-lifecycle.js +26 -4
  97. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  98. package/dist/providers/auth/oauth-repair-cooldown.d.ts +5 -0
  99. package/dist/providers/auth/oauth-repair-cooldown.js +39 -0
  100. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
  101. package/dist/providers/auth/token-scanner/index.d.ts +6 -0
  102. package/dist/providers/auth/token-scanner/index.js +53 -0
  103. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  104. package/dist/providers/core/api/provider-config.d.ts +17 -2
  105. package/dist/providers/core/api/provider-types.d.ts +6 -0
  106. package/dist/providers/core/api/provider-types.js.map +1 -1
  107. package/dist/providers/core/config/camoufox-launcher.d.ts +7 -0
  108. package/dist/providers/core/config/camoufox-launcher.js +68 -21
  109. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  110. package/dist/providers/core/config/service-profiles.js +19 -0
  111. package/dist/providers/core/config/service-profiles.js.map +1 -1
  112. package/dist/providers/core/contracts/deepseek-provider-contract.d.ts +34 -0
  113. package/dist/providers/core/contracts/deepseek-provider-contract.js +100 -0
  114. package/dist/providers/core/contracts/deepseek-provider-contract.js.map +1 -0
  115. package/dist/providers/core/runtime/anthropic-http-provider.d.ts +0 -5
  116. package/dist/providers/core/runtime/anthropic-http-provider.js +0 -26
  117. package/dist/providers/core/runtime/anthropic-http-provider.js.map +1 -1
  118. package/dist/providers/core/runtime/deepseek-http-provider.d.ts +35 -0
  119. package/dist/providers/core/runtime/deepseek-http-provider.js +373 -0
  120. package/dist/providers/core/runtime/deepseek-http-provider.js.map +1 -0
  121. package/dist/providers/core/runtime/deepseek-session-pow.d.ts +55 -0
  122. package/dist/providers/core/runtime/deepseek-session-pow.js +422 -0
  123. package/dist/providers/core/runtime/deepseek-session-pow.js.map +1 -0
  124. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +0 -3
  125. package/dist/providers/core/runtime/gemini-cli-http-provider.js +0 -72
  126. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  127. package/dist/providers/core/runtime/gemini-http-provider.d.ts +1 -7
  128. package/dist/providers/core/runtime/gemini-http-provider.js +3 -110
  129. package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
  130. package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
  131. package/dist/providers/core/runtime/http-request-executor.js +4 -0
  132. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  133. package/dist/providers/core/runtime/http-transport-provider.d.ts +10 -4
  134. package/dist/providers/core/runtime/http-transport-provider.js +308 -82
  135. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  136. package/dist/providers/core/runtime/iflow-http-provider.d.ts +0 -4
  137. package/dist/providers/core/runtime/iflow-http-provider.js +0 -28
  138. package/dist/providers/core/runtime/iflow-http-provider.js.map +1 -1
  139. package/dist/providers/core/runtime/provider-factory.d.ts +5 -0
  140. package/dist/providers/core/runtime/provider-factory.js +59 -6
  141. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  142. package/dist/providers/core/runtime/responses-provider.d.ts +0 -2
  143. package/dist/providers/core/runtime/responses-provider.js +0 -11
  144. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  145. package/dist/providers/core/strategies/oauth-device-flow.js +16 -1
  146. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  147. package/dist/providers/core/utils/provider-type-utils.js +2 -1
  148. package/dist/providers/core/utils/provider-type-utils.js.map +1 -1
  149. package/dist/providers/profile/families/anthropic-profile.d.ts +2 -0
  150. package/dist/providers/profile/families/anthropic-profile.js +32 -0
  151. package/dist/providers/profile/families/anthropic-profile.js.map +1 -0
  152. package/dist/providers/profile/families/antigravity-profile.d.ts +2 -0
  153. package/dist/providers/profile/families/antigravity-profile.js +109 -0
  154. package/dist/providers/profile/families/antigravity-profile.js.map +1 -0
  155. package/dist/providers/profile/families/glm-profile.d.ts +2 -0
  156. package/dist/providers/profile/families/glm-profile.js +48 -0
  157. package/dist/providers/profile/families/glm-profile.js.map +1 -0
  158. package/dist/providers/profile/families/iflow-profile.d.ts +2 -0
  159. package/dist/providers/profile/families/iflow-profile.js +232 -0
  160. package/dist/providers/profile/families/iflow-profile.js.map +1 -0
  161. package/dist/providers/profile/families/qwen-profile.d.ts +2 -0
  162. package/dist/providers/profile/families/qwen-profile.js +14 -0
  163. package/dist/providers/profile/families/qwen-profile.js.map +1 -0
  164. package/dist/providers/profile/families/responses-profile.d.ts +2 -0
  165. package/dist/providers/profile/families/responses-profile.js +28 -0
  166. package/dist/providers/profile/families/responses-profile.js.map +1 -0
  167. package/dist/providers/profile/profile-contracts.d.ts +74 -0
  168. package/dist/providers/profile/profile-contracts.js +2 -0
  169. package/dist/providers/profile/profile-contracts.js.map +1 -0
  170. package/dist/providers/profile/profile-registry.d.ts +3 -0
  171. package/dist/providers/profile/profile-registry.js +40 -0
  172. package/dist/providers/profile/profile-registry.js.map +1 -0
  173. package/dist/providers/profile/provider-directory.d.ts +2 -0
  174. package/dist/providers/profile/provider-directory.js +55 -0
  175. package/dist/providers/profile/provider-directory.js.map +1 -0
  176. package/dist/providers/profile/provider-profile-loader.js +43 -3
  177. package/dist/providers/profile/provider-profile-loader.js.map +1 -1
  178. package/dist/providers/profile/provider-profile.d.ts +8 -0
  179. package/dist/scripts/deepseek/pow-solver.mjs +146 -0
  180. package/dist/scripts/deepseek/sha3_wasm_bg.7b9ca65ddd.wasm +0 -0
  181. package/dist/server/handlers/config-admin-handler.js +27 -0
  182. package/dist/server/handlers/config-admin-handler.js.map +1 -1
  183. package/dist/server/runtime/http-server/clock-client-registry.d.ts +113 -0
  184. package/dist/server/runtime/http-server/clock-client-registry.js +592 -0
  185. package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -0
  186. package/dist/server/runtime/http-server/clock-client-routes.d.ts +2 -0
  187. package/dist/server/runtime/http-server/clock-client-routes.js +481 -0
  188. package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -0
  189. package/dist/server/runtime/http-server/clock-daemon-inject-config.d.ts +1 -0
  190. package/dist/server/runtime/http-server/clock-daemon-inject-config.js +11 -0
  191. package/dist/server/runtime/http-server/clock-daemon-inject-config.js.map +1 -0
  192. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +3 -3
  193. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  194. package/dist/server/runtime/http-server/daemon-admin/auth-session.d.ts +1 -0
  195. package/dist/server/runtime/http-server/daemon-admin/auth-session.js +18 -2
  196. package/dist/server/runtime/http-server/daemon-admin/auth-session.js.map +1 -1
  197. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +2 -15
  198. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
  199. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +65 -7
  200. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  201. package/dist/server/runtime/http-server/executor-metadata.js +37 -1
  202. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  203. package/dist/server/runtime/http-server/executor-provider.js +55 -0
  204. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  205. package/dist/server/runtime/http-server/executor-response.js +49 -1
  206. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  207. package/dist/server/runtime/http-server/index.d.ts +10 -0
  208. package/dist/server/runtime/http-server/index.js +534 -9
  209. package/dist/server/runtime/http-server/index.js.map +1 -1
  210. package/dist/server/runtime/http-server/managed-process-probe.d.ts +6 -0
  211. package/dist/server/runtime/http-server/managed-process-probe.js +294 -0
  212. package/dist/server/runtime/http-server/managed-process-probe.js.map +1 -0
  213. package/dist/server/runtime/http-server/middleware.js +16 -1
  214. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  215. package/dist/server/runtime/http-server/provider-utils.js +6 -2
  216. package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
  217. package/dist/server/runtime/http-server/request-executor.d.ts +1 -0
  218. package/dist/server/runtime/http-server/request-executor.js +360 -35
  219. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  220. package/dist/server/runtime/http-server/routes.js +95 -3
  221. package/dist/server/runtime/http-server/routes.js.map +1 -1
  222. package/dist/server/runtime/http-server/stats-manager.d.ts +10 -0
  223. package/dist/server/runtime/http-server/stats-manager.js +119 -16
  224. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  225. package/dist/server/runtime/http-server/tmux-session-probe.d.ts +3 -0
  226. package/dist/server/runtime/http-server/tmux-session-probe.js +101 -0
  227. package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -0
  228. package/dist/server/utils/stage-logger.js +21 -5
  229. package/dist/server/utils/stage-logger.js.map +1 -1
  230. package/dist/token-daemon/index.js +59 -10
  231. package/dist/token-daemon/index.js.map +1 -1
  232. package/dist/token-daemon/server-utils.d.ts +1 -0
  233. package/dist/token-daemon/server-utils.js +4 -1
  234. package/dist/token-daemon/server-utils.js.map +1 -1
  235. package/dist/token-daemon/token-daemon.js +38 -4
  236. package/dist/token-daemon/token-daemon.js.map +1 -1
  237. package/dist/token-daemon/token-types.d.ts +1 -1
  238. package/dist/token-daemon/token-types.js +2 -1
  239. package/dist/token-daemon/token-types.js.map +1 -1
  240. package/dist/token-daemon/token-utils.js +5 -2
  241. package/dist/token-daemon/token-utils.js.map +1 -1
  242. package/dist/utils/clock-client-token.d.ts +3 -0
  243. package/dist/utils/clock-client-token.js +54 -0
  244. package/dist/utils/clock-client-token.js.map +1 -0
  245. package/dist/utils/managed-server-pids.d.ts +25 -0
  246. package/dist/utils/managed-server-pids.js +176 -0
  247. package/dist/utils/managed-server-pids.js.map +1 -0
  248. package/dist/utils/process-lifecycle-logger.d.ts +8 -0
  249. package/dist/utils/process-lifecycle-logger.js +151 -0
  250. package/dist/utils/process-lifecycle-logger.js.map +1 -0
  251. package/dist/utils/runtime-exit-forensics.d.ts +30 -0
  252. package/dist/utils/runtime-exit-forensics.js +101 -0
  253. package/dist/utils/runtime-exit-forensics.js.map +1 -0
  254. package/dist/utils/shutdown-caller-context.d.ts +22 -0
  255. package/dist/utils/shutdown-caller-context.js +25 -0
  256. package/dist/utils/shutdown-caller-context.js.map +1 -0
  257. package/docs/PROVIDERS_BUILTIN.md +8 -0
  258. package/docs/PROVIDER_TYPES.md +3 -1
  259. package/docs/SERVERTOOL_PRE_COMMAND_HOOKS.md +85 -0
  260. package/docs/clock-client-daemon-design.md +343 -0
  261. package/docs/daemon-admin-ui.html +948 -74
  262. package/docs/providers/deepseek-web-provider-design.md +192 -0
  263. package/docs/routing-instructions.md +4 -1
  264. package/docs/stop-message-auto.md +4 -3
  265. package/docs/v2-architecture/PROVIDER-V2-CHANGESET-RELEASE-CHECKLIST.md +80 -0
  266. package/docs/v2-architecture/PROVIDER-V2-LAYERING-ADR-DRAFT.md +225 -0
  267. package/docs/v2-architecture/PROVIDER-V2-MIGRATION-MATRIX-DRAFT.md +88 -0
  268. package/docs/v2-architecture/PROVIDER-V2-PHASED-MIGRATION-ROLLBACK-DRAFT.md +164 -0
  269. package/docs/v2-architecture/PROVIDER-V2-PROFILE-API-REGISTRY-DRAFT.md +201 -0
  270. package/docs/v2-architecture/PROVIDER-V2-PROFILE-GEMINI-DRAFT.md +56 -0
  271. package/docs/v2-architecture/PROVIDER-V2-REFACTOR-OVERVIEW-DRAFT.md +102 -0
  272. package/docs/v2-architecture/PROVIDER-V2-VERIFICATION-MATRIX-DRAFT.md +163 -0
  273. package/package.json +10 -9
  274. package/scripts/copy-compat-assets.mjs +18 -0
  275. package/scripts/copy-modules-config.mjs +1 -0
  276. package/scripts/deepseek/pow-solver.mjs +146 -0
  277. package/scripts/deepseek/sha3_wasm_bg.7b9ca65ddd.wasm +0 -0
  278. package/scripts/ensure-cli-executable.mjs +64 -0
  279. package/scripts/install-global.sh +5 -2
  280. package/scripts/install.sh +1 -1
  281. package/scripts/monitor/daemon-kill-watch.mjs +184 -0
  282. package/scripts/monitor/port-kill-watch.sh +74 -0
  283. package/scripts/quick-install.sh +1 -1
@@ -376,6 +376,10 @@
376
376
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
377
377
  }
378
378
 
379
+ .grid.grid-clock {
380
+ grid-template-columns: minmax(0, 2.2fr) minmax(0, 0.8fr);
381
+ }
382
+
379
383
  .grid.grid-one {
380
384
  grid-template-columns: minmax(0, 1fr);
381
385
  }
@@ -465,6 +469,71 @@
465
469
  font-weight: 650;
466
470
  }
467
471
 
472
+ .table tr.clock-detail-row td {
473
+ background: rgba(255, 255, 255, 0.01);
474
+ }
475
+
476
+ .clock-group-toggle {
477
+ all: unset;
478
+ cursor: pointer;
479
+ display: block;
480
+ width: 100%;
481
+ white-space: normal;
482
+ line-height: 1.35;
483
+ }
484
+
485
+ .clock-group-summary {
486
+ margin-top: 4px;
487
+ font-size: 11px;
488
+ color: var(--muted);
489
+ white-space: normal;
490
+ overflow-wrap: anywhere;
491
+ line-height: 1.3;
492
+ }
493
+
494
+ .clock-detail-list {
495
+ display: flex;
496
+ flex-direction: column;
497
+ gap: 6px;
498
+ padding: 4px 0;
499
+ }
500
+
501
+ .clock-detail-item {
502
+ display: flex;
503
+ align-items: center;
504
+ gap: 8px;
505
+ min-width: 0;
506
+ }
507
+
508
+ .clock-detail-item .clock-detail-content {
509
+ min-width: 0;
510
+ flex: 1;
511
+ white-space: normal;
512
+ overflow-wrap: anywhere;
513
+ line-height: 1.3;
514
+ }
515
+
516
+ .table.table-clock-merged th:nth-child(1),
517
+ .table.table-clock-merged td:nth-child(1) {
518
+ width: 10%;
519
+ }
520
+ .table.table-clock-merged th:nth-child(2),
521
+ .table.table-clock-merged td:nth-child(2) {
522
+ width: 50%;
523
+ }
524
+ .table.table-clock-merged th:nth-child(3),
525
+ .table.table-clock-merged td:nth-child(3) {
526
+ width: 14%;
527
+ }
528
+ .table.table-clock-merged th:nth-child(4),
529
+ .table.table-clock-merged td:nth-child(4) {
530
+ width: 14%;
531
+ }
532
+ .table.table-clock-merged th:nth-child(5),
533
+ .table.table-clock-merged td:nth-child(5) {
534
+ width: 12%;
535
+ }
536
+
468
537
  .table tr.provider-row:hover td {
469
538
  background: rgba(78, 161, 255, 0.06);
470
539
  }
@@ -502,6 +571,19 @@
502
571
  justify-content: flex-end;
503
572
  }
504
573
 
574
+ .auth-actions {
575
+ display: flex;
576
+ flex-wrap: wrap;
577
+ gap: 6px;
578
+ }
579
+
580
+ .auth-action-btn {
581
+ padding: 3px 8px;
582
+ font-size: 11px;
583
+ border-radius: 8px;
584
+ white-space: nowrap;
585
+ }
586
+
505
587
  .truncate {
506
588
  white-space: nowrap;
507
589
  overflow: hidden;
@@ -585,12 +667,19 @@
585
667
 
586
668
  .auth-mode-tabs {
587
669
  margin: 10px 0;
670
+ position: relative;
671
+ z-index: 2;
588
672
  }
589
673
 
590
674
  .auth-mode-tabs .tab {
591
675
  min-width: 120px;
592
676
  }
593
677
 
678
+ .auth-mode-panels {
679
+ position: relative;
680
+ z-index: 2;
681
+ }
682
+
594
683
  .log {
595
684
  white-space: pre-wrap;
596
685
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
@@ -639,6 +728,7 @@
639
728
  <div class="pill"><span id="statusDot" class="dot"></span><span id="statusText">connecting…</span></div>
640
729
  <div class="pill"><span class="mono" id="serverId">serverId: —</span></div>
641
730
  <div class="pill"><span class="mono" id="serverVersion">version: —</span></div>
731
+ <div class="pill"><span class="mono" id="clockOverviewText">clock: loading…</span></div>
642
732
  <button id="restartRuntimeBtn" class="primary">Restart runtime</button>
643
733
  </div>
644
734
  </header>
@@ -702,6 +792,7 @@
702
792
  <button class="tab" data-tab="credentials">Auth Provider Pool</button>
703
793
  <button class="tab" data-tab="quota">Quota Pool</button>
704
794
  <button class="tab" data-tab="routing">Runtime Routing Pool</button>
795
+ <button class="tab" data-tab="clock">Clock</button>
705
796
  </div>
706
797
 
707
798
  <section id="panelControl" data-panel="control" style="display:none;">
@@ -794,6 +885,59 @@
794
885
  </div>
795
886
  </section>
796
887
 
888
+ <section id="panelClock" data-panel="clock" style="display:none;">
889
+ <div class="grid grid-clock">
890
+ <div class="card" style="box-shadow: none;">
891
+ <p class="section-title">Clock tasks (merged)</p>
892
+ <p class="section-sub">Merged by content + recurrence + tool to reduce repeated rows. Expand a row to inspect per-time details.</p>
893
+ <div class="row" style="margin-bottom: 10px;">
894
+ <label for="clockSessionFilterInput">session filter</label>
895
+ <input id="clockSessionFilterInput" type="text" placeholder="conversation session id (optional)" style="min-width: 320px; flex: 1;" />
896
+ <button id="refreshClockBtn" class="primary">Refresh</button>
897
+ <span id="clockHint" class="muted" style="margin-left:auto; font-size:12px;"></span>
898
+ </div>
899
+ <div id="clockSummaryPills" class="row" style="margin-bottom: 10px;"></div>
900
+
901
+ <p class="section-title" style="margin-top: 8px;">Merged list</p>
902
+ <div class="table-wrap">
903
+ <table class="table table-clock-merged">
904
+ <thead>
905
+ <tr>
906
+ <th>status</th>
907
+ <th>task (foldable)</th>
908
+ <th>next due</th>
909
+ <th>time plan</th>
910
+ <th>scope</th>
911
+ </tr>
912
+ </thead>
913
+ <tbody id="clockMergedTbody"></tbody>
914
+ </table>
915
+ </div>
916
+ </div>
917
+
918
+ <div class="card" style="box-shadow: none;">
919
+ <p class="section-title">Clock daemon bindings</p>
920
+ <p class="section-sub">tmux daemon registry and heartbeat state used by clock injection.</p>
921
+ <div class="table-wrap">
922
+ <table class="table">
923
+ <thead>
924
+ <tr>
925
+ <th>daemonId</th>
926
+ <th>tmuxSession</th>
927
+ <th>boundConversations</th>
928
+ <th>heartbeat</th>
929
+ <th>lastInject</th>
930
+ <th>status</th>
931
+ <th>lastError</th>
932
+ </tr>
933
+ </thead>
934
+ <tbody id="clockDaemonsTbody"></tbody>
935
+ </table>
936
+ </div>
937
+ </div>
938
+ </div>
939
+ </section>
940
+
797
941
  <section id="panelProviders" data-panel="providers">
798
942
  <div class="grid grid-wide-left">
799
943
  <div class="card" style="box-shadow: none;">
@@ -1105,6 +1249,7 @@
1105
1249
  <th>status</th>
1106
1250
  <th>expires</th>
1107
1251
  <th>secretRef</th>
1252
+ <th>action</th>
1108
1253
  </tr>
1109
1254
  </thead>
1110
1255
  <tbody id="credentialsTbody"></tbody>
@@ -1202,7 +1347,7 @@
1202
1347
  <label for="quotaFilterInput">filter</label>
1203
1348
  <input id="quotaFilterInput" type="text" placeholder="providerKey contains…" style="width: 320px;" />
1204
1349
  <label><input id="quotaHideOkToggle" type="checkbox" /> hide ok</label>
1205
- <label><input id="quotaOnlyRoutedTargetsToggle" type="checkbox" checked /> only routed targets</label>
1350
+ <label><input id="quotaOnlyRoutedTargetsToggle" type="checkbox" checked disabled /> routed models only</label>
1206
1351
  <button id="quotaSelectVisibleBtn">Select visible</button>
1207
1352
  <button id="quotaClearSelectionBtn">Clear selection</button>
1208
1353
  <label><input id="quotaAutoRefreshToggle" type="checkbox" checked /> auto refresh</label>
@@ -1362,6 +1507,9 @@
1362
1507
  adminAuth: null,
1363
1508
  controlSnapshot: null,
1364
1509
  controlSnapshotUpdatedAt: 0,
1510
+ clockSnapshot: null,
1511
+ clockSnapshotUpdatedAt: 0,
1512
+ clockExpandedGroups: {},
1365
1513
  quotaProviders: [],
1366
1514
  quotaProvidersUpdatedAt: 0,
1367
1515
  quotaProviderMap: null,
@@ -1416,10 +1564,19 @@
1416
1564
  el.style.display = value ? "block" : "none";
1417
1565
  const raw = value || "";
1418
1566
  const max = 12000;
1419
- const out = raw.length > max ? raw.slice(0, max) + "\n(truncated)" : raw;
1567
+ const out = raw.length > max ? raw.slice(0, max) + "\n...(truncated)" : raw;
1420
1568
  el.textContent = out;
1421
1569
  }
1422
1570
 
1571
+ function appendLog(id, line) {
1572
+ const el = $(id);
1573
+ if (!el) return;
1574
+ const prev = textOf(el.textContent || "").trim();
1575
+ const nextLine = textOf(line || "").trim();
1576
+ if (!nextLine) return;
1577
+ setLog(id, prev ? (prev + "\n" + nextLine) : nextLine);
1578
+ }
1579
+
1423
1580
  function getApiKey() {
1424
1581
  try {
1425
1582
  return sessionStorage.getItem("routecodex:apikey") || "";
@@ -1460,7 +1617,7 @@
1460
1617
  }
1461
1618
 
1462
1619
  function selectTab(name) {
1463
- document.querySelectorAll(".tab").forEach((btn) => {
1620
+ document.querySelectorAll(".tab[data-tab]").forEach((btn) => {
1464
1621
  btn.classList.toggle("active", btn.getAttribute("data-tab") === name);
1465
1622
  });
1466
1623
  const panels = [
@@ -1470,7 +1627,8 @@
1470
1627
  { name: "tokens", el: $("panelTokens") },
1471
1628
  { name: "credentials", el: $("panelCredentials") },
1472
1629
  { name: "quota", el: $("panelQuota") },
1473
- { name: "routing", el: $("panelRouting") }
1630
+ { name: "routing", el: $("panelRouting") },
1631
+ { name: "clock", el: $("panelClock") }
1474
1632
  ];
1475
1633
  for (const p of panels) p.el.style.display = p.name === name ? "block" : "none";
1476
1634
 
@@ -1480,7 +1638,7 @@
1480
1638
  }
1481
1639
 
1482
1640
  function getActiveTab() {
1483
- const active = document.querySelector(".tab.active");
1641
+ const active = document.querySelector(".tab[data-tab].active");
1484
1642
  const name = active ? active.getAttribute("data-tab") : null;
1485
1643
  return name || "providers";
1486
1644
  }
@@ -1492,7 +1650,8 @@
1492
1650
  tokens: 0,
1493
1651
  credentials: 0,
1494
1652
  quota: 0,
1495
- routing: 0
1653
+ routing: 0,
1654
+ clock: 0
1496
1655
  };
1497
1656
 
1498
1657
  async function maybeRefreshTab(name) {
@@ -1516,6 +1675,9 @@
1516
1675
  await refreshRuntimes();
1517
1676
  await refreshRoutingSources();
1518
1677
  }
1678
+ else if (key === "clock") {
1679
+ await refreshClockTab();
1680
+ }
1519
1681
  } catch {
1520
1682
  // ignore refresh failures on tab switch
1521
1683
  }
@@ -2100,6 +2262,486 @@
2100
2262
  };
2101
2263
  }
2102
2264
 
2265
+ async function refreshClockOverview() {
2266
+ const overviewEl = $("clockOverviewText");
2267
+ if (!overviewEl) return;
2268
+ try {
2269
+ const res = await fetch("/daemon/clock/tasks");
2270
+ if (!res || !res.ok) throw new Error(res ? String(res.status) : "request_failed");
2271
+ const data = await res.json().catch(() => ({}));
2272
+ const sessions = data && Array.isArray(data.sessions) ? data.sessions : [];
2273
+ const records = data && Array.isArray(data.records) ? data.records : [];
2274
+ const now = Date.now();
2275
+ let taskCount = 0;
2276
+ let dueCount = 0;
2277
+ for (const session of sessions) {
2278
+ const tasks = session && Array.isArray(session.tasks) ? session.tasks : [];
2279
+ taskCount += tasks.length;
2280
+ for (const task of tasks) {
2281
+ let dueAtMs = Number(task && task.dueAtMs);
2282
+ if (!Number.isFinite(dueAtMs) && task && typeof task.dueAt === "string") {
2283
+ const parsed = Date.parse(task.dueAt);
2284
+ dueAtMs = Number.isFinite(parsed) ? parsed : NaN;
2285
+ }
2286
+ if (Number.isFinite(dueAtMs) && dueAtMs <= now) {
2287
+ dueCount += 1;
2288
+ }
2289
+ }
2290
+ }
2291
+ const onlineCount = records.filter((record) => {
2292
+ const hb = Number(record && record.lastHeartbeatAtMs);
2293
+ return Number.isFinite(hb) && now - hb <= 45_000;
2294
+ }).length;
2295
+ overviewEl.textContent = "clock: tasks " + taskCount + " · due " + dueCount + " · daemon " + onlineCount + "/" + records.length;
2296
+ } catch {
2297
+ overviewEl.textContent = "clock: unavailable";
2298
+ }
2299
+ }
2300
+
2301
+ function clockParseDueAtMs(task) {
2302
+ const rawMs = Number(task && task.dueAtMs);
2303
+ if (Number.isFinite(rawMs)) return Math.floor(rawMs);
2304
+ const rawIso = task && typeof task.dueAt === "string" ? task.dueAt.trim() : "";
2305
+ if (!rawIso) return NaN;
2306
+ const parsed = Date.parse(rawIso);
2307
+ return Number.isFinite(parsed) ? Math.floor(parsed) : NaN;
2308
+ }
2309
+
2310
+ function clockTaskStatusKind(task, now) {
2311
+ const dueAtMs = clockParseDueAtMs(task);
2312
+ if (!Number.isFinite(dueAtMs)) return "warn";
2313
+ const delta = dueAtMs - now;
2314
+ if (delta < -60_000) return "bad";
2315
+ if (delta <= 0) return "warn";
2316
+ return "ok";
2317
+ }
2318
+
2319
+ function clockTaskStatusText(task, now) {
2320
+ const dueAtMs = clockParseDueAtMs(task);
2321
+ if (!Number.isFinite(dueAtMs)) return "invalid";
2322
+ const delta = dueAtMs - now;
2323
+ if (delta < -60_000) return "overdue";
2324
+ if (delta <= 0) return "due";
2325
+ if (delta <= 60_000) return "soon";
2326
+ return "scheduled";
2327
+ }
2328
+
2329
+ function clockRecurrenceText(task) {
2330
+ const rec = task && task.recurrence && typeof task.recurrence === "object" ? task.recurrence : null;
2331
+ if (!rec) return "one-shot";
2332
+ const kind = textOf(rec.kind || "").trim() || "recurring";
2333
+ const maxRuns = Number(rec.maxRuns);
2334
+ const count = Number(task && task.deliveryCount);
2335
+ let base = kind;
2336
+ if (kind === "interval") {
2337
+ const everyMinutes = Number(rec.everyMinutes);
2338
+ if (Number.isFinite(everyMinutes) && everyMinutes > 0) {
2339
+ base += "/" + Math.floor(everyMinutes) + "m";
2340
+ }
2341
+ }
2342
+ if (Number.isFinite(maxRuns) && maxRuns > 0) {
2343
+ const safeCount = Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
2344
+ base += " " + safeCount + "/" + Math.floor(maxRuns);
2345
+ }
2346
+ return base;
2347
+ }
2348
+
2349
+ function clockNormalizeContent(value) {
2350
+ return textOf(value).trim().replace(/\s+/g, " ");
2351
+ }
2352
+
2353
+ function clockShortLabel(value, head = 8, tail = 4) {
2354
+ const raw = textOf(value).trim();
2355
+ if (!raw) return "—";
2356
+ const maxLen = Math.max(6, head + tail + 1);
2357
+ if (raw.length <= maxLen) return raw;
2358
+ return raw.slice(0, head) + "…" + raw.slice(-tail);
2359
+ }
2360
+
2361
+ function clockSessionPreview(sessionIds, limit = 3) {
2362
+ const ids = Array.isArray(sessionIds) ? sessionIds.filter(Boolean) : [];
2363
+ if (!ids.length) return "—";
2364
+ const shown = ids.slice(0, limit).map((id) => clockShortLabel(id, 6, 3));
2365
+ if (ids.length > limit) {
2366
+ shown.push("+" + String(ids.length - limit));
2367
+ }
2368
+ return shown.join(", ");
2369
+ }
2370
+
2371
+ function clockFormatCompactTs(ms) {
2372
+ if (!Number.isFinite(ms) || ms <= 0) return "—";
2373
+ const d = new Date(ms);
2374
+ if (Number.isNaN(d.getTime())) return "—";
2375
+ const pad = (n) => String(n).padStart(2, "0");
2376
+ return pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
2377
+ }
2378
+
2379
+ function clockReadableTmuxSessionId(value) {
2380
+ const raw = textOf(value).trim();
2381
+ if (!raw) return "—";
2382
+ const managed = /^rcc_([a-zA-Z0-9_-]+)_(\d{13})_([a-f0-9]{4,})$/i.exec(raw);
2383
+ if (!managed) return clockShortLabel(raw, 10, 6);
2384
+ const role = textOf(managed[1]).replace(/_/g, "-");
2385
+ const startedAt = Number(managed[2]);
2386
+ const startedText = clockFormatCompactTs(startedAt);
2387
+ const suffix = textOf(managed[3]).slice(0, 4);
2388
+ return role + "@" + startedText + "#" + suffix;
2389
+ }
2390
+
2391
+ function clockTmuxPreview(values, limit = 2) {
2392
+ const list = Array.isArray(values) ? values.filter((v) => textOf(v).trim() && textOf(v).trim() !== "—") : [];
2393
+ if (!list.length) return "—";
2394
+ const uniq = Array.from(new Set(list));
2395
+ const shown = uniq.slice(0, limit);
2396
+ if (uniq.length > limit) {
2397
+ shown.push("+" + String(uniq.length - limit));
2398
+ }
2399
+ return shown.join(", ");
2400
+ }
2401
+
2402
+ function clockRecurrenceShort(task) {
2403
+ const rec = task && task.recurrence && typeof task.recurrence === "object" ? task.recurrence : null;
2404
+ if (!rec) return "1x";
2405
+ const kind = textOf(rec.kind || "").trim().toLowerCase();
2406
+ const maxRuns = Number(rec.maxRuns);
2407
+ let prefix = "R";
2408
+ if (kind === "daily") prefix = "D";
2409
+ else if (kind === "weekly") prefix = "W";
2410
+ else if (kind === "interval") {
2411
+ const everyMinutes = Number(rec.everyMinutes);
2412
+ prefix = Number.isFinite(everyMinutes) && everyMinutes > 0 ? "I" + String(Math.floor(everyMinutes)) + "m" : "I";
2413
+ }
2414
+ if (Number.isFinite(maxRuns) && maxRuns > 0) {
2415
+ return prefix + "/" + String(Math.floor(maxRuns));
2416
+ }
2417
+ return prefix;
2418
+ }
2419
+
2420
+ function clockBuildGroupKey(task) {
2421
+ const content = clockNormalizeContent(task && task.task);
2422
+ const tool = textOf(task && task.tool).trim();
2423
+ return [content || "(empty)", tool || "tool:none"].join("||");
2424
+ }
2425
+
2426
+ function renderClockTabFromSnapshot() {
2427
+ const snap = UI.clockSnapshot && typeof UI.clockSnapshot === "object" ? UI.clockSnapshot : {};
2428
+ const sessions = Array.isArray(snap.sessions) ? snap.sessions : [];
2429
+ const records = Array.isArray(snap.records) ? snap.records : [];
2430
+
2431
+ const mergedBody = $("clockMergedTbody");
2432
+ const daemonBody = $("clockDaemonsTbody");
2433
+ const summaryBox = $("clockSummaryPills");
2434
+ const hint = $("clockHint");
2435
+
2436
+ if (mergedBody) mergedBody.replaceChildren();
2437
+ if (daemonBody) daemonBody.replaceChildren();
2438
+ if (summaryBox) summaryBox.replaceChildren();
2439
+
2440
+ const now = Date.now();
2441
+ const groups = new Map();
2442
+ const rows = [];
2443
+ const conversationToTmuxSession = new Map();
2444
+ for (const record of records) {
2445
+ const tmuxSession = textOf((record && record.tmuxSessionId) || (record && record.sessionId)).trim();
2446
+ if (!tmuxSession) continue;
2447
+ const conversationIds = Array.isArray(record && record.conversationSessionIds) ? record.conversationSessionIds : [];
2448
+ for (const cid of conversationIds) {
2449
+ const key = textOf(cid).trim();
2450
+ if (!key || conversationToTmuxSession.has(key)) continue;
2451
+ conversationToTmuxSession.set(key, tmuxSession);
2452
+ }
2453
+ }
2454
+ for (const session of sessions) {
2455
+ const sessionId = textOf(session && session.sessionId).trim();
2456
+ const mappedTmuxSessionId = textOf(conversationToTmuxSession.get(sessionId)).trim();
2457
+ const mappedTmuxShort = clockReadableTmuxSessionId(mappedTmuxSessionId);
2458
+ const tasks = session && Array.isArray(session.tasks) ? session.tasks : [];
2459
+ for (const task of tasks) {
2460
+ const key = clockBuildGroupKey(task);
2461
+ const dueMs = clockParseDueAtMs(task);
2462
+ const status = clockTaskStatusText(task, now);
2463
+ const statusKind = clockTaskStatusKind(task, now);
2464
+ const taskId = textOf(task && task.taskId).trim();
2465
+ const recurrence = clockRecurrenceText(task);
2466
+ const tool = textOf(task && task.tool).trim() || "—";
2467
+ const deliveryCount = Number(task && task.deliveryCount);
2468
+ const row = {
2469
+ groupKey: key,
2470
+ sessionId,
2471
+ sessionShort: clockShortLabel(sessionId, 6, 3),
2472
+ tmuxSessionId: mappedTmuxSessionId,
2473
+ tmuxShort: mappedTmuxShort,
2474
+ taskId,
2475
+ taskShort: clockShortLabel(taskId, 6, 4),
2476
+ content: textOf(task && task.task).trim(),
2477
+ dueMs,
2478
+ dueAt: Number.isFinite(dueMs) ? formatEpochWithDelta(dueMs) : "—",
2479
+ dueCompact: Number.isFinite(dueMs) ? formatEpochMs(dueMs) : "—",
2480
+ status,
2481
+ statusKind,
2482
+ recurrence,
2483
+ recurrenceShort: clockRecurrenceShort(task),
2484
+ deliveryCount,
2485
+ tool,
2486
+ toolShort: clockShortLabel(tool, 8, 3)
2487
+ };
2488
+ row.metaShort = row.toolShort + " · " + row.recurrenceShort + (Number.isFinite(row.deliveryCount) ? " · #" + String(Math.max(0, Math.floor(row.deliveryCount))) : "");
2489
+ rows.push(row);
2490
+
2491
+ if (!groups.has(key)) {
2492
+ groups.set(key, {
2493
+ groupKey: key,
2494
+ content: row.content || "(empty)",
2495
+ sessionIds: new Set(),
2496
+ tmuxDisplays: new Set(),
2497
+ taskCount: 0,
2498
+ dueMin: Number.isFinite(dueMs) ? dueMs : NaN,
2499
+ dueMax: Number.isFinite(dueMs) ? dueMs : NaN,
2500
+ dueCount: 0,
2501
+ overdueCount: 0,
2502
+ statusKind: row.statusKind,
2503
+ toolSet: new Set(),
2504
+ recurrenceSet: new Set(),
2505
+ rows: []
2506
+ });
2507
+ }
2508
+ const group = groups.get(key);
2509
+ group.sessionIds.add(sessionId || "—");
2510
+ if (row.tmuxShort && row.tmuxShort !== "—") {
2511
+ group.tmuxDisplays.add(row.tmuxShort);
2512
+ }
2513
+ group.toolSet.add(row.toolShort);
2514
+ group.recurrenceSet.add(row.recurrenceShort);
2515
+ group.taskCount += 1;
2516
+ group.rows.push(row);
2517
+ if (row.status === "due") group.dueCount += 1;
2518
+ if (row.status === "overdue") group.overdueCount += 1;
2519
+ if (Number.isFinite(dueMs)) {
2520
+ if (!Number.isFinite(group.dueMin) || dueMs < group.dueMin) group.dueMin = dueMs;
2521
+ if (!Number.isFinite(group.dueMax) || dueMs > group.dueMax) group.dueMax = dueMs;
2522
+ }
2523
+ if (row.statusKind === "bad") group.statusKind = "bad";
2524
+ else if (row.statusKind === "warn" && group.statusKind !== "bad") group.statusKind = "warn";
2525
+ }
2526
+ }
2527
+
2528
+ const merged = Array.from(groups.values());
2529
+ merged.sort((a, b) => {
2530
+ const aDue = Number.isFinite(a.dueMin) ? a.dueMin : Number.POSITIVE_INFINITY;
2531
+ const bDue = Number.isFinite(b.dueMin) ? b.dueMin : Number.POSITIVE_INFINITY;
2532
+ if (aDue !== bDue) return aDue - bDue;
2533
+ return b.taskCount - a.taskCount;
2534
+ });
2535
+
2536
+ rows.sort((a, b) => {
2537
+ const aDue = Number.isFinite(a.dueMs) ? a.dueMs : Number.POSITIVE_INFINITY;
2538
+ const bDue = Number.isFinite(b.dueMs) ? b.dueMs : Number.POSITIVE_INFINITY;
2539
+ if (aDue !== bDue) return aDue - bDue;
2540
+ return a.sessionId.localeCompare(b.sessionId);
2541
+ });
2542
+
2543
+ if (!UI.clockExpandedGroups || typeof UI.clockExpandedGroups !== "object") {
2544
+ UI.clockExpandedGroups = {};
2545
+ }
2546
+
2547
+ if (summaryBox) {
2548
+ const dueNow = rows.filter((r) => r.status === "due").length;
2549
+ const overdue = rows.filter((r) => r.status === "overdue").length;
2550
+ const recurring = rows.filter((r) => r.recurrence !== "one-shot").length;
2551
+ const onlineDaemon = records.filter((record) => {
2552
+ const hb = Number(record && record.lastHeartbeatAtMs);
2553
+ return Number.isFinite(hb) && now - hb <= 45_000;
2554
+ }).length;
2555
+ const pills = [
2556
+ ["sessions", sessions.length, ""],
2557
+ ["tasks", rows.length, ""],
2558
+ ["merged", merged.length, ""],
2559
+ ["due", dueNow, dueNow ? "warn" : "ok"],
2560
+ ["overdue", overdue, overdue ? "bad" : "ok"],
2561
+ ["recurring", recurring, ""],
2562
+ ["daemon", onlineDaemon + "/" + records.length, onlineDaemon ? "ok" : "warn"]
2563
+ ];
2564
+ for (const [label, value, kind] of pills) {
2565
+ summaryBox.appendChild(pill(label + ": " + value, kind));
2566
+ }
2567
+ }
2568
+
2569
+ if (mergedBody) {
2570
+ if (!merged.length) {
2571
+ mergedBody.appendChild(createInfoRow(5, "No clock tasks."));
2572
+ } else {
2573
+ for (const item of merged) {
2574
+ const isExpanded = !!UI.clockExpandedGroups[item.groupKey];
2575
+ const tr = document.createElement("tr");
2576
+ tr.className = "group-row";
2577
+ const statusText = item.overdueCount > 0
2578
+ ? "overdue " + item.overdueCount
2579
+ : item.dueCount > 0
2580
+ ? "due " + item.dueCount
2581
+ : "scheduled";
2582
+ const statusTd = document.createElement("td");
2583
+ statusTd.appendChild(pill(statusText, item.statusKind));
2584
+ tr.appendChild(statusTd);
2585
+
2586
+ const taskTd = document.createElement("td");
2587
+ const toggleBtn = document.createElement("button");
2588
+ toggleBtn.type = "button";
2589
+ toggleBtn.className = "clock-group-toggle mono";
2590
+ toggleBtn.textContent = (isExpanded ? "▾ " : "▸ ") + (item.content || "(empty)");
2591
+ toggleBtn.title = isExpanded ? "collapse" : "expand";
2592
+ toggleBtn.addEventListener("click", () => {
2593
+ UI.clockExpandedGroups[item.groupKey] = !UI.clockExpandedGroups[item.groupKey];
2594
+ renderClockTabFromSnapshot();
2595
+ });
2596
+ taskTd.appendChild(toggleBtn);
2597
+ const groupSummary = document.createElement("div");
2598
+ groupSummary.className = "clock-group-summary mono";
2599
+ const toolPart = Array.from(item.toolSet).slice(0, 2).join(", ") || "—";
2600
+ const recPart = Array.from(item.recurrenceSet).slice(0, 2).join(", ") || "1x";
2601
+ groupSummary.textContent = "tool " + toolPart + " · rec " + recPart;
2602
+ groupSummary.title = groupSummary.textContent;
2603
+ taskTd.appendChild(groupSummary);
2604
+ tr.appendChild(taskTd);
2605
+
2606
+ const nextDueText = Number.isFinite(item.dueMin) ? formatEpochWithDelta(item.dueMin) : "—";
2607
+ tr.appendChild(createCell("td", nextDueText, "mono truncate", { title: true }));
2608
+
2609
+ const sortedDueRows = item.rows
2610
+ .filter((r) => Number.isFinite(r.dueMs))
2611
+ .sort((a, b) => a.dueMs - b.dueMs);
2612
+ let planText = "—";
2613
+ if (sortedDueRows.length) {
2614
+ planText = sortedDueRows.slice(0, 3).map((r) => r.dueCompact).join(" | ");
2615
+ if (sortedDueRows.length > 3) {
2616
+ planText += " +" + String(sortedDueRows.length - 3);
2617
+ }
2618
+ }
2619
+ tr.appendChild(createCell("td", planText, "mono truncate", { title: true }));
2620
+
2621
+ const sessionList = Array.from(item.sessionIds);
2622
+ const tmuxPreview = clockTmuxPreview(Array.from(item.tmuxDisplays));
2623
+ const scopeText = String(item.taskCount) + " task" + (item.taskCount > 1 ? "s" : "") + " · " + String(sessionList.length) + " session" + (sessionList.length > 1 ? "s" : "") + " · tmux " + tmuxPreview;
2624
+ tr.appendChild(createCell("td", scopeText, "mono truncate", { title: true }));
2625
+ mergedBody.appendChild(tr);
2626
+
2627
+ const detailTr = document.createElement("tr");
2628
+ detailTr.className = "clock-detail-row";
2629
+ if (!isExpanded) detailTr.style.display = "none";
2630
+ const detailTd = document.createElement("td");
2631
+ detailTd.colSpan = 5;
2632
+ if (isExpanded) {
2633
+ const detailList = document.createElement("div");
2634
+ detailList.className = "clock-detail-list";
2635
+ const detailRows = item.rows.slice().sort((a, b) => {
2636
+ const aDue = Number.isFinite(a.dueMs) ? a.dueMs : Number.POSITIVE_INFINITY;
2637
+ const bDue = Number.isFinite(b.dueMs) ? b.dueMs : Number.POSITIVE_INFINITY;
2638
+ if (aDue !== bDue) return aDue - bDue;
2639
+ return a.sessionId.localeCompare(b.sessionId);
2640
+ });
2641
+ for (const detail of detailRows.slice(0, 40)) {
2642
+ const line = document.createElement("div");
2643
+ line.className = "clock-detail-item mono";
2644
+
2645
+ const timeSpan = document.createElement("span");
2646
+ timeSpan.textContent = detail.dueCompact;
2647
+ timeSpan.title = detail.dueAt;
2648
+ line.appendChild(timeSpan);
2649
+
2650
+ line.appendChild(pill(detail.status, detail.statusKind));
2651
+
2652
+ const idSpan = document.createElement("span");
2653
+ const tmuxPrefix = detail.tmuxShort && detail.tmuxShort !== "—" ? "tmux " + detail.tmuxShort + " · " : "";
2654
+ idSpan.textContent = tmuxPrefix + "sid " + detail.sessionShort + " · id " + detail.taskShort + " · " + detail.metaShort;
2655
+ idSpan.title = "tmux " + (detail.tmuxSessionId || "—") + " | session " + detail.sessionId + " | task " + detail.taskId + " | " + detail.tool + " | " + detail.recurrence;
2656
+ line.appendChild(idSpan);
2657
+
2658
+ const contentSpan = document.createElement("span");
2659
+ contentSpan.className = "clock-detail-content";
2660
+ contentSpan.textContent = detail.content || "(empty)";
2661
+ contentSpan.title = detail.content || "(empty)";
2662
+ line.appendChild(contentSpan);
2663
+
2664
+ detailList.appendChild(line);
2665
+ }
2666
+ if (detailRows.length > 40) {
2667
+ const more = document.createElement("div");
2668
+ more.className = "mono muted";
2669
+ more.textContent = "… " + String(detailRows.length - 40) + " more";
2670
+ detailList.appendChild(more);
2671
+ }
2672
+ detailTd.appendChild(detailList);
2673
+ }
2674
+ detailTr.appendChild(detailTd);
2675
+ mergedBody.appendChild(detailTr);
2676
+ }
2677
+ }
2678
+ }
2679
+
2680
+ if (daemonBody) {
2681
+ if (!records.length) {
2682
+ daemonBody.appendChild(createInfoRow(7, "No clock daemon bindings."));
2683
+ } else {
2684
+ for (const rec of records) {
2685
+ const tr = document.createElement("tr");
2686
+ const daemonId = textOf(rec && rec.daemonId).trim() || "—";
2687
+ const tmuxSession = textOf((rec && rec.tmuxSessionId) || (rec && rec.sessionId)).trim() || "—";
2688
+ const conversations = Array.isArray(rec && rec.conversationSessionIds) ? rec.conversationSessionIds : [];
2689
+ const hb = Number(rec && rec.lastHeartbeatAtMs);
2690
+ const inject = Number(rec && rec.lastInjectAtMs);
2691
+ const errText = textOf(rec && rec.lastError).trim();
2692
+ const isOnline = Number.isFinite(hb) && now - hb <= 45_000;
2693
+ const statusKind = errText ? "bad" : isOnline ? "ok" : "warn";
2694
+ const statusText = errText ? "error" : isOnline ? "online" : "stale";
2695
+
2696
+ tr.appendChild(createCell("td", daemonId, "mono truncate", { title: true }));
2697
+ const tmuxReadable = clockReadableTmuxSessionId(tmuxSession);
2698
+ const tmuxCell = createCell("td", tmuxReadable, "mono truncate", { title: true });
2699
+ tmuxCell.title = tmuxSession || tmuxReadable;
2700
+ tr.appendChild(tmuxCell);
2701
+ tr.appendChild(createCell("td", String(conversations.length), "mono"));
2702
+ tr.appendChild(createCell("td", Number.isFinite(hb) ? formatEpochWithDelta(hb) : "—", "mono truncate", { title: true }));
2703
+ tr.appendChild(createCell("td", Number.isFinite(inject) ? formatEpochWithDelta(inject) : "—", "mono truncate", { title: true }));
2704
+ const statusTd = document.createElement("td");
2705
+ statusTd.appendChild(pill(statusText, statusKind));
2706
+ tr.appendChild(statusTd);
2707
+ tr.appendChild(createCell("td", errText || "—", "mono truncate", { title: true }));
2708
+ daemonBody.appendChild(tr);
2709
+ }
2710
+ }
2711
+ }
2712
+
2713
+ if (hint) {
2714
+ hint.textContent = "groups " + merged.length + " · tasks " + rows.length + " · updated: " + formatTs(UI.clockSnapshotUpdatedAt);
2715
+ }
2716
+ }
2717
+
2718
+ async function refreshClockTab() {
2719
+ try {
2720
+ const filter = textOf($("clockSessionFilterInput") && $("clockSessionFilterInput").value).trim();
2721
+ const path = filter
2722
+ ? "/daemon/clock/tasks?sessionId=" + encodeURIComponent(filter)
2723
+ : "/daemon/clock/tasks";
2724
+ const data = await apiFetch(path);
2725
+ UI.clockSnapshot = data && typeof data === "object" ? data : {};
2726
+ UI.clockSnapshotUpdatedAt = Date.now();
2727
+ renderClockTabFromSnapshot();
2728
+ } catch (e) {
2729
+ const mergedBody = $("clockMergedTbody");
2730
+ const daemonBody = $("clockDaemonsTbody");
2731
+ if (mergedBody) {
2732
+ mergedBody.replaceChildren();
2733
+ mergedBody.appendChild(createErrorRow(5, e && e.message ? e.message : String(e)));
2734
+ }
2735
+ if (daemonBody) {
2736
+ daemonBody.replaceChildren();
2737
+ daemonBody.appendChild(createInfoRow(7, "No daemon bindings."));
2738
+ }
2739
+ const hint = $("clockHint");
2740
+ if (hint) hint.textContent = "clock snapshot failed";
2741
+ if (e && e.status === 401) notifyUnauthorizedOnce("clock");
2742
+ }
2743
+ }
2744
+
2103
2745
  async function refreshStatus() {
2104
2746
  // /health is public; use it to always show version even before admin login.
2105
2747
  try {
@@ -2121,6 +2763,7 @@
2121
2763
  $("statusDot").classList.add("err");
2122
2764
  $("serverId").textContent = "serverId: —";
2123
2765
  }
2766
+ await refreshClockOverview();
2124
2767
  }
2125
2768
 
2126
2769
  function formatTs(ms) {
@@ -2672,32 +3315,59 @@
2672
3315
  tr.appendChild(createCell("td", c.provider || "", "mono"));
2673
3316
  tr.appendChild(createCell("td", c.alias || "", "mono"));
2674
3317
  const statusTd = document.createElement("td");
2675
- statusTd.appendChild(pill(c.status || "—", (c.status || "") === "valid" ? "ok" : "warn"));
3318
+ const statusRaw = textOf(c.status || "").trim().toLowerCase();
3319
+ statusTd.appendChild(pill(c.status || "—", statusRaw === "valid" ? "ok" : "warn"));
2676
3320
  const issue = findAuthIssueForProviderAlias(c.provider, c.alias);
2677
3321
  if (issue) {
2678
3322
  statusTd.appendChild(document.createTextNode(" "));
2679
3323
  statusTd.appendChild(pill("verify required", "bad"));
2680
- if (issue.url) {
2681
- statusTd.appendChild(document.createTextNode(" "));
2682
- const a = document.createElement("a");
2683
- a.href = "#";
2684
- a.textContent = "open";
2685
- a.className = "mono";
2686
- a.addEventListener("click", (ev) => {
2687
- ev.preventDefault();
2688
- void openAuthIssueInCamoufox(c.provider, c.alias, issue.url);
2689
- });
2690
- statusTd.appendChild(a);
2691
- }
2692
3324
  }
2693
3325
  tr.appendChild(statusTd);
2694
3326
  tr.appendChild(createCell("td", exp, "mono"));
2695
3327
  tr.appendChild(createCell("td", c.secretRef || "—", "mono"));
3328
+
3329
+ const actionTd = document.createElement("td");
3330
+ actionTd.className = "actions-cell";
3331
+ const actions = document.createElement("div");
3332
+ actions.className = "auth-actions";
3333
+ let hasAction = false;
3334
+
3335
+ if (issue && issue.url) {
3336
+ const openVerify = document.createElement("button");
3337
+ openVerify.type = "button";
3338
+ openVerify.className = "auth-action-btn";
3339
+ openVerify.textContent = "Open Verify";
3340
+ openVerify.addEventListener("click", () => {
3341
+ void openAuthIssueInCamoufox(c.provider, c.alias, issue.url);
3342
+ });
3343
+ actions.appendChild(openVerify);
3344
+ hasAction = true;
3345
+ }
3346
+
3347
+ if (textOf(c.kind || "").trim().toLowerCase() === "oauth" && (statusRaw === "expired" || statusRaw === "invalid")) {
3348
+ const recover = document.createElement("button");
3349
+ recover.type = "button";
3350
+ recover.className = "auth-action-btn primary";
3351
+ recover.textContent = "Recover";
3352
+ recover.addEventListener("click", () => {
3353
+ void runCredentialAutoRecovery(c);
3354
+ });
3355
+ actions.appendChild(recover);
3356
+ hasAction = true;
3357
+ }
3358
+
3359
+ if (hasAction) {
3360
+ actionTd.appendChild(actions);
3361
+ } else {
3362
+ actionTd.classList.add("mono");
3363
+ actionTd.textContent = "—";
3364
+ }
3365
+ tr.appendChild(actionTd);
2696
3366
  body.appendChild(tr);
2697
3367
  }
2698
3368
  updateOauthAuthIssueHint();
2699
3369
  } catch (e) {
2700
- body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
3370
+ body.appendChild(createErrorRow(7, e && e.message ? e.message : e));
2701
3371
  updateOauthAuthIssueHint();
2702
3372
  }
2703
3373
  }
@@ -2727,6 +3397,7 @@
2727
3397
  async function authorizeOauth(mode = "manual") {
2728
3398
  const safeMode = mode === "auto" ? "auto" : "manual";
2729
3399
  setLog("credentialOpLog", "");
3400
+ appendLog("credentialOpLog", `[${safeMode}] start authorize request ...`);
2730
3401
  const provider = $("oauthProviderSelect").value;
2731
3402
  const alias = ($("oauthAuthAliasInput").value || "default").trim() || "default";
2732
3403
  updateOauthAuthIssueHint();
@@ -2762,6 +3433,167 @@
2762
3433
  }
2763
3434
  }
2764
3435
 
3436
+ function setOauthContext(providerRaw, aliasRaw) {
3437
+ const provider = textOf(providerRaw || "").trim().toLowerCase();
3438
+ const alias = textOf(aliasRaw || "default").trim() || "default";
3439
+ const providerSelect = $("oauthProviderSelect");
3440
+ if (providerSelect) {
3441
+ const optionExists = Array.from(providerSelect.options || []).some((opt) => textOf(opt.value).trim().toLowerCase() === provider);
3442
+ if (optionExists) {
3443
+ providerSelect.value = provider;
3444
+ }
3445
+ }
3446
+ const aliasInput = $("oauthAuthAliasInput");
3447
+ if (aliasInput) {
3448
+ aliasInput.value = alias;
3449
+ }
3450
+ updateOauthAuthIssueHint();
3451
+ }
3452
+
3453
+ async function openManualAuthFallback(providerRaw, aliasRaw, reason) {
3454
+ const provider = textOf(providerRaw || "").trim().toLowerCase();
3455
+ const alias = textOf(aliasRaw || "default").trim() || "default";
3456
+ selectTab("credentials");
3457
+ setOauthMode("manual");
3458
+ setOauthContext(provider, alias);
3459
+ if (reason) {
3460
+ appendLog("credentialOpLog", reason);
3461
+ }
3462
+
3463
+ appendLog("credentialOpLog", "[step3] 自动启动 Manual Auth(headful)…");
3464
+ try {
3465
+ await apiFetch("/config/settings", {
3466
+ method: "PUT",
3467
+ body: JSON.stringify({ oauthBrowser: "camoufox" })
3468
+ });
3469
+ } catch (e) {
3470
+ appendLog("credentialOpLog", "[step3] set oauthBrowser=camoufox failed: " + (e && e.message ? e.message : String(e)));
3471
+ }
3472
+
3473
+ try {
3474
+ const out = await apiFetch("/daemon/oauth/authorize", {
3475
+ method: "POST",
3476
+ body: JSON.stringify({
3477
+ provider,
3478
+ alias,
3479
+ mode: "manual",
3480
+ openBrowser: true,
3481
+ forceReauthorize: true,
3482
+ headful: true
3483
+ })
3484
+ });
3485
+ appendLog("credentialOpLog", "[step3] manual auth started: tokenFile=" + textOf(out && out.tokenFile ? out.tokenFile : "—"));
3486
+ appendLog("credentialOpLog", "[manual] 已尝试弹出认证窗口;若未自动完成,请在弹出的 portal 完成登录。");
3487
+ toast("Manual auth started: " + provider + "." + alias, "ok");
3488
+ return true;
3489
+ } catch (e) {
3490
+ appendLog("credentialOpLog", "[step3] manual auth auto-start failed: " + (e && e.message ? e.message : String(e)));
3491
+ appendLog("credentialOpLog", "[manual] 请点击 Start Manual Auth 重试。");
3492
+ }
3493
+
3494
+ try {
3495
+ const btn = $("oauthAuthorizeManualBtn");
3496
+ if (btn && typeof btn.scrollIntoView === "function") {
3497
+ btn.scrollIntoView({ behavior: "smooth", block: "center" });
3498
+ }
3499
+ if (btn && typeof btn.focus === "function") {
3500
+ btn.focus();
3501
+ }
3502
+ } catch {
3503
+ // ignore focus/scroll failures
3504
+ }
3505
+ return false;
3506
+ }
3507
+
3508
+ async function runCredentialAutoRecovery(cred) {
3509
+ const id = textOf(cred && cred.id ? cred.id : "").trim();
3510
+ const provider = textOf(cred && cred.provider ? cred.provider : "").trim().toLowerCase();
3511
+ const alias = textOf(cred && cred.alias ? cred.alias : "default").trim() || "default";
3512
+ if (!id || !provider) {
3513
+ setLog("credentialOpLog", "[recover] missing credential id/provider");
3514
+ return;
3515
+ }
3516
+
3517
+ selectTab("credentials");
3518
+ setOauthContext(provider, alias);
3519
+ setLog("credentialOpLog", "[recover] " + provider + "." + alias + " (id=" + id + ")");
3520
+
3521
+ let refreshOk = false;
3522
+ appendLog("credentialOpLog", "[step1] 尝试 refresh token …");
3523
+ try {
3524
+ const out = await apiFetch("/daemon/credentials/" + encodeURIComponent(id) + "/refresh", {
3525
+ method: "POST"
3526
+ });
3527
+ appendLog(
3528
+ "credentialOpLog",
3529
+ "[step1] refresh result: status=" + textOf(out && out.status ? out.status : "—") + " refreshed=" + (out && out.refreshed === true ? "yes" : "no")
3530
+ );
3531
+ await refreshCredentials();
3532
+ const latest = findCredentialForSelection(provider, alias);
3533
+ const latestStatus = textOf(latest && latest.status ? latest.status : "").trim().toLowerCase();
3534
+ const exp = Number(latest && latest.expiresInSec != null ? latest.expiresInSec : NaN);
3535
+ if (latestStatus === "valid" && (!Number.isFinite(exp) || exp > 0)) {
3536
+ refreshOk = true;
3537
+ appendLog("credentialOpLog", "[done] refresh 后已恢复有效 token。");
3538
+ toast("OAuth refreshed: " + provider + "." + alias, "ok");
3539
+ }
3540
+ } catch (e) {
3541
+ appendLog("credentialOpLog", "[step1] refresh failed: " + (e && e.message ? e.message : String(e)));
3542
+ }
3543
+ if (refreshOk) {
3544
+ return;
3545
+ }
3546
+
3547
+ appendLog("credentialOpLog", "[step2] refresh 未恢复,启动 Camoufox auto oauth …");
3548
+ try {
3549
+ await apiFetch("/config/settings", {
3550
+ method: "PUT",
3551
+ body: JSON.stringify({ oauthBrowser: "camoufox" })
3552
+ });
3553
+ } catch (e) {
3554
+ appendLog("credentialOpLog", "[step2] set oauthBrowser=camoufox failed: " + (e && e.message ? e.message : String(e)));
3555
+ }
3556
+
3557
+ try {
3558
+ const out = await apiFetch("/daemon/oauth/authorize", {
3559
+ method: "POST",
3560
+ body: JSON.stringify({
3561
+ provider,
3562
+ alias,
3563
+ mode: "auto",
3564
+ openBrowser: true,
3565
+ forceReauthorize: false,
3566
+ headful: false
3567
+ })
3568
+ });
3569
+ appendLog("credentialOpLog", "[step2] auto oauth ok: tokenFile=" + textOf(out && out.tokenFile ? out.tokenFile : "—"));
3570
+ await refreshCredentials();
3571
+ if (provider === "antigravity") {
3572
+ try {
3573
+ await refreshQuota();
3574
+ await refreshCredentials();
3575
+ } catch {
3576
+ // ignore quota refresh errors in recovery path
3577
+ }
3578
+ }
3579
+ const latest = findCredentialForSelection(provider, alias);
3580
+ const latestStatus = textOf(latest && latest.status ? latest.status : "").trim().toLowerCase();
3581
+ if (latestStatus === "valid") {
3582
+ appendLog("credentialOpLog", "[done] auto oauth 后 token 已恢复有效。");
3583
+ toast("OAuth recovered: " + provider + "." + alias, "ok");
3584
+ return;
3585
+ }
3586
+ appendLog("credentialOpLog", "[step2] auto oauth 执行完成,但状态仍非 valid。");
3587
+ } catch (e) {
3588
+ appendLog("credentialOpLog", "[step2] auto oauth failed: " + (e && e.message ? e.message : String(e)));
3589
+ }
3590
+
3591
+ const manualStarted = await openManualAuthFallback(provider, alias, "[step3] 自动恢复失败,切换到手动认证。");
3592
+ if (!manualStarted) {
3593
+ toast("Auto recovery failed, switched to manual auth (" + provider + "." + alias + ").");
3594
+ }
3595
+ }
3596
+
2765
3597
  const routingEditorState = {
2766
3598
  value: {},
2767
3599
  dirty: false
@@ -2873,29 +3705,69 @@
2873
3705
  return false;
2874
3706
  }
2875
3707
 
2876
- function resolveTargetToProviderKeys(target) {
2877
- const raw = textOf(target).trim();
2878
- if (!raw) return [];
2879
- const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
2880
- const keys = list.map((p) => textOf(p && p.providerKey ? p.providerKey : "")).filter(Boolean);
2881
- const known = new Set(keys);
2882
- if (known.has(raw)) return [raw];
2883
- const dot = raw.indexOf(".");
2884
- if (dot <= 0) return [];
2885
- const providerId = raw.slice(0, dot);
2886
- const modelId = raw.slice(dot + 1);
2887
- if (!providerId || !modelId) return [];
2888
- const prefix = `${providerId}.`;
2889
- const suffix = `.${modelId}`;
2890
- const out = [];
2891
- for (const k of keys) {
2892
- if (k.startsWith(prefix) && k.endsWith(suffix)) out.push(k);
2893
- }
2894
- out.sort((a, b) => a.localeCompare(b));
2895
- return out;
2896
- }
3708
+ function resolveTargetToProviderKeys(target, providersOverride) {
3709
+ const raw = textOf(target).trim();
3710
+ if (!raw) return [];
3711
+ const list = Array.isArray(providersOverride)
3712
+ ? providersOverride
3713
+ : (Array.isArray(UI.quotaProviders) ? UI.quotaProviders : []);
3714
+ const keys = list.map((p) => textOf(p && p.providerKey ? p.providerKey : "")).filter(Boolean);
3715
+ const known = new Set(keys);
3716
+ if (!known.size) return [];
3717
+ if (known.has(raw)) return [raw];
3718
+
3719
+ const candidates = new Set();
3720
+ const bracketMatch = raw.match(/^([^.[\]]+)\[([^\]]+)\]\.(.+)$/);
3721
+ if (bracketMatch) {
3722
+ const providerId = textOf(bracketMatch[1]).trim();
3723
+ const authAlias = textOf(bracketMatch[2]).trim();
3724
+ const modelId = textOf(bracketMatch[3]).trim();
3725
+ if (providerId && authAlias && modelId) {
3726
+ candidates.add(providerId + "." + authAlias + "." + modelId);
3727
+ }
3728
+ }
3729
+
3730
+ const dotted = raw.split(".").filter(Boolean);
3731
+ if (dotted.length >= 3) {
3732
+ const providerId = dotted[0];
3733
+ const modelId = dotted.slice(2).join(".");
3734
+ if (providerId && modelId) {
3735
+ candidates.add(raw);
3736
+ candidates.add(providerId + "." + modelId);
3737
+ }
3738
+ } else if (dotted.length === 2) {
3739
+ candidates.add(raw);
3740
+ }
3741
+
3742
+ for (const c of candidates) {
3743
+ if (known.has(c)) return [c];
3744
+ }
3745
+
3746
+ const out = [];
3747
+ const addByProviderModel = (providerId, modelId) => {
3748
+ if (!providerId || !modelId) return;
3749
+ const prefix = providerId + ".";
3750
+ const suffix = "." + modelId;
3751
+ for (const k of keys) {
3752
+ if (k.startsWith(prefix) && k.endsWith(suffix)) out.push(k);
3753
+ }
3754
+ };
2897
3755
 
2898
- function getQuotaStateByProviderKey(providerKey) {
3756
+ if (bracketMatch) {
3757
+ addByProviderModel(textOf(bracketMatch[1]).trim(), textOf(bracketMatch[3]).trim());
3758
+ }
3759
+
3760
+ if (dotted.length >= 2) {
3761
+ addByProviderModel(dotted[0], dotted.slice(1).join("."));
3762
+ if (dotted.length >= 3) {
3763
+ addByProviderModel(dotted[0], dotted.slice(2).join("."));
3764
+ }
3765
+ }
3766
+
3767
+ return Array.from(new Set(out)).sort((a, b) => a.localeCompare(b));
3768
+ }
3769
+
3770
+ function getQuotaStateByProviderKey(providerKey) {
2899
3771
  const map = UI.quotaProviderMap instanceof Map ? UI.quotaProviderMap : null;
2900
3772
  if (map && map.has(providerKey)) return map.get(providerKey);
2901
3773
  const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
@@ -3378,31 +4250,13 @@
3378
4250
  function resolveRoutedProviderKeys(routingTargets, providers) {
3379
4251
  const targets = routingTargets instanceof Set ? Array.from(routingTargets) : [];
3380
4252
  const list = Array.isArray(providers) ? providers : [];
3381
- const keys = [];
3382
- for (const p of list) {
3383
- const k = textOf(p && p.providerKey ? p.providerKey : "");
3384
- if (k) keys.push(k);
3385
- }
3386
- const known = new Set(keys);
3387
4253
  const resolved = new Set();
3388
- if (!targets.length || !known.size) return resolved;
4254
+ if (!targets.length || !list.length) return resolved;
3389
4255
 
3390
4256
  for (const t of targets) {
3391
- const target = textOf(t);
3392
- if (!target) continue;
3393
- if (known.has(target)) {
3394
- resolved.add(target);
3395
- continue;
3396
- }
3397
- const dot = target.indexOf(".");
3398
- if (dot <= 0) continue;
3399
- const providerId = target.slice(0, dot);
3400
- const modelId = target.slice(dot + 1);
3401
- if (!providerId || !modelId) continue;
3402
- const prefix = `${providerId}.`;
3403
- const suffix = `.${modelId}`;
3404
- for (const k of known) {
3405
- if (k.startsWith(prefix) && k.endsWith(suffix)) resolved.add(k);
4257
+ const mapped = resolveTargetToProviderKeys(t, list);
4258
+ for (const key of mapped) {
4259
+ if (key) resolved.add(key);
3406
4260
  }
3407
4261
  }
3408
4262
  return resolved;
@@ -3557,16 +4411,16 @@
3557
4411
 
3558
4412
  const filter = textOf($("quotaFilterInput").value || "").trim().toLowerCase();
3559
4413
  const hideOk = Boolean($("quotaHideOkToggle").checked);
3560
- const onlyRoutedTargets = Boolean($("quotaOnlyRoutedTargetsToggle").checked);
3561
- const routedProviderKeys = onlyRoutedTargets ? resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders) : null;
4414
+ const routedProviderKeys = resolveRoutedProviderKeys(UI.routingTargets, UI.quotaProviders);
3562
4415
 
3563
4416
  const list = Array.isArray(UI.quotaProviders) ? UI.quotaProviders : [];
3564
4417
  const next = list
3565
4418
  .filter((q) => {
3566
- const key = textOf(q && q.providerKey ? q.providerKey : "").toLowerCase();
4419
+ const keyRaw = textOf(q && q.providerKey ? q.providerKey : "");
4420
+ const key = keyRaw.toLowerCase();
3567
4421
  if (filter && !key.includes(filter)) return false;
3568
4422
  if (hideOk && q && q.inPool === true) return false;
3569
- if (routedProviderKeys && !routedProviderKeys.has(textOf(q && q.providerKey ? q.providerKey : ""))) return false;
4423
+ if (!routedProviderKeys.has(keyRaw)) return false;
3570
4424
  return true;
3571
4425
  })
3572
4426
  .slice();
@@ -4063,7 +4917,7 @@
4063
4917
  body.appendChild(tr);
4064
4918
  }
4065
4919
  } catch (e) {
4066
- body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
4920
+ body.appendChild(createErrorRow(7, e && e.message ? e.message : e));
4067
4921
  }
4068
4922
  }
4069
4923
 
@@ -4173,7 +5027,7 @@
4173
5027
  }
4174
5028
 
4175
5029
  // Bind events
4176
- document.querySelectorAll(".tab").forEach((btn) => {
5030
+ document.querySelectorAll(".tab[data-tab]").forEach((btn) => {
4177
5031
  btn.addEventListener("click", () => selectTab(btn.getAttribute("data-tab")));
4178
5032
  });
4179
5033
 
@@ -4309,6 +5163,14 @@
4309
5163
  .catch(() => {});
4310
5164
  });
4311
5165
 
5166
+ $("refreshClockBtn").addEventListener("click", refreshClockTab);
5167
+ $("clockSessionFilterInput").addEventListener("keydown", (ev) => {
5168
+ if (ev && ev.key === "Enter") {
5169
+ ev.preventDefault();
5170
+ void refreshClockTab();
5171
+ }
5172
+ });
5173
+
4312
5174
  $("restartRuntimeBtn").addEventListener("click", async () => {
4313
5175
  setLog("providerOpLog", "");
4314
5176
  if (!confirm("Reload config from disk and rebuild runtime now?")) return;
@@ -4455,10 +5317,22 @@
4455
5317
 
4456
5318
  $("refreshCredentialsBtn").addEventListener("click", refreshCredentials);
4457
5319
  $("saveOauthBrowserBtn").addEventListener("click", saveSettings);
4458
- $("oauthModeManualBtn").addEventListener("click", () => setOauthMode("manual"));
4459
- $("oauthModeAutoBtn").addEventListener("click", () => setOauthMode("auto"));
4460
- $("oauthAuthorizeManualBtn").addEventListener("click", () => void authorizeOauth("manual"));
4461
- $("oauthAuthorizeAutoBtn").addEventListener("click", () => void authorizeOauth("auto"));
5320
+ $("oauthModeManualBtn").addEventListener("click", () => {
5321
+ setOauthMode("manual");
5322
+ appendLog("credentialOpLog", "[ui] switched to manual mode");
5323
+ });
5324
+ $("oauthModeAutoBtn").addEventListener("click", () => {
5325
+ setOauthMode("auto");
5326
+ appendLog("credentialOpLog", "[ui] switched to auto mode");
5327
+ });
5328
+ $("oauthAuthorizeManualBtn").addEventListener("click", () => {
5329
+ appendLog("credentialOpLog", "[ui] click Start Manual Auth");
5330
+ void authorizeOauth("manual");
5331
+ });
5332
+ $("oauthAuthorizeAutoBtn").addEventListener("click", () => {
5333
+ appendLog("credentialOpLog", "[ui] click Run Auto Auth");
5334
+ void authorizeOauth("auto");
5335
+ });
4462
5336
  $("oauthVerifyOpenBtn").addEventListener("click", () => void openSelectedVerifyUrl());
4463
5337
  $("oauthVerifyCopyBtn").addEventListener("click", () => void copySelectedVerifyUrl());
4464
5338
  $("oauthVerifyRecheckBtn").addEventListener("click", async () => {