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