@jsonstudio/rcc 0.89.1121 → 0.89.1189

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 (194) hide show
  1. package/dist/build-info.js +2 -2
  2. package/dist/cli/commands/clean.d.ts +16 -0
  3. package/dist/cli/commands/clean.js +58 -0
  4. package/dist/cli/commands/clean.js.map +1 -0
  5. package/dist/cli/commands/code.d.ts +55 -0
  6. package/dist/cli/commands/code.js +376 -0
  7. package/dist/cli/commands/code.js.map +1 -0
  8. package/dist/cli/commands/config.d.ts +31 -0
  9. package/dist/cli/commands/config.js +168 -0
  10. package/dist/cli/commands/config.js.map +1 -0
  11. package/dist/cli/commands/env.d.ts +20 -0
  12. package/dist/cli/commands/env.js +73 -0
  13. package/dist/cli/commands/env.js.map +1 -0
  14. package/dist/cli/commands/examples.d.ts +5 -0
  15. package/dist/cli/commands/examples.js +66 -0
  16. package/dist/cli/commands/examples.js.map +1 -0
  17. package/dist/cli/commands/port.d.ts +24 -0
  18. package/dist/cli/commands/port.js +85 -0
  19. package/dist/cli/commands/port.js.map +1 -0
  20. package/dist/cli/commands/restart.d.ts +50 -0
  21. package/dist/cli/commands/restart.js +176 -0
  22. package/dist/cli/commands/restart.js.map +1 -0
  23. package/dist/cli/commands/start.d.ts +68 -0
  24. package/dist/cli/commands/start.js +295 -0
  25. package/dist/cli/commands/start.js.map +1 -0
  26. package/dist/cli/commands/status.d.ts +16 -0
  27. package/dist/cli/commands/status.js +104 -0
  28. package/dist/cli/commands/status.js.map +1 -0
  29. package/dist/cli/commands/stop.d.ts +35 -0
  30. package/dist/cli/commands/stop.js +95 -0
  31. package/dist/cli/commands/stop.js.map +1 -0
  32. package/dist/cli/logger.d.ts +8 -0
  33. package/dist/cli/logger.js +9 -0
  34. package/dist/cli/logger.js.map +1 -0
  35. package/dist/cli/main.d.ts +6 -0
  36. package/dist/cli/main.js +16 -0
  37. package/dist/cli/main.js.map +1 -0
  38. package/dist/cli/program.d.ts +8 -0
  39. package/dist/cli/program.js +16 -0
  40. package/dist/cli/program.js.map +1 -0
  41. package/dist/cli/register/basic-commands.d.ts +30 -0
  42. package/dist/cli/register/basic-commands.js +11 -0
  43. package/dist/cli/register/basic-commands.js.map +1 -0
  44. package/dist/cli/register/code-command.d.ts +3 -0
  45. package/dist/cli/register/code-command.js +5 -0
  46. package/dist/cli/register/code-command.js.map +1 -0
  47. package/dist/cli/register/restart-command.d.ts +3 -0
  48. package/dist/cli/register/restart-command.js +5 -0
  49. package/dist/cli/register/restart-command.js.map +1 -0
  50. package/dist/cli/register/start-command.d.ts +3 -0
  51. package/dist/cli/register/start-command.js +5 -0
  52. package/dist/cli/register/start-command.js.map +1 -0
  53. package/dist/cli/register/status-config-commands.d.ts +16 -0
  54. package/dist/cli/register/status-config-commands.js +7 -0
  55. package/dist/cli/register/status-config-commands.js.map +1 -0
  56. package/dist/cli/register/stop-command.d.ts +3 -0
  57. package/dist/cli/register/stop-command.js +5 -0
  58. package/dist/cli/register/stop-command.js.map +1 -0
  59. package/dist/cli/runtime.d.ts +5 -0
  60. package/dist/cli/runtime.js +11 -0
  61. package/dist/cli/runtime.js.map +1 -0
  62. package/dist/cli/server/port-utils.d.ts +52 -0
  63. package/dist/cli/server/port-utils.js +193 -0
  64. package/dist/cli/server/port-utils.js.map +1 -0
  65. package/dist/cli/spinner.d.ts +10 -0
  66. package/dist/cli/spinner.js +59 -0
  67. package/dist/cli/spinner.js.map +1 -0
  68. package/dist/cli/utils/normalize.d.ts +2 -0
  69. package/dist/cli/utils/normalize.js +22 -0
  70. package/dist/cli/utils/normalize.js.map +1 -0
  71. package/dist/cli/utils/safe-read-json.d.ts +1 -0
  72. package/dist/cli/utils/safe-read-json.js +11 -0
  73. package/dist/cli/utils/safe-read-json.js.map +1 -0
  74. package/dist/cli.js +149 -1738
  75. package/dist/cli.js.map +1 -1
  76. package/dist/client/anthropic/anthropic-protocol-client.js +4 -3
  77. package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
  78. package/dist/client/gemini/gemini-protocol-client.js +5 -0
  79. package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
  80. package/dist/client/gemini-cli/gemini-cli-protocol-client.d.ts +1 -1
  81. package/dist/client/gemini-cli/gemini-cli-protocol-client.js +10 -3
  82. package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
  83. package/dist/commands/provider-update.js +355 -5
  84. package/dist/commands/provider-update.js.map +1 -1
  85. package/dist/commands/quota-daemon.js +2 -2
  86. package/dist/commands/quota-daemon.js.map +1 -1
  87. package/dist/config/provider-v2-loader.js +4 -2
  88. package/dist/config/provider-v2-loader.js.map +1 -1
  89. package/dist/docs/daemon-admin-ui.html +583 -87
  90. package/dist/index.js +32 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/manager/modules/quota/index.d.ts +19 -1
  93. package/dist/manager/modules/quota/index.js +130 -5
  94. package/dist/manager/modules/quota/index.js.map +1 -1
  95. package/dist/manager/modules/routing/index.js.map +1 -1
  96. package/dist/manager/storage/file-store.js +1 -1
  97. package/dist/manager/storage/file-store.js.map +1 -1
  98. package/dist/manager/types.d.ts +5 -0
  99. package/dist/providers/auth/oauth-lifecycle.js +2 -2
  100. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  101. package/dist/providers/core/api/provider-config.d.ts +2 -0
  102. package/dist/providers/core/api/provider-types.d.ts +2 -0
  103. package/dist/providers/core/config/service-profiles.js +1 -1
  104. package/dist/providers/core/config/service-profiles.js.map +1 -1
  105. package/dist/providers/core/runtime/base-provider.js +21 -27
  106. package/dist/providers/core/runtime/base-provider.js.map +1 -1
  107. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +1 -0
  108. package/dist/providers/core/runtime/gemini-cli-http-provider.js +37 -6
  109. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  110. package/dist/providers/core/runtime/http-request-executor.js +23 -29
  111. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  112. package/dist/providers/core/runtime/http-transport-provider.js +46 -38
  113. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  114. package/dist/providers/core/utils/http-client.d.ts +9 -0
  115. package/dist/providers/core/utils/http-client.js +9 -11
  116. package/dist/providers/core/utils/http-client.js.map +1 -1
  117. package/dist/providers/core/utils/provider-error-reporter.js +2 -6
  118. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  119. package/dist/providers/mock/mock-provider-runtime.js +19 -5
  120. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  121. package/dist/server/handlers/handler-utils.d.ts +1 -1
  122. package/dist/server/handlers/handler-utils.js +4 -4
  123. package/dist/server/handlers/handler-utils.js.map +1 -1
  124. package/dist/server/handlers/responses-handler.js +2 -1
  125. package/dist/server/handlers/responses-handler.js.map +1 -1
  126. package/dist/server/handlers/sse-dispatcher.js +1 -4
  127. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  128. package/dist/server/runtime/http-server/colored-logger.d.ts +1 -1
  129. package/dist/server/runtime/http-server/colored-logger.js +22 -10
  130. package/dist/server/runtime/http-server/colored-logger.js.map +1 -1
  131. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +12 -6
  132. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  133. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +116 -98
  134. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  135. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +108 -15
  136. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  137. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +2 -1
  138. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -1
  139. package/dist/server/runtime/http-server/daemon-admin/stats-handler.d.ts +3 -0
  140. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +56 -0
  141. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -0
  142. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +8 -4
  143. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  144. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +9 -0
  145. package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
  146. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  147. package/dist/server/runtime/http-server/executor-provider.js +74 -0
  148. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  149. package/dist/server/runtime/http-server/index.d.ts +2 -0
  150. package/dist/server/runtime/http-server/index.js +107 -17
  151. package/dist/server/runtime/http-server/index.js.map +1 -1
  152. package/dist/server/runtime/http-server/request-executor.js +18 -11
  153. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  154. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  155. package/dist/server/runtime/http-server/routes.js +17 -4
  156. package/dist/server/runtime/http-server/routes.js.map +1 -1
  157. package/dist/server/runtime/http-server/stats-manager.d.ts +7 -0
  158. package/dist/server/runtime/http-server/stats-manager.js +31 -6
  159. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  160. package/dist/server/runtime/http-server/types.d.ts +5 -0
  161. package/dist/server/utils/http-error-mapper.js +70 -9
  162. package/dist/server/utils/http-error-mapper.js.map +1 -1
  163. package/dist/server/utils/request-id-manager.js +9 -5
  164. package/dist/server/utils/request-id-manager.js.map +1 -1
  165. package/dist/server/utils/sse-request-parser.js +2 -1
  166. package/dist/server/utils/sse-request-parser.js.map +1 -1
  167. package/dist/server/utils/utf8-chunk-buffer.d.ts +15 -30
  168. package/dist/server/utils/utf8-chunk-buffer.js +78 -88
  169. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -1
  170. package/dist/server/utils/warmup-storm-tracker.js +1 -1
  171. package/dist/server/utils/warmup-storm-tracker.js.map +1 -1
  172. package/dist/tools/provider-update/fetch-models.js +8 -5
  173. package/dist/tools/provider-update/fetch-models.js.map +1 -1
  174. package/dist/tools/provider-update/probe-context.d.ts +24 -0
  175. package/dist/tools/provider-update/probe-context.js +199 -0
  176. package/dist/tools/provider-update/probe-context.js.map +1 -0
  177. package/dist/tools/provider-update/types.d.ts +1 -0
  178. package/package.json +10 -4
  179. package/scripts/anthropic-compare-modes.mjs +40 -3
  180. package/scripts/antigravity-smoke.mjs +180 -0
  181. package/scripts/backfill-apply-patch-exec-errorsamples.mjs +225 -0
  182. package/scripts/compare-codex-rccx.mjs +59 -1
  183. package/scripts/compare-responses-request.mjs +50 -4
  184. package/scripts/lib/errorsamples.mjs +23 -0
  185. package/scripts/mock-provider/run-regressions.mjs +12 -2
  186. package/scripts/policy-violations-report.mjs +257 -0
  187. package/scripts/publish-rcc.mjs +16 -2
  188. package/scripts/scan-apply-patch-samples.mjs +148 -7
  189. package/scripts/tests/unified-hub-responses-enforce-safe.mjs +37 -0
  190. package/scripts/tests/unified-hub-shadow-regression.mjs +55 -0
  191. package/scripts/unified-hub-shadow-compare.mjs +359 -0
  192. package/scripts/verify-e2e-gemini-followup-sample.mjs +269 -0
  193. package/scripts/virtual-router-shadow-v2-real.mjs +71 -1
  194. package/scripts/virtual-router-shadow-v2.mjs +41 -0
@@ -32,7 +32,7 @@
32
32
  }
33
33
 
34
34
  .container {
35
- max-width: 1120px;
35
+ max-width: 1680px;
36
36
  margin: 22px auto 40px;
37
37
  padding: 0 16px;
38
38
  }
@@ -50,6 +50,7 @@
50
50
  align-items: center;
51
51
  justify-content: space-between;
52
52
  gap: 14px;
53
+ flex-wrap: wrap;
53
54
  margin-bottom: 14px;
54
55
  }
55
56
 
@@ -156,6 +157,7 @@
156
157
  display: flex;
157
158
  gap: 6px;
158
159
  margin: 14px 0 10px;
160
+ flex-wrap: wrap;
159
161
  }
160
162
 
161
163
  .tab {
@@ -178,6 +180,15 @@
178
180
  gap: 12px;
179
181
  }
180
182
 
183
+ .grid.grid-wide-left {
184
+ grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
185
+ }
186
+
187
+ /* Ensure grid children can shrink without overflowing into the next column */
188
+ .grid > .card {
189
+ min-width: 0;
190
+ }
191
+
181
192
  @media (max-width: 980px) {
182
193
  .grid {
183
194
  grid-template-columns: minmax(0, 1fr);
@@ -199,9 +210,7 @@
199
210
  .table {
200
211
  width: 100%;
201
212
  border-collapse: collapse;
202
- border-radius: 12px;
203
- overflow: hidden;
204
- border: 1px solid var(--border);
213
+ table-layout: fixed;
205
214
  }
206
215
 
207
216
  .table th,
@@ -210,6 +219,8 @@
210
219
  font-size: 12px;
211
220
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
212
221
  vertical-align: top;
222
+ overflow-wrap: anywhere;
223
+ word-break: break-word;
213
224
  }
214
225
 
215
226
  .table th {
@@ -219,10 +230,52 @@
219
230
  background: rgba(255, 255, 255, 0.03);
220
231
  }
221
232
 
233
+ .table tr.group-row td {
234
+ background: rgba(255, 255, 255, 0.02);
235
+ color: rgba(255, 255, 255, 0.86);
236
+ font-weight: 650;
237
+ }
238
+
239
+ .indent {
240
+ padding-left: 22px !important;
241
+ }
242
+
243
+ .table-wrap {
244
+ width: 100%;
245
+ max-width: 100%;
246
+ overflow: auto;
247
+ border-radius: 12px;
248
+ border: 1px solid var(--border);
249
+ }
250
+
251
+ .table-wrap .table {
252
+ border: 0;
253
+ min-width: 860px;
254
+ }
255
+
256
+ .table td.actions-cell {
257
+ width: 220px;
258
+ overflow: visible;
259
+ }
260
+
261
+ .actions {
262
+ display: flex;
263
+ gap: 8px;
264
+ flex-wrap: wrap;
265
+ justify-content: flex-end;
266
+ }
267
+
268
+ .truncate {
269
+ white-space: nowrap;
270
+ overflow: hidden;
271
+ text-overflow: ellipsis;
272
+ }
273
+
222
274
  .mono {
223
275
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
224
276
  "Courier New", monospace;
225
277
  color: var(--muted);
278
+ word-break: break-all;
226
279
  }
227
280
 
228
281
  .notice {
@@ -246,6 +299,25 @@
246
299
  border-radius: 12px;
247
300
  padding: 10px 12px;
248
301
  }
302
+
303
+ @media (max-width: 640px) {
304
+ header {
305
+ flex-direction: column;
306
+ align-items: flex-start;
307
+ }
308
+ .statusline {
309
+ width: 100%;
310
+ justify-content: flex-start;
311
+ }
312
+ .row {
313
+ align-items: stretch;
314
+ }
315
+ .row > input,
316
+ .row > select,
317
+ .row > button {
318
+ max-width: 100%;
319
+ }
320
+ }
249
321
  </style>
250
322
  </head>
251
323
  <body>
@@ -282,12 +354,14 @@
282
354
 
283
355
  <div class="tabs">
284
356
  <button class="tab active" data-tab="providers">Provider Pool</button>
357
+ <button class="tab" data-tab="tokens">Token Stats</button>
285
358
  <button class="tab" data-tab="credentials">Auth Provider Pool</button>
359
+ <button class="tab" data-tab="quota">Quota Pool</button>
286
360
  <button class="tab" data-tab="routing">Runtime Routing Pool</button>
287
361
  </div>
288
362
 
289
363
  <section id="panelProviders" data-panel="providers">
290
- <div class="grid">
364
+ <div class="grid grid-wide-left">
291
365
  <div class="card" style="box-shadow: none;">
292
366
  <p class="section-title">Providers in <span class="mono">virtualrouter.providers</span></p>
293
367
  <p class="section-sub">
@@ -297,20 +371,23 @@
297
371
  <button id="refreshProvidersBtn" class="primary">Refresh</button>
298
372
  <button id="newProviderBtn">New provider…</button>
299
373
  </div>
300
- <table class="table">
301
- <thead>
302
- <tr>
303
- <th>id</th>
304
- <th>type</th>
305
- <th>enabled</th>
306
- <th>baseURL</th>
307
- <th>models</th>
308
- <th>auth</th>
309
- <th></th>
310
- </tr>
311
- </thead>
312
- <tbody id="providersTbody"></tbody>
313
- </table>
374
+ <div class="table-wrap">
375
+ <table class="table">
376
+ <thead>
377
+ <tr>
378
+ <th>id</th>
379
+ <th>type</th>
380
+ <th>enabled</th>
381
+ <th>baseURL</th>
382
+ <th>models</th>
383
+ <th>compat</th>
384
+ <th>auth</th>
385
+ <th></th>
386
+ </tr>
387
+ </thead>
388
+ <tbody id="providersTbody"></tbody>
389
+ </table>
390
+ </div>
314
391
  </div>
315
392
 
316
393
  <div class="card" style="box-shadow: none;">
@@ -414,6 +491,37 @@
414
491
  </div>
415
492
  </section>
416
493
 
494
+ <section id="panelTokens" data-panel="tokens" style="display:none;">
495
+ <div class="grid">
496
+ <div class="card" style="box-shadow:none;">
497
+ <p class="section-title">Token usage (session + historical)</p>
498
+ <p class="section-sub">
499
+ Session stats reset on server restart. Historical totals are aggregated from
500
+ <span class="mono">~/.routecodex/logs/provider-stats.jsonl</span> (best-effort).
501
+ </p>
502
+ <div class="row" style="margin-bottom: 10px;">
503
+ <button id="refreshTokensBtn" class="primary">Refresh</button>
504
+ </div>
505
+ <div class="notice mono" id="tokenTotalsBox" style="white-space: pre-wrap;"></div>
506
+ <div class="table-wrap" style="margin-top: 10px;">
507
+ <table class="table">
508
+ <thead>
509
+ <tr>
510
+ <th>providerKey</th>
511
+ <th>model</th>
512
+ <th>session req/err</th>
513
+ <th>session tokens in/out/total</th>
514
+ <th>historical req/err</th>
515
+ <th>historical tokens in/out/total</th>
516
+ </tr>
517
+ </thead>
518
+ <tbody id="tokensTbody"></tbody>
519
+ </table>
520
+ </div>
521
+ </div>
522
+ </div>
523
+ </section>
524
+
417
525
  <section id="panelCredentials" data-panel="credentials" style="display:none;">
418
526
  <div class="grid">
419
527
  <div class="card" style="box-shadow:none;">
@@ -424,19 +532,21 @@
424
532
  <div class="row" style="margin-bottom: 10px;">
425
533
  <button id="refreshCredentialsBtn" class="primary">Refresh</button>
426
534
  </div>
427
- <table class="table">
428
- <thead>
429
- <tr>
430
- <th>kind</th>
431
- <th>provider</th>
432
- <th>alias</th>
433
- <th>status</th>
434
- <th>expires</th>
435
- <th>secretRef</th>
436
- </tr>
437
- </thead>
438
- <tbody id="credentialsTbody"></tbody>
439
- </table>
535
+ <div class="table-wrap">
536
+ <table class="table">
537
+ <thead>
538
+ <tr>
539
+ <th>kind</th>
540
+ <th>provider</th>
541
+ <th>alias</th>
542
+ <th>status</th>
543
+ <th>expires</th>
544
+ <th>secretRef</th>
545
+ </tr>
546
+ </thead>
547
+ <tbody id="credentialsTbody"></tbody>
548
+ </table>
549
+ </div>
440
550
  </div>
441
551
 
442
552
  <div class="card" style="box-shadow:none;">
@@ -476,6 +586,52 @@
476
586
  </div>
477
587
  </section>
478
588
 
589
+ <section id="panelQuota" data-panel="quota" style="display:none;">
590
+ <div class="grid">
591
+ <div class="card" style="box-shadow:none;">
592
+ <p class="section-title">Quota (daemon)</p>
593
+ <p class="section-sub">
594
+ VirtualRouter consumes this via <span class="mono">quotaView</span>. When
595
+ <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>
600
+ </div>
601
+ <div class="table-wrap">
602
+ <table class="table">
603
+ <thead>
604
+ <tr>
605
+ <th>providerKey</th>
606
+ <th>auth</th>
607
+ <th>inPool</th>
608
+ <th>reason</th>
609
+ <th>cooldownUntil</th>
610
+ <th>blacklistUntil</th>
611
+ <th>errCount</th>
612
+ <th></th>
613
+ </tr>
614
+ </thead>
615
+ <tbody id="quotaTbody"></tbody>
616
+ </table>
617
+ </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">
624
+ <div style="margin-bottom: 6px;">
625
+ 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>
632
+ </div>
633
+ </section>
634
+
479
635
  <section id="panelRouting" data-panel="routing" style="display:none;">
480
636
  <div class="grid">
481
637
  <div class="card" style="box-shadow:none;">
@@ -497,18 +653,20 @@
497
653
  <div class="row" style="margin-bottom: 10px;">
498
654
  <button id="refreshRuntimesBtn" class="primary">Refresh</button>
499
655
  </div>
500
- <table class="table">
501
- <thead>
502
- <tr>
503
- <th>providerKey</th>
504
- <th>runtimeKey</th>
505
- <th>family</th>
506
- <th>protocol</th>
507
- <th>series</th>
508
- </tr>
509
- </thead>
510
- <tbody id="runtimesTbody"></tbody>
511
- </table>
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>
512
670
  </div>
513
671
  </div>
514
672
  </section>
@@ -522,7 +680,10 @@
522
680
  const el = $(id);
523
681
  if (!el) return;
524
682
  el.style.display = value ? "block" : "none";
525
- el.textContent = value || "";
683
+ const raw = value || "";
684
+ const max = 12000;
685
+ const out = raw.length > max ? raw.slice(0, max) + "\n…(truncated)" : raw;
686
+ el.textContent = out;
526
687
  }
527
688
 
528
689
  function getApiKey() {
@@ -571,10 +732,71 @@
571
732
  });
572
733
  const panels = [
573
734
  { name: "providers", el: $("panelProviders") },
735
+ { name: "tokens", el: $("panelTokens") },
574
736
  { name: "credentials", el: $("panelCredentials") },
737
+ { name: "quota", el: $("panelQuota") },
575
738
  { name: "routing", el: $("panelRouting") }
576
739
  ];
577
740
  for (const p of panels) p.el.style.display = p.name === name ? "block" : "none";
741
+
742
+ // Light auto-refresh on tab switch to avoid showing stale "Unauthorized" after setting apikey.
743
+ void maybeRefreshTab(name);
744
+ }
745
+
746
+ function getActiveTab() {
747
+ const active = document.querySelector(".tab.active");
748
+ const name = active ? active.getAttribute("data-tab") : null;
749
+ return name || "providers";
750
+ }
751
+
752
+ const tabLastRefreshedAt = {
753
+ providers: 0,
754
+ tokens: 0,
755
+ credentials: 0,
756
+ quota: 0,
757
+ routing: 0
758
+ };
759
+
760
+ async function maybeRefreshTab(name) {
761
+ const key = name in tabLastRefreshedAt ? name : "providers";
762
+ const now = Date.now();
763
+ if (now - tabLastRefreshedAt[key] < 1500) {
764
+ return;
765
+ }
766
+ tabLastRefreshedAt[key] = now;
767
+ try {
768
+ if (key === "providers") await refreshProviders();
769
+ else if (key === "tokens") await refreshTokens();
770
+ else if (key === "credentials") await refreshCredentials();
771
+ else if (key === "quota") await refreshQuota();
772
+ else if (key === "routing") await refreshRuntimes();
773
+ } catch {
774
+ // ignore refresh failures on tab switch
775
+ }
776
+ }
777
+
778
+ function textOf(value) {
779
+ if (value === null || value === undefined) return "";
780
+ return String(value);
781
+ }
782
+
783
+ function createCell(tag, text, className, opts = {}) {
784
+ const el = document.createElement(tag);
785
+ if (className) el.className = className;
786
+ const s = textOf(text);
787
+ el.textContent = s;
788
+ if (opts.title && s) el.title = s;
789
+ return el;
790
+ }
791
+
792
+ function createErrorRow(colSpan, message) {
793
+ const tr = document.createElement("tr");
794
+ const td = document.createElement("td");
795
+ td.colSpan = colSpan;
796
+ td.className = "mono";
797
+ td.textContent = `Failed to load: ${textOf(message)}`;
798
+ tr.appendChild(td);
799
+ return tr;
578
800
  }
579
801
 
580
802
  function presetFor(type) {
@@ -642,29 +864,67 @@
642
864
 
643
865
  async function refreshProviders() {
644
866
  const body = $("providersTbody");
645
- body.innerHTML = "";
867
+ body.replaceChildren();
646
868
  try {
647
869
  const data = await apiFetch("/config/providers");
648
- for (const p of data.providers || []) {
649
- const tr = document.createElement("tr");
650
- tr.innerHTML = `
651
- <td class="mono">${p.id || ""}</td>
652
- <td>${p.type || ""}</td>
653
- <td>${String(p.enabled)}</td>
654
- <td class="mono">${p.baseURL || ""}</td>
655
- <td>${p.modelCount || 0}</td>
656
- <td>${p.authType || ""}</td>
657
- <td>
658
- <button data-action="edit" data-id="${p.id}">Edit</button>
659
- <button data-action="delete" data-id="${p.id}" class="danger">Delete</button>
660
- </td>
661
- `;
662
- body.appendChild(tr);
870
+ const list = Array.isArray(data.providers) ? data.providers : [];
871
+ const grouped = new Map();
872
+ for (const p of list) {
873
+ const t = textOf(p.type || "unknown") || "unknown";
874
+ if (!grouped.has(t)) grouped.set(t, []);
875
+ grouped.get(t).push(p);
876
+ }
877
+ const types = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b));
878
+ for (const type of types) {
879
+ const groupRow = document.createElement("tr");
880
+ groupRow.className = "group-row";
881
+ const groupCell = document.createElement("td");
882
+ groupCell.colSpan = 8;
883
+ groupCell.textContent = `${type} (${grouped.get(type).length})`;
884
+ groupRow.appendChild(groupCell);
885
+ body.appendChild(groupRow);
886
+
887
+ const items = grouped.get(type);
888
+ items.sort((a, b) => textOf(a.id).localeCompare(textOf(b.id)));
889
+ for (const p of items) {
890
+ const tr = document.createElement("tr");
891
+ tr.appendChild(createCell("td", p.id || "", "mono indent"));
892
+ tr.appendChild(createCell("td", p.type || "", ""));
893
+ tr.appendChild(createCell("td", String(Boolean(p.enabled)), ""));
894
+ tr.appendChild(createCell("td", p.baseURL || "", "mono truncate", { title: true }));
895
+ const preview = Array.isArray(p.modelsPreview) ? p.modelsPreview.map((x) => textOf(x)).filter(Boolean) : [];
896
+ const modelSummary = preview.length ? `${p.modelCount || 0}: ${preview.join(", ")}${(p.modelCount || 0) > preview.length ? ", …" : ""}` : String(p.modelCount || 0);
897
+ tr.appendChild(createCell("td", modelSummary, "mono truncate", { title: true }));
898
+ tr.appendChild(createCell("td", p.compatibilityProfile || "", "mono truncate", { title: true }));
899
+ tr.appendChild(createCell("td", p.authType || "", ""));
900
+ const actionsTd = document.createElement("td");
901
+ actionsTd.className = "actions-cell";
902
+ const box = document.createElement("div");
903
+ box.className = "actions";
904
+ const edit = document.createElement("button");
905
+ edit.textContent = "Edit";
906
+ edit.setAttribute("data-action", "edit");
907
+ edit.setAttribute("data-id", textOf(p.id));
908
+ const test = document.createElement("button");
909
+ test.textContent = "Test";
910
+ test.setAttribute("data-action", "test");
911
+ test.setAttribute("data-id", textOf(p.id));
912
+ test.disabled = !(p && p.enabled !== false && Number(p.modelCount || 0) > 0);
913
+ const del = document.createElement("button");
914
+ del.textContent = "Delete";
915
+ del.className = "danger";
916
+ del.setAttribute("data-action", "delete");
917
+ del.setAttribute("data-id", textOf(p.id));
918
+ box.appendChild(edit);
919
+ box.appendChild(test);
920
+ box.appendChild(del);
921
+ actionsTd.appendChild(box);
922
+ tr.appendChild(actionsTd);
923
+ body.appendChild(tr);
924
+ }
663
925
  }
664
926
  } catch (e) {
665
- const tr = document.createElement("tr");
666
- tr.innerHTML = `<td colspan="7" class="mono">Failed to load: ${e.message}</td>`;
667
- body.appendChild(tr);
927
+ body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
668
928
  }
669
929
  }
670
930
 
@@ -777,26 +1037,22 @@
777
1037
 
778
1038
  async function refreshCredentials() {
779
1039
  const body = $("credentialsTbody");
780
- body.innerHTML = "";
1040
+ body.replaceChildren();
781
1041
  try {
782
1042
  const items = await apiFetch("/daemon/credentials");
783
1043
  for (const c of items || []) {
784
1044
  const tr = document.createElement("tr");
785
1045
  const exp = c.expiresInSec == null ? "—" : `${c.expiresInSec}s`;
786
- tr.innerHTML = `
787
- <td>${c.kind}</td>
788
- <td class="mono">${c.provider}</td>
789
- <td class="mono">${c.alias}</td>
790
- <td>${c.status}</td>
791
- <td class="mono">${exp}</td>
792
- <td class="mono">${c.secretRef || "—"}</td>
793
- `;
1046
+ tr.appendChild(createCell("td", c.kind || "", ""));
1047
+ tr.appendChild(createCell("td", c.provider || "", "mono"));
1048
+ tr.appendChild(createCell("td", c.alias || "", "mono"));
1049
+ tr.appendChild(createCell("td", c.status || "", ""));
1050
+ tr.appendChild(createCell("td", exp, "mono"));
1051
+ tr.appendChild(createCell("td", c.secretRef || "—", "mono"));
794
1052
  body.appendChild(tr);
795
1053
  }
796
1054
  } catch (e) {
797
- const tr = document.createElement("tr");
798
- tr.innerHTML = `<td colspan="6" class="mono">Failed to load: ${e.message}</td>`;
799
- body.appendChild(tr);
1055
+ body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
800
1056
  }
801
1057
  }
802
1058
 
@@ -867,24 +1123,236 @@
867
1123
 
868
1124
  async function refreshRuntimes() {
869
1125
  const body = $("runtimesTbody");
870
- body.innerHTML = "";
1126
+ body.replaceChildren();
871
1127
  try {
872
1128
  const items = await apiFetch("/providers/runtimes");
873
1129
  for (const r of items || []) {
874
1130
  const tr = document.createElement("tr");
875
- tr.innerHTML = `
876
- <td class="mono">${r.providerKey || ""}</td>
877
- <td class="mono">${r.runtimeKey || ""}</td>
878
- <td>${r.family || ""}</td>
879
- <td>${r.protocol || ""}</td>
880
- <td>${r.series || ""}</td>
881
- `;
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 || "", ""));
882
1136
  body.appendChild(tr);
883
1137
  }
884
1138
  } catch (e) {
885
- const tr = document.createElement("tr");
886
- tr.innerHTML = `<td colspan="5" class="mono">Failed to load: ${e.message}</td>`;
887
- body.appendChild(tr);
1139
+ body.appendChild(createErrorRow(5, e && e.message ? e.message : e));
1140
+ }
1141
+ }
1142
+
1143
+ function formatEpochMs(ms) {
1144
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "—";
1145
+ try {
1146
+ return new Date(ms).toLocaleString();
1147
+ } catch {
1148
+ return String(ms);
1149
+ }
1150
+ }
1151
+
1152
+ async function refreshQuota() {
1153
+ const body = $("quotaTbody");
1154
+ body.replaceChildren();
1155
+ setLog("quotaOpLog", "");
1156
+ try {
1157
+ const out = await apiFetch("/quota/providers");
1158
+ const list = Array.isArray(out.providers) ? out.providers : [];
1159
+ for (const q of list) {
1160
+ 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);
1190
+ body.appendChild(tr);
1191
+ }
1192
+ } catch (e) {
1193
+ body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
1194
+ }
1195
+ }
1196
+
1197
+ function formatInt(n) {
1198
+ const v = typeof n === "number" && Number.isFinite(n) ? n : 0;
1199
+ try { return v.toLocaleString(); } catch { return String(v); }
1200
+ }
1201
+
1202
+ function formatTokensRow(label, totals) {
1203
+ const inTok = formatInt(totals.totalPromptTokens);
1204
+ const outTok = formatInt(totals.totalCompletionTokens);
1205
+ const totTok = formatInt(totals.totalOutputTokens);
1206
+ const req = formatInt(totals.requestCount);
1207
+ const err = formatInt(totals.errorCount);
1208
+ return `${label}: requests=${req} (err=${err}) tokens in/out/total=${inTok}/${outTok}/${totTok}`;
1209
+ }
1210
+
1211
+ async function refreshTokens() {
1212
+ const body = $("tokensTbody");
1213
+ body.replaceChildren();
1214
+ $("tokenTotalsBox").textContent = "";
1215
+ try {
1216
+ const out = await apiFetch("/daemon/stats");
1217
+ const session = out && out.session ? out.session : null;
1218
+ const historical = out && out.historical ? out.historical : null;
1219
+ const totals = out && out.totals ? out.totals : null;
1220
+
1221
+ const sessionTotals = totals && totals.session ? totals.session : { requestCount: 0, errorCount: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalOutputTokens: 0 };
1222
+ const historicalTotals = totals && totals.historical ? totals.historical : { requestCount: 0, errorCount: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalOutputTokens: 0 };
1223
+
1224
+ const lines = [];
1225
+ lines.push(formatTokensRow("ALL (session)", sessionTotals));
1226
+ lines.push(formatTokensRow("ALL (historical)", historicalTotals));
1227
+ $("tokenTotalsBox").textContent = lines.join("\n");
1228
+
1229
+ const sessionRows = session && Array.isArray(session.totals) ? session.totals : [];
1230
+ const histRows = historical && Array.isArray(historical.totals) ? historical.totals : [];
1231
+
1232
+ const byKey = new Map();
1233
+ const keyOf = (r) => `${textOf(r.providerKey)}|${textOf(r.model || "")}`;
1234
+ for (const r of sessionRows) {
1235
+ if (!r || !r.providerKey) continue;
1236
+ byKey.set(keyOf(r), { providerKey: textOf(r.providerKey), model: textOf(r.model || ""), session: r, historical: null });
1237
+ }
1238
+ for (const r of histRows) {
1239
+ if (!r || !r.providerKey) continue;
1240
+ const k = keyOf(r);
1241
+ const existing = byKey.get(k) || { providerKey: textOf(r.providerKey), model: textOf(r.model || ""), session: null, historical: null };
1242
+ existing.historical = r;
1243
+ byKey.set(k, existing);
1244
+ }
1245
+
1246
+ const rows = Array.from(byKey.values()).sort((a, b) => {
1247
+ const ak = `${a.providerKey}.${a.model}`;
1248
+ const bk = `${b.providerKey}.${b.model}`;
1249
+ return ak.localeCompare(bk);
1250
+ });
1251
+
1252
+ for (const row of rows) {
1253
+ const tr = document.createElement("tr");
1254
+ tr.appendChild(createCell("td", row.providerKey, "mono truncate", { title: true }));
1255
+ tr.appendChild(createCell("td", row.model || "—", "mono truncate", { title: true }));
1256
+
1257
+ const s = row.session;
1258
+ const sReqErr = s ? `${formatInt(s.requestCount)} / ${formatInt(s.errorCount)}` : "—";
1259
+ const sTok = s ? `${formatInt(s.totalPromptTokens)}/${formatInt(s.totalCompletionTokens)}/${formatInt(s.totalOutputTokens)}` : "—";
1260
+ tr.appendChild(createCell("td", sReqErr, "mono"));
1261
+ tr.appendChild(createCell("td", sTok, "mono"));
1262
+
1263
+ const h = row.historical;
1264
+ const hReqErr = h ? `${formatInt(h.requestCount)} / ${formatInt(h.errorCount)}` : "—";
1265
+ const hTok = h ? `${formatInt(h.totalPromptTokens)}/${formatInt(h.totalCompletionTokens)}/${formatInt(h.totalOutputTokens)}` : "—";
1266
+ tr.appendChild(createCell("td", hReqErr, "mono"));
1267
+ tr.appendChild(createCell("td", hTok, "mono"));
1268
+
1269
+ body.appendChild(tr);
1270
+ }
1271
+ } catch (e) {
1272
+ body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
1273
+ }
1274
+ }
1275
+
1276
+ async function testProviderFromPool(providerId) {
1277
+ setLog("providerOpLog", "");
1278
+ try {
1279
+ const detail = await apiFetch(`/config/providers/${encodeURIComponent(providerId)}`);
1280
+ const provider = detail && detail.provider ? detail.provider : null;
1281
+ const models = provider && provider.models && typeof provider.models === "object" ? Object.keys(provider.models) : [];
1282
+ if (!models.length) {
1283
+ throw new Error("No models configured for this provider");
1284
+ }
1285
+ const modelId = models[0];
1286
+ const directModel = `${providerId}.${modelId}`;
1287
+ const payload = { model: directModel, input: [{ role: "user", content: "ping" }], stream: false };
1288
+ const headers = new Headers({ "content-type": "application/json" });
1289
+ const apiKey = getApiKey();
1290
+ if (apiKey) headers.set("x-api-key", apiKey);
1291
+ const started = Date.now();
1292
+ const resp = await fetch("/v1/responses", { method: "POST", headers, body: JSON.stringify(payload) });
1293
+ const text = await resp.text();
1294
+ const ms = Date.now() - started;
1295
+ if (!resp.ok) {
1296
+ throw new Error(`HTTP ${resp.status} (${ms}ms): ${text}`);
1297
+ }
1298
+ let json = null;
1299
+ try { json = text ? JSON.parse(text) : null; } catch { json = null; }
1300
+ const summary =
1301
+ json && typeof json.output_text === "string"
1302
+ ? json.output_text.slice(0, 200)
1303
+ : json && Array.isArray(json.output)
1304
+ ? "(output items=" + json.output.length + ")"
1305
+ : "(ok)";
1306
+ setLog("providerOpLog", `Test OK (${ms}ms) model=${directModel}\n${summary}`);
1307
+ } catch (e) {
1308
+ setLog("providerOpLog", `Test failed: ${e && e.message ? e.message : e}`);
1309
+ }
1310
+ }
1311
+
1312
+ async function quotaAction(kind, providerKey) {
1313
+ setLog("quotaOpLog", "");
1314
+ if (!providerKey) return;
1315
+ try {
1316
+ if (kind === "recover") {
1317
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/recover`, { method: "POST" });
1318
+ await refreshQuota();
1319
+ return;
1320
+ }
1321
+ if (kind === "reset") {
1322
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/reset`, { method: "POST" });
1323
+ await refreshQuota();
1324
+ return;
1325
+ }
1326
+ if (kind === "disable") {
1327
+ const minutesRaw = prompt("Disable duration (minutes)", "60");
1328
+ if (!minutesRaw) return;
1329
+ const minutes = Number.parseFloat(minutesRaw);
1330
+ if (!Number.isFinite(minutes) || minutes <= 0) {
1331
+ throw new Error("Invalid minutes");
1332
+ }
1333
+ const modeRaw = prompt("Mode: cooldown or blacklist", "cooldown");
1334
+ const mode = (modeRaw || "cooldown").trim().toLowerCase() === "blacklist" ? "blacklist" : "cooldown";
1335
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/disable`, {
1336
+ method: "POST",
1337
+ body: JSON.stringify({ mode, durationMinutes: minutes })
1338
+ });
1339
+ await refreshQuota();
1340
+ return;
1341
+ }
1342
+ } catch (e) {
1343
+ setLog("quotaOpLog", `Action failed: ${e && e.message ? e.message : e}`);
1344
+ }
1345
+ }
1346
+
1347
+ async function resetQuotaModule() {
1348
+ setLog("quotaOpLog", "");
1349
+ if (!confirm("Reset provider-quota module now? This clears cooldown/blacklist state.")) return;
1350
+ try {
1351
+ const out = await apiFetch("/daemon/modules/provider-quota/reset", { method: "POST" });
1352
+ setLog("quotaOpLog", `OK. resetAt=${out.resetAt || "—"}`);
1353
+ await refreshQuota();
1354
+ } catch (e) {
1355
+ setLog("quotaOpLog", `Reset failed: ${e.message}`);
888
1356
  }
889
1357
  }
890
1358
 
@@ -897,11 +1365,19 @@
897
1365
  const value = ($("apiKeyInput").value || "").trim();
898
1366
  setApiKey(value);
899
1367
  $("apiKeyHint").textContent = value ? "saved (session only)" : "";
1368
+ Promise.resolve()
1369
+ .then(refreshStatus)
1370
+ .then(() => selectTab(getActiveTab()))
1371
+ .catch(() => {});
900
1372
  });
901
1373
  $("clearApiKeyBtn").addEventListener("click", () => {
902
1374
  setApiKey("");
903
1375
  $("apiKeyInput").value = "";
904
1376
  $("apiKeyHint").textContent = "";
1377
+ Promise.resolve()
1378
+ .then(refreshStatus)
1379
+ .then(() => selectTab(getActiveTab()))
1380
+ .catch(() => {});
905
1381
  });
906
1382
 
907
1383
  $("restartRuntimeBtn").addEventListener("click", async () => {
@@ -914,6 +1390,7 @@
914
1390
  await refreshStatus();
915
1391
  await refreshProviders();
916
1392
  await refreshCredentials();
1393
+ await refreshQuota();
917
1394
  await refreshRuntimes();
918
1395
  } catch (e) {
919
1396
  setLog("providerOpLog", `Restart failed: ${e.message}`);
@@ -921,6 +1398,7 @@
921
1398
  });
922
1399
 
923
1400
  $("refreshProvidersBtn").addEventListener("click", refreshProviders);
1401
+ $("refreshTokensBtn").addEventListener("click", refreshTokens);
924
1402
  $("newProviderBtn").addEventListener("click", () => {
925
1403
  $("providerEditorTitle").textContent = "Provider editor (new)";
926
1404
  $("providerIdInput").value = "";
@@ -932,6 +1410,10 @@
932
1410
  if (!btn) return;
933
1411
  const id = btn.getAttribute("data-id");
934
1412
  const action = btn.getAttribute("data-action");
1413
+ if (action === "test" && id) {
1414
+ await testProviderFromPool(id);
1415
+ return;
1416
+ }
935
1417
  if (action === "edit") await loadProvider(id);
936
1418
  if (action === "delete") await deleteProvider(id);
937
1419
  });
@@ -954,6 +1436,18 @@
954
1436
  $("saveOauthBrowserBtn").addEventListener("click", saveSettings);
955
1437
  $("oauthAuthorizeBtn").addEventListener("click", authorizeOauth);
956
1438
 
1439
+ $("refreshQuotaBtn").addEventListener("click", refreshQuota);
1440
+ $("quotaTbody").addEventListener("click", (ev) => {
1441
+ const el = ev.target;
1442
+ if (!el || el.tagName !== "BUTTON") return;
1443
+ const action = el.getAttribute("data-action");
1444
+ const key = el.getAttribute("data-key");
1445
+ if (action === "quota-recover") void quotaAction("recover", key);
1446
+ else if (action === "quota-reset") void quotaAction("reset", key);
1447
+ else if (action === "quota-disable") void quotaAction("disable", key);
1448
+ });
1449
+ $("resetQuotaBtn").addEventListener("click", resetQuotaModule);
1450
+
957
1451
  $("loadRoutingBtn").addEventListener("click", loadRouting);
958
1452
  $("saveRoutingBtn").addEventListener("click", saveRouting);
959
1453
  $("refreshRuntimesBtn").addEventListener("click", refreshRuntimes);
@@ -968,6 +1462,8 @@
968
1462
  await refreshStatus();
969
1463
  await refreshProviders();
970
1464
  await refreshCredentials();
1465
+ await refreshQuota();
1466
+ await refreshRuntimes();
971
1467
  await loadSettings();
972
1468
  })();
973
1469
  </script>