wyxrouter 0.4.71
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/CHANGELOG.md +222 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/README.zh-CN.md +1311 -0
- package/assets/pixel-router-2d.png +0 -0
- package/cli/LICENSE +42 -0
- package/cli/README.md +125 -0
- package/cli/app/package.json +58 -0
- package/cli/cli.js +806 -0
- package/cli/package.json +48 -0
- package/i18n/README.ja-JP.md +1209 -0
- package/i18n/README.ru.md +1311 -0
- package/i18n/README.vi.md +1310 -0
- package/i18n/README.zh-CN.md +1306 -0
- package/images/9router.png +0 -0
- package/jsconfig.json +12 -0
- package/next.config.mjs +71 -0
- package/open-sse/config/appConstants.js +203 -0
- package/open-sse/config/codexInstructions.js +119 -0
- package/open-sse/config/constants.js +4 -0
- package/open-sse/config/defaultThinkingSignature.js +12 -0
- package/open-sse/config/errorConfig.js +85 -0
- package/open-sse/config/googleTtsLanguages.js +62 -0
- package/open-sse/config/kiroConstants.js +322 -0
- package/open-sse/config/models.js +13 -0
- package/open-sse/config/ollamaModels.js +19 -0
- package/open-sse/config/providerModels.js +944 -0
- package/open-sse/config/providers.js +458 -0
- package/open-sse/config/runtimeConfig.js +72 -0
- package/open-sse/config/ttsModels.js +129 -0
- package/open-sse/executors/antigravity.js +504 -0
- package/open-sse/executors/azure.js +57 -0
- package/open-sse/executors/base.js +185 -0
- package/open-sse/executors/codex.js +469 -0
- package/open-sse/executors/commandcode.js +88 -0
- package/open-sse/executors/cursor.js +795 -0
- package/open-sse/executors/default.js +497 -0
- package/open-sse/executors/gemini-cli.js +89 -0
- package/open-sse/executors/github.js +379 -0
- package/open-sse/executors/grok-web.js +345 -0
- package/open-sse/executors/iflow.js +108 -0
- package/open-sse/executors/index.js +75 -0
- package/open-sse/executors/kiro.js +508 -0
- package/open-sse/executors/ollama-local.js +14 -0
- package/open-sse/executors/opencode-go.js +41 -0
- package/open-sse/executors/opencode.js +32 -0
- package/open-sse/executors/perplexity-web.js +507 -0
- package/open-sse/executors/qoder.js +450 -0
- package/open-sse/executors/qwen.js +129 -0
- package/open-sse/executors/vertex.js +131 -0
- package/open-sse/executors/xiaomi-tokenplan.js +19 -0
- package/open-sse/handlers/chatCore/nonStreamingHandler.js +230 -0
- package/open-sse/handlers/chatCore/requestDetail.js +102 -0
- package/open-sse/handlers/chatCore/sseToJsonHandler.js +231 -0
- package/open-sse/handlers/chatCore/streamingHandler.js +103 -0
- package/open-sse/handlers/chatCore.js +287 -0
- package/open-sse/handlers/embeddingProviders/_base.js +4 -0
- package/open-sse/handlers/embeddingProviders/gemini.js +54 -0
- package/open-sse/handlers/embeddingProviders/index.js +23 -0
- package/open-sse/handlers/embeddingProviders/openai.js +39 -0
- package/open-sse/handlers/embeddingProviders/openaiCompatNode.js +13 -0
- package/open-sse/handlers/embeddingsCore.js +126 -0
- package/open-sse/handlers/fetch/index.js +237 -0
- package/open-sse/handlers/imageGenerationCore.js +189 -0
- package/open-sse/handlers/imageProviders/_base.js +31 -0
- package/open-sse/handlers/imageProviders/blackForestLabs.js +43 -0
- package/open-sse/handlers/imageProviders/cloudflareAi.js +178 -0
- package/open-sse/handlers/imageProviders/codex.js +198 -0
- package/open-sse/handlers/imageProviders/comfyui.js +8 -0
- package/open-sse/handlers/imageProviders/falAi.js +41 -0
- package/open-sse/handlers/imageProviders/gemini.js +25 -0
- package/open-sse/handlers/imageProviders/huggingface.js +22 -0
- package/open-sse/handlers/imageProviders/index.js +40 -0
- package/open-sse/handlers/imageProviders/nanobanana.js +58 -0
- package/open-sse/handlers/imageProviders/openai.js +40 -0
- package/open-sse/handlers/imageProviders/runwayml.js +47 -0
- package/open-sse/handlers/imageProviders/sdwebui.js +17 -0
- package/open-sse/handlers/imageProviders/stabilityAi.js +34 -0
- package/open-sse/handlers/responsesHandler.js +103 -0
- package/open-sse/handlers/search/callers.js +371 -0
- package/open-sse/handlers/search/chatSearch.js +409 -0
- package/open-sse/handlers/search/index.js +201 -0
- package/open-sse/handlers/search/normalizers.js +223 -0
- package/open-sse/handlers/sttCore.js +194 -0
- package/open-sse/handlers/ttsCore.js +74 -0
- package/open-sse/handlers/ttsProviders/_base.js +39 -0
- package/open-sse/handlers/ttsProviders/edgeTts.js +89 -0
- package/open-sse/handlers/ttsProviders/elevenlabs.js +48 -0
- package/open-sse/handlers/ttsProviders/gemini.js +117 -0
- package/open-sse/handlers/ttsProviders/genericFormats.js +169 -0
- package/open-sse/handlers/ttsProviders/googleTts.js +54 -0
- package/open-sse/handlers/ttsProviders/index.js +50 -0
- package/open-sse/handlers/ttsProviders/localDevice.js +87 -0
- package/open-sse/handlers/ttsProviders/minimax.js +59 -0
- package/open-sse/handlers/ttsProviders/openai.js +30 -0
- package/open-sse/handlers/ttsProviders/openrouter.js +70 -0
- package/open-sse/index.js +82 -0
- package/open-sse/rtk/applyFilter.js +15 -0
- package/open-sse/rtk/autodetect.js +111 -0
- package/open-sse/rtk/caveman.js +100 -0
- package/open-sse/rtk/cavemanPrompts.js +78 -0
- package/open-sse/rtk/constants.js +55 -0
- package/open-sse/rtk/filters/buildOutput.js +127 -0
- package/open-sse/rtk/filters/dedupLog.js +44 -0
- package/open-sse/rtk/filters/find.js +49 -0
- package/open-sse/rtk/filters/gitDiff.js +92 -0
- package/open-sse/rtk/filters/gitStatus.js +117 -0
- package/open-sse/rtk/filters/grep.js +48 -0
- package/open-sse/rtk/filters/ls.js +79 -0
- package/open-sse/rtk/filters/readNumbered.js +27 -0
- package/open-sse/rtk/filters/searchList.js +52 -0
- package/open-sse/rtk/filters/smartTruncate.js +15 -0
- package/open-sse/rtk/filters/tree.js +32 -0
- package/open-sse/rtk/index.js +155 -0
- package/open-sse/rtk/registry.js +38 -0
- package/open-sse/services/accountFallback.js +238 -0
- package/open-sse/services/combo.js +198 -0
- package/open-sse/services/compact.js +71 -0
- package/open-sse/services/kiroModels.js +332 -0
- package/open-sse/services/model.js +261 -0
- package/open-sse/services/oauthCredentialManager.js +151 -0
- package/open-sse/services/projectId.js +306 -0
- package/open-sse/services/provider.js +356 -0
- package/open-sse/services/qoderModels.js +214 -0
- package/open-sse/services/tokenRefresh.js +939 -0
- package/open-sse/services/usage.js +1496 -0
- package/open-sse/transformer/responsesTransformer.js +439 -0
- package/open-sse/transformer/streamToJsonConverter.js +103 -0
- package/open-sse/translator/formats.js +36 -0
- package/open-sse/translator/helpers/claudeHelper.js +216 -0
- package/open-sse/translator/helpers/geminiHelper.js +372 -0
- package/open-sse/translator/helpers/imageHelper.js +34 -0
- package/open-sse/translator/helpers/maxTokensHelper.js +27 -0
- package/open-sse/translator/helpers/openaiHelper.js +130 -0
- package/open-sse/translator/helpers/responsesApiHelper.js +139 -0
- package/open-sse/translator/helpers/toolCallHelper.js +148 -0
- package/open-sse/translator/index.js +251 -0
- package/open-sse/translator/request/antigravity-to-openai.js +229 -0
- package/open-sse/translator/request/claude-to-openai.js +232 -0
- package/open-sse/translator/request/gemini-to-openai.js +147 -0
- package/open-sse/translator/request/openai-responses.js +318 -0
- package/open-sse/translator/request/openai-to-claude.js +401 -0
- package/open-sse/translator/request/openai-to-commandcode.js +170 -0
- package/open-sse/translator/request/openai-to-cursor.js +183 -0
- package/open-sse/translator/request/openai-to-gemini.js +470 -0
- package/open-sse/translator/request/openai-to-kiro.js +629 -0
- package/open-sse/translator/request/openai-to-kiro.old.js +278 -0
- package/open-sse/translator/request/openai-to-ollama.js +192 -0
- package/open-sse/translator/request/openai-to-vertex.js +42 -0
- package/open-sse/translator/response/claude-to-openai.js +206 -0
- package/open-sse/translator/response/commandcode-to-openai.js +197 -0
- package/open-sse/translator/response/cursor-to-openai.js +30 -0
- package/open-sse/translator/response/gemini-to-openai.js +245 -0
- package/open-sse/translator/response/kiro-to-openai.js +195 -0
- package/open-sse/translator/response/ollama-to-openai.js +152 -0
- package/open-sse/translator/response/openai-responses.js +590 -0
- package/open-sse/translator/response/openai-to-antigravity.js +122 -0
- package/open-sse/translator/response/openai-to-claude.js +266 -0
- package/open-sse/utils/bypassHandler.js +298 -0
- package/open-sse/utils/claudeCloaking.js +155 -0
- package/open-sse/utils/claudeHeaderCache.js +70 -0
- package/open-sse/utils/clientDetector.js +63 -0
- package/open-sse/utils/cursorChecksum.js +149 -0
- package/open-sse/utils/cursorProtobuf.js +904 -0
- package/open-sse/utils/debugLog.js +14 -0
- package/open-sse/utils/error.js +147 -0
- package/open-sse/utils/ollamaTransform.js +85 -0
- package/open-sse/utils/proxyFetch.js +368 -0
- package/open-sse/utils/reasoningContentInjector.js +79 -0
- package/open-sse/utils/requestLogger.js +260 -0
- package/open-sse/utils/responsesStreamHelpers.js +49 -0
- package/open-sse/utils/sessionManager.js +82 -0
- package/open-sse/utils/stream.js +462 -0
- package/open-sse/utils/streamHandler.js +250 -0
- package/open-sse/utils/streamHelpers.js +122 -0
- package/open-sse/utils/toolDeduper.js +49 -0
- package/open-sse/utils/usageTracking.js +347 -0
- package/package.json +100 -0
- package/postcss.config.mjs +12 -0
- package/public/favicon.svg +11 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/i18n/literals/ar.json +195 -0
- package/public/i18n/literals/bn.json +195 -0
- package/public/i18n/literals/cs.json +195 -0
- package/public/i18n/literals/da.json +195 -0
- package/public/i18n/literals/de.json +195 -0
- package/public/i18n/literals/el.json +195 -0
- package/public/i18n/literals/es.json +195 -0
- package/public/i18n/literals/fi.json +195 -0
- package/public/i18n/literals/fr.json +195 -0
- package/public/i18n/literals/he.json +195 -0
- package/public/i18n/literals/hi.json +195 -0
- package/public/i18n/literals/hu.json +195 -0
- package/public/i18n/literals/id.json +195 -0
- package/public/i18n/literals/it.json +195 -0
- package/public/i18n/literals/ja.json +195 -0
- package/public/i18n/literals/ko.json +195 -0
- package/public/i18n/literals/nl.json +195 -0
- package/public/i18n/literals/no.json +195 -0
- package/public/i18n/literals/pl.json +195 -0
- package/public/i18n/literals/pt-BR.json +195 -0
- package/public/i18n/literals/pt-PT.json +195 -0
- package/public/i18n/literals/ro.json +195 -0
- package/public/i18n/literals/ru.json +195 -0
- package/public/i18n/literals/sv.json +195 -0
- package/public/i18n/literals/th.json +195 -0
- package/public/i18n/literals/tl.json +195 -0
- package/public/i18n/literals/tr.json +195 -0
- package/public/i18n/literals/uk.json +195 -0
- package/public/i18n/literals/ur.json +195 -0
- package/public/i18n/literals/vi.json +195 -0
- package/public/i18n/literals/zh-CN.json +772 -0
- package/public/i18n/literals/zh-TW.json +195 -0
- package/public/icons/discord.svg +4 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/next.svg +1 -0
- package/public/providers/alicode-intl.png +0 -0
- package/public/providers/alicode.png +0 -0
- package/public/providers/amp.png +0 -0
- package/public/providers/anthropic-m.png +0 -0
- package/public/providers/anthropic.png +0 -0
- package/public/providers/antigravity.png +0 -0
- package/public/providers/assemblyai.png +0 -0
- package/public/providers/aws-polly.png +0 -0
- package/public/providers/azure.png +0 -0
- package/public/providers/black-forest-labs.png +0 -0
- package/public/providers/blackbox.png +0 -0
- package/public/providers/brave-search.png +0 -0
- package/public/providers/byteplus.png +0 -0
- package/public/providers/cartesia.png +0 -0
- package/public/providers/cerebras.png +0 -0
- package/public/providers/chutes.png +0 -0
- package/public/providers/claude.png +0 -0
- package/public/providers/cline.png +0 -0
- package/public/providers/cloudflare-ai.png +0 -0
- package/public/providers/codebuddy.svg +37 -0
- package/public/providers/codex.png +0 -0
- package/public/providers/cohere.png +0 -0
- package/public/providers/comfyui.png +0 -0
- package/public/providers/commandcode.png +0 -0
- package/public/providers/continue.png +0 -0
- package/public/providers/copilot.png +0 -0
- package/public/providers/coqui.png +0 -0
- package/public/providers/cursor.png +0 -0
- package/public/providers/deepgram.png +0 -0
- package/public/providers/deepseek-tui.png +0 -0
- package/public/providers/deepseek.png +0 -0
- package/public/providers/droid.png +0 -0
- package/public/providers/edge-tts.png +0 -0
- package/public/providers/elevenlabs.png +0 -0
- package/public/providers/exa.png +0 -0
- package/public/providers/fal-ai.png +0 -0
- package/public/providers/firecrawl.png +0 -0
- package/public/providers/fireworks.png +0 -0
- package/public/providers/gemini-cli.png +0 -0
- package/public/providers/gemini.png +0 -0
- package/public/providers/github.png +0 -0
- package/public/providers/glm-cn.png +0 -0
- package/public/providers/glm.png +0 -0
- package/public/providers/google-pse.png +0 -0
- package/public/providers/google-tts.png +0 -0
- package/public/providers/grok-web.png +0 -0
- package/public/providers/groq.png +0 -0
- package/public/providers/hermes.png +0 -0
- package/public/providers/huggingface.png +0 -0
- package/public/providers/hyperbolic.png +0 -0
- package/public/providers/iflow.png +0 -0
- package/public/providers/inworld.png +0 -0
- package/public/providers/jcode.png +0 -0
- package/public/providers/jina-ai.png +0 -0
- package/public/providers/jina-reader.png +0 -0
- package/public/providers/kilocode.png +0 -0
- package/public/providers/kimi-coding.png +0 -0
- package/public/providers/kimi.png +0 -0
- package/public/providers/kiro.png +0 -0
- package/public/providers/linkup.png +0 -0
- package/public/providers/local-device.png +0 -0
- package/public/providers/minimax-cn.png +0 -0
- package/public/providers/minimax.png +0 -0
- package/public/providers/mistral.png +0 -0
- package/public/providers/nanobanana.png +0 -0
- package/public/providers/nebius.png +0 -0
- package/public/providers/nvidia.png +0 -0
- package/public/providers/oai-cc.png +0 -0
- package/public/providers/oai-r.png +0 -0
- package/public/providers/ollama-local.png +0 -0
- package/public/providers/ollama.png +0 -0
- package/public/providers/openai.png +0 -0
- package/public/providers/openclaw.png +0 -0
- package/public/providers/opencode-go.png +0 -0
- package/public/providers/opencode.png +0 -0
- package/public/providers/openrouter.png +0 -0
- package/public/providers/perplexity-web.png +0 -0
- package/public/providers/perplexity.png +0 -0
- package/public/providers/playht.png +0 -0
- package/public/providers/qoder.png +0 -0
- package/public/providers/qwen.png +0 -0
- package/public/providers/recraft.png +0 -0
- package/public/providers/roo.png +0 -0
- package/public/providers/runwayml.png +0 -0
- package/public/providers/sdwebui.png +0 -0
- package/public/providers/searchapi.png +0 -0
- package/public/providers/searxng.png +0 -0
- package/public/providers/serper.png +0 -0
- package/public/providers/siliconflow.png +0 -0
- package/public/providers/stability-ai.png +0 -0
- package/public/providers/tavily.png +0 -0
- package/public/providers/together.png +0 -0
- package/public/providers/topaz.png +0 -0
- package/public/providers/tortoise.png +0 -0
- package/public/providers/vertex-partner.png +0 -0
- package/public/providers/vertex.png +0 -0
- package/public/providers/volcengine-ark.png +0 -0
- package/public/providers/voyage-ai.png +0 -0
- package/public/providers/xai.png +0 -0
- package/public/providers/xiaomi-mimo.png +0 -0
- package/public/providers/xiaomi-tokenplan.png +0 -0
- package/public/providers/youcom.png +0 -0
- package/public/sw.js +22 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/compact-request-details.mjs +71 -0
- package/scripts/import-codex-gptjson.mjs +342 -0
- package/scripts/start-standalone.mjs +25 -0
- package/scripts/translate-readme.js +201 -0
- package/src/app/(dashboard)/dashboard/automation/page.js +294 -0
- package/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js +967 -0
- package/src/app/(dashboard)/dashboard/basic-chat/page.js +5 -0
- package/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js +66 -0
- package/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js +173 -0
- package/src/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +11 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js +481 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js +66 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js +174 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +390 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.js +301 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +458 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js +323 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js +640 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/DeepSeekTuiToolCard.js +338 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js +271 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js +410 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/EndpointPresetControl.js +128 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js +317 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/JcodeToolCard.js +380 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.js +275 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/MitmLinkCard.js +40 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +329 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js +318 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js +388 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js +500 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ToolSummaryCard.js +39 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointMatch.js +13 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/index.js +19 -0
- package/src/app/(dashboard)/dashboard/cli-tools/page.js +7 -0
- package/src/app/(dashboard)/dashboard/combos/page.js +612 -0
- package/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js +91 -0
- package/src/app/(dashboard)/dashboard/console-log/page.js +8 -0
- package/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +1555 -0
- package/src/app/(dashboard)/dashboard/endpoint/page.js +7 -0
- package/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js +1903 -0
- package/src/app/(dashboard)/dashboard/media-providers/[kind]/page.js +289 -0
- package/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +410 -0
- package/src/app/(dashboard)/dashboard/media-providers/web/page.js +209 -0
- package/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +117 -0
- package/src/app/(dashboard)/dashboard/mitm/page.js +5 -0
- package/src/app/(dashboard)/dashboard/page.js +7 -0
- package/src/app/(dashboard)/dashboard/profile/page.js +1140 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +389 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js +125 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/CompatibleModelsSection.js +250 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js +299 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/CooldownTimer.js +42 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/EditCompatibleNodeModal.js +161 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js +95 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/PassthroughModelsSection.js +183 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/page.js +2053 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/page.new.js +1724 -0
- package/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js +497 -0
- package/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js +185 -0
- package/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js +294 -0
- package/src/app/(dashboard)/dashboard/providers/new/page.js +220 -0
- package/src/app/(dashboard)/dashboard/providers/page.js +1365 -0
- package/src/app/(dashboard)/dashboard/proxy-pools/page.js +1092 -0
- package/src/app/(dashboard)/dashboard/quota/page.js +11 -0
- package/src/app/(dashboard)/dashboard/skills/page.js +112 -0
- package/src/app/(dashboard)/dashboard/translator/page.js +303 -0
- package/src/app/(dashboard)/dashboard/usage/components/OverviewCards.js +35 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js +185 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js +127 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js +259 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +1394 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +244 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js +327 -0
- package/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js +433 -0
- package/src/app/(dashboard)/dashboard/usage/components/UsageChart.js +141 -0
- package/src/app/(dashboard)/dashboard/usage/components/UsageTable.js +247 -0
- package/src/app/(dashboard)/dashboard/usage/page.js +75 -0
- package/src/app/(dashboard)/layout.js +6 -0
- package/src/app/api/auth/login/route.js +76 -0
- package/src/app/api/auth/logout/route.js +12 -0
- package/src/app/api/auth/oidc/callback/route.js +87 -0
- package/src/app/api/auth/oidc/start/route.js +52 -0
- package/src/app/api/auth/oidc/test/route.js +84 -0
- package/src/app/api/auth/status/route.js +45 -0
- package/src/app/api/cli-tools/all-statuses/route.js +46 -0
- package/src/app/api/cli-tools/antigravity-mitm/alias/route.js +53 -0
- package/src/app/api/cli-tools/antigravity-mitm/route.js +202 -0
- package/src/app/api/cli-tools/claude-settings/route.js +203 -0
- package/src/app/api/cli-tools/cline-settings/route.js +133 -0
- package/src/app/api/cli-tools/codex-gateway/accounts/route.js +16 -0
- package/src/app/api/cli-tools/codex-settings/route.js +239 -0
- package/src/app/api/cli-tools/copilot-settings/route.js +148 -0
- package/src/app/api/cli-tools/cowork-mcp-registry/route.js +77 -0
- package/src/app/api/cli-tools/cowork-mcp-tools/route.js +95 -0
- package/src/app/api/cli-tools/cowork-settings/route.js +412 -0
- package/src/app/api/cli-tools/deepseek-tui-settings/route.js +164 -0
- package/src/app/api/cli-tools/droid-settings/route.js +213 -0
- package/src/app/api/cli-tools/hermes-settings/route.js +175 -0
- package/src/app/api/cli-tools/jcode-settings/route.js +216 -0
- package/src/app/api/cli-tools/kilo-settings/route.js +131 -0
- package/src/app/api/cli-tools/openclaw-settings/route.js +292 -0
- package/src/app/api/cli-tools/opencode-settings/route.js +259 -0
- package/src/app/api/combos/[id]/route.js +81 -0
- package/src/app/api/combos/route.js +48 -0
- package/src/app/api/health/route.js +15 -0
- package/src/app/api/init/route.js +4 -0
- package/src/app/api/keys/[id]/route.js +58 -0
- package/src/app/api/keys/route.js +42 -0
- package/src/app/api/locale/route.js +30 -0
- package/src/app/api/mcp/[plugin]/message/route.js +21 -0
- package/src/app/api/mcp/[plugin]/sse/route.js +37 -0
- package/src/app/api/media-providers/tts/deepgram/voices/route.js +65 -0
- package/src/app/api/media-providers/tts/elevenlabs/voices/route.js +71 -0
- package/src/app/api/media-providers/tts/inworld/voices/route.js +61 -0
- package/src/app/api/media-providers/tts/minimax/voices/route.js +113 -0
- package/src/app/api/media-providers/tts/voices/route.js +99 -0
- package/src/app/api/models/alias/route.js +53 -0
- package/src/app/api/models/availability/route.js +103 -0
- package/src/app/api/models/custom/route.js +48 -0
- package/src/app/api/models/disabled/route.js +50 -0
- package/src/app/api/models/route.js +64 -0
- package/src/app/api/models/test/ping.js +191 -0
- package/src/app/api/models/test/route.js +14 -0
- package/src/app/api/oauth/[provider]/[action]/route.js +343 -0
- package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/cancel/route.js +19 -0
- package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/manual/[workerId]/route.js +30 -0
- package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/route.js +23 -0
- package/src/app/api/oauth/codebuddy/bulk-import/latest/route.js +25 -0
- package/src/app/api/oauth/codebuddy/bulk-import/route.js +49 -0
- package/src/app/api/oauth/codebuddy/quota-cookie/route.js +133 -0
- package/src/app/api/oauth/codex/import-token/route.js +96 -0
- package/src/app/api/oauth/cursor/auto-import/route.js +258 -0
- package/src/app/api/oauth/cursor/import/route.js +100 -0
- package/src/app/api/oauth/gitlab/pat/route.js +62 -0
- package/src/app/api/oauth/iflow/cookie/route.js +137 -0
- package/src/app/api/oauth/kiro/auto-import/route.js +85 -0
- package/src/app/api/oauth/kiro/bulk-import/[jobId]/cancel/route.js +18 -0
- package/src/app/api/oauth/kiro/bulk-import/[jobId]/manual/[workerId]/route.js +29 -0
- package/src/app/api/oauth/kiro/bulk-import/[jobId]/route.js +22 -0
- package/src/app/api/oauth/kiro/bulk-import/latest/route.js +25 -0
- package/src/app/api/oauth/kiro/bulk-import/route.js +49 -0
- package/src/app/api/oauth/kiro/import/route.js +110 -0
- package/src/app/api/oauth/kiro/social-authorize/route.js +27 -0
- package/src/app/api/oauth/kiro/social-exchange/route.js +41 -0
- package/src/app/api/pricing/route.js +134 -0
- package/src/app/api/provider-nodes/[id]/route.js +101 -0
- package/src/app/api/provider-nodes/route.js +104 -0
- package/src/app/api/provider-nodes/validate/route.js +201 -0
- package/src/app/api/providers/[id]/models/route.js +526 -0
- package/src/app/api/providers/[id]/route.js +189 -0
- package/src/app/api/providers/[id]/test/route.js +23 -0
- package/src/app/api/providers/[id]/test/testUtils.js +714 -0
- package/src/app/api/providers/[id]/test-models/route.js +66 -0
- package/src/app/api/providers/client/route.js +126 -0
- package/src/app/api/providers/kilo/free-models/route.js +55 -0
- package/src/app/api/providers/route.js +206 -0
- package/src/app/api/providers/suggested-models/filters.js +20 -0
- package/src/app/api/providers/suggested-models/route.js +32 -0
- package/src/app/api/providers/test-batch/route.js +131 -0
- package/src/app/api/providers/validate/route.js +637 -0
- package/src/app/api/proxy-pools/[id]/route.js +123 -0
- package/src/app/api/proxy-pools/[id]/test/route.js +70 -0
- package/src/app/api/proxy-pools/cloudflare-deploy/route.js +145 -0
- package/src/app/api/proxy-pools/deno-deploy/route.js +175 -0
- package/src/app/api/proxy-pools/route.js +93 -0
- package/src/app/api/proxy-pools/vercel-deploy/route.js +142 -0
- package/src/app/api/settings/database/route.js +36 -0
- package/src/app/api/settings/proxy-test/route.js +23 -0
- package/src/app/api/settings/require-login/route.js +15 -0
- package/src/app/api/settings/route.js +100 -0
- package/src/app/api/shutdown/route.js +24 -0
- package/src/app/api/tags/route.js +18 -0
- package/src/app/api/translator/console-logs/route.js +24 -0
- package/src/app/api/translator/console-logs/stream/route.js +79 -0
- package/src/app/api/translator/load/route.js +45 -0
- package/src/app/api/translator/save/route.js +44 -0
- package/src/app/api/translator/send/route.js +94 -0
- package/src/app/api/translator/translate/route.js +90 -0
- package/src/app/api/tunnel/disable/route.js +12 -0
- package/src/app/api/tunnel/enable/route.js +16 -0
- package/src/app/api/tunnel/status/route.js +13 -0
- package/src/app/api/tunnel/tailscale-check/route.js +50 -0
- package/src/app/api/tunnel/tailscale-disable/route.js +12 -0
- package/src/app/api/tunnel/tailscale-enable/route.js +12 -0
- package/src/app/api/tunnel/tailscale-install/route.js +72 -0
- package/src/app/api/usage/[connectionId]/route.js +188 -0
- package/src/app/api/usage/chart/route.js +21 -0
- package/src/app/api/usage/history/route.js +12 -0
- package/src/app/api/usage/logs/route.js +12 -0
- package/src/app/api/usage/providers/route.js +42 -0
- package/src/app/api/usage/request-details/route.js +57 -0
- package/src/app/api/usage/request-logs/route.js +13 -0
- package/src/app/api/usage/stats/route.js +23 -0
- package/src/app/api/usage/stream/route.js +79 -0
- package/src/app/api/v1/api/chat/route.js +37 -0
- package/src/app/api/v1/audio/speech/route.js +16 -0
- package/src/app/api/v1/audio/transcriptions/route.js +19 -0
- package/src/app/api/v1/audio/voices/route.js +68 -0
- package/src/app/api/v1/chat/completions/route.js +35 -0
- package/src/app/api/v1/embeddings/route.js +21 -0
- package/src/app/api/v1/images/generations/route.js +16 -0
- package/src/app/api/v1/messages/count_tokens/route.js +52 -0
- package/src/app/api/v1/messages/route.js +36 -0
- package/src/app/api/v1/models/[kind]/route.js +55 -0
- package/src/app/api/v1/models/info/route.js +110 -0
- package/src/app/api/v1/models/route.js +451 -0
- package/src/app/api/v1/responses/compact/route.js +37 -0
- package/src/app/api/v1/responses/route.js +30 -0
- package/src/app/api/v1/route.js +1 -0
- package/src/app/api/v1/search/route.js +21 -0
- package/src/app/api/v1/web/fetch/route.js +21 -0
- package/src/app/api/v1beta/models/[...path]/route.js +328 -0
- package/src/app/api/v1beta/models/route.js +44 -0
- package/src/app/api/version/route.js +45 -0
- package/src/app/api/version/shutdown/route.js +15 -0
- package/src/app/api/version/update/route.js +21 -0
- package/src/app/callback/page.js +148 -0
- package/src/app/dashboard/settings/pricing/page.js +173 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +496 -0
- package/src/app/landing/components/AnimatedBackground.js +57 -0
- package/src/app/landing/components/Features.js +133 -0
- package/src/app/landing/components/FlowAnimation.js +175 -0
- package/src/app/landing/components/Footer.js +61 -0
- package/src/app/landing/components/GetStarted.js +97 -0
- package/src/app/landing/components/HeroSection.js +47 -0
- package/src/app/landing/components/HowItWorks.js +66 -0
- package/src/app/landing/components/Navigation.js +72 -0
- package/src/app/landing/page.js +106 -0
- package/src/app/layout.js +49 -0
- package/src/app/login/page.js +197 -0
- package/src/app/manifest.js +30 -0
- package/src/app/page.js +5 -0
- package/src/dashboardGuard.js +242 -0
- package/src/i18n/RuntimeI18nProvider.js +27 -0
- package/src/i18n/config.js +146 -0
- package/src/i18n/runtime.js +162 -0
- package/src/lib/appUpdater.js +200 -0
- package/src/lib/auth/dashboardSession.js +68 -0
- package/src/lib/auth/loginLimiter.js +52 -0
- package/src/lib/auth/oidc.js +234 -0
- package/src/lib/consoleLogBuffer.js +79 -0
- package/src/lib/dataDir.js +29 -0
- package/src/lib/db/adapters/betterSqliteAdapter.js +55 -0
- package/src/lib/db/adapters/bunSqliteAdapter.js +63 -0
- package/src/lib/db/adapters/nodeSqliteAdapter.js +84 -0
- package/src/lib/db/adapters/sqljsAdapter.js +115 -0
- package/src/lib/db/backup.js +35 -0
- package/src/lib/db/driver.js +85 -0
- package/src/lib/db/helpers/jsonCol.js +9 -0
- package/src/lib/db/helpers/kvStore.js +39 -0
- package/src/lib/db/helpers/metaStore.js +22 -0
- package/src/lib/db/index.js +171 -0
- package/src/lib/db/migrate.js +286 -0
- package/src/lib/db/migrations/001-initial.js +14 -0
- package/src/lib/db/migrations/index.js +10 -0
- package/src/lib/db/paths.js +18 -0
- package/src/lib/db/repos/aliasRepo.js +62 -0
- package/src/lib/db/repos/apiKeysRepo.js +75 -0
- package/src/lib/db/repos/combosRepo.js +73 -0
- package/src/lib/db/repos/connectionsRepo.js +226 -0
- package/src/lib/db/repos/disabledModelsRepo.js +56 -0
- package/src/lib/db/repos/nodesRepo.js +95 -0
- package/src/lib/db/repos/pricingRepo.js +108 -0
- package/src/lib/db/repos/proxyPoolsRepo.js +103 -0
- package/src/lib/db/repos/requestDetailsRepo.js +259 -0
- package/src/lib/db/repos/settingsRepo.js +104 -0
- package/src/lib/db/repos/usageRepo.js +731 -0
- package/src/lib/db/schema.js +157 -0
- package/src/lib/db/version.js +21 -0
- package/src/lib/disabledModelsDb.js +4 -0
- package/src/lib/localDb.js +21 -0
- package/src/lib/mcp/stdioSseBridge.js +198 -0
- package/src/lib/mitmAliasCache.js +46 -0
- package/src/lib/network/connectionProxy.js +160 -0
- package/src/lib/network/initOutboundProxy.js +25 -0
- package/src/lib/network/outboundProxy.js +68 -0
- package/src/lib/network/proxyTest.js +91 -0
- package/src/lib/oauth/constants/oauth.js +284 -0
- package/src/lib/oauth/constants/xai.js +61 -0
- package/src/lib/oauth/providers.js +1506 -0
- package/src/lib/oauth/services/antigravity.js +321 -0
- package/src/lib/oauth/services/claude.js +136 -0
- package/src/lib/oauth/services/codebuddyBulkImportManager.js +454 -0
- package/src/lib/oauth/services/codex.js +144 -0
- package/src/lib/oauth/services/cursor.js +179 -0
- package/src/lib/oauth/services/gemini.js +240 -0
- package/src/lib/oauth/services/github.js +225 -0
- package/src/lib/oauth/services/iflow.js +202 -0
- package/src/lib/oauth/services/index.js +17 -0
- package/src/lib/oauth/services/kiro.js +334 -0
- package/src/lib/oauth/services/kiroBulkImportManager.js +778 -0
- package/src/lib/oauth/services/kiroConnections.js +93 -0
- package/src/lib/oauth/services/kiroGoogleAutomation.js +1136 -0
- package/src/lib/oauth/services/oauth.js +157 -0
- package/src/lib/oauth/services/openai.js +123 -0
- package/src/lib/oauth/services/qoder.js +216 -0
- package/src/lib/oauth/services/qwen.js +170 -0
- package/src/lib/oauth/services/xai.js +238 -0
- package/src/lib/oauth/utils/banner.js +63 -0
- package/src/lib/oauth/utils/pkce.js +39 -0
- package/src/lib/oauth/utils/server.js +415 -0
- package/src/lib/oauth/utils/ui.js +48 -0
- package/src/lib/providerNormalization.js +45 -0
- package/src/lib/qoder/constants.js +64 -0
- package/src/lib/qoder/cosy.js +175 -0
- package/src/lib/qoder/encoding.js +55 -0
- package/src/lib/requestDetailsDb.js +4 -0
- package/src/lib/tunnel/cloudflare/cloudflared.js +449 -0
- package/src/lib/tunnel/cloudflare/config.js +9 -0
- package/src/lib/tunnel/cloudflare/healthCheck.js +29 -0
- package/src/lib/tunnel/cloudflare/manager.js +151 -0
- package/src/lib/tunnel/cloudflare/pid.js +23 -0
- package/src/lib/tunnel/index.js +48 -0
- package/src/lib/tunnel/shared/dnsResolver.js +17 -0
- package/src/lib/tunnel/shared/internetCheck.js +26 -0
- package/src/lib/tunnel/shared/state.js +41 -0
- package/src/lib/tunnel/shared/watchdogConfig.js +8 -0
- package/src/lib/tunnel/tailscale/config.js +7 -0
- package/src/lib/tunnel/tailscale/healthCheck.js +29 -0
- package/src/lib/tunnel/tailscale/manager.js +129 -0
- package/src/lib/tunnel/tailscale/tailscale.js +790 -0
- package/src/lib/updater/updater.js +235 -0
- package/src/lib/usage/fetcher.js +208 -0
- package/src/lib/usageDb.js +7 -0
- package/src/mitm/antigravityIdeVersion.js +50 -0
- package/src/mitm/cert/generate.js +32 -0
- package/src/mitm/cert/install.js +269 -0
- package/src/mitm/cert/rootCA.js +173 -0
- package/src/mitm/config.js +87 -0
- package/src/mitm/dbReader.js +22 -0
- package/src/mitm/dns/dnsConfig.js +266 -0
- package/src/mitm/handlers/antigravity.js +33 -0
- package/src/mitm/handlers/base.js +226 -0
- package/src/mitm/handlers/copilot.js +35 -0
- package/src/mitm/handlers/cursor.js +15 -0
- package/src/mitm/handlers/kiro.js +526 -0
- package/src/mitm/logger.js +106 -0
- package/src/mitm/manager.js +851 -0
- package/src/mitm/paths.js +32 -0
- package/src/mitm/server.js +435 -0
- package/src/mitm/winElevated.js +81 -0
- package/src/models/index.js +38 -0
- package/src/proxy.js +5 -0
- package/src/shared/components/AddCustomEmbeddingModal.js +183 -0
- package/src/shared/components/Avatar.js +88 -0
- package/src/shared/components/Badge.js +54 -0
- package/src/shared/components/BulkAccountAutomationModal.js +508 -0
- package/src/shared/components/Button.js +56 -0
- package/src/shared/components/Card.js +116 -0
- package/src/shared/components/ChangelogModal.js +97 -0
- package/src/shared/components/CodeBuddyQuotaCookieModal.js +109 -0
- package/src/shared/components/ComboFormModal.js +176 -0
- package/src/shared/components/CursorAuthModal.js +212 -0
- package/src/shared/components/DonateModal.js +136 -0
- package/src/shared/components/Drawer.js +82 -0
- package/src/shared/components/EditConnectionModal.js +286 -0
- package/src/shared/components/Footer.js +132 -0
- package/src/shared/components/GitLabAuthModal.js +194 -0
- package/src/shared/components/Header.js +380 -0
- package/src/shared/components/HeaderLanguage.js +46 -0
- package/src/shared/components/HeaderMenu.js +126 -0
- package/src/shared/components/IFlowCookieModal.js +132 -0
- package/src/shared/components/Input.js +65 -0
- package/src/shared/components/KiroAuthModal.js +1171 -0
- package/src/shared/components/KiroOAuthWrapper.js +149 -0
- package/src/shared/components/KiroSocialOAuthModal.js +205 -0
- package/src/shared/components/LanguageSwitcher.js +190 -0
- package/src/shared/components/Loading.js +75 -0
- package/src/shared/components/ManualConfigModal.js +44 -0
- package/src/shared/components/McpMarketplaceModal.js +255 -0
- package/src/shared/components/Modal.js +146 -0
- package/src/shared/components/ModelSelectModal.js +537 -0
- package/src/shared/components/NineRemoteButton.js +23 -0
- package/src/shared/components/NineRemotePromoModal.js +99 -0
- package/src/shared/components/NoAuthProxyCard.js +86 -0
- package/src/shared/components/OAuthModal.js +682 -0
- package/src/shared/components/Pagination.js +150 -0
- package/src/shared/components/PricingModal.js +208 -0
- package/src/shared/components/ProviderIcon.js +63 -0
- package/src/shared/components/ProviderInfoCard.js +82 -0
- package/src/shared/components/RequestLogger.js +121 -0
- package/src/shared/components/SegmentedControl.js +48 -0
- package/src/shared/components/Select.js +67 -0
- package/src/shared/components/Sidebar.js +441 -0
- package/src/shared/components/ThemeProvider.js +15 -0
- package/src/shared/components/ThemeToggle.js +42 -0
- package/src/shared/components/Toggle.js +69 -0
- package/src/shared/components/Tooltip.js +25 -0
- package/src/shared/components/UsageStats.js +505 -0
- package/src/shared/components/index.js +46 -0
- package/src/shared/components/layouts/AuthLayout.js +29 -0
- package/src/shared/components/layouts/DashboardLayout.js +104 -0
- package/src/shared/components/layouts/index.js +4 -0
- package/src/shared/constants/cliTools.js +397 -0
- package/src/shared/constants/colors.js +77 -0
- package/src/shared/constants/config.js +99 -0
- package/src/shared/constants/coworkPlugins.js +75 -0
- package/src/shared/constants/index.js +4 -0
- package/src/shared/constants/locales.js +36 -0
- package/src/shared/constants/mitmToolHosts.js +12 -0
- package/src/shared/constants/models.js +38 -0
- package/src/shared/constants/pricing.js +303 -0
- package/src/shared/constants/providers.js +289 -0
- package/src/shared/constants/skills.js +78 -0
- package/src/shared/constants/ttsProviders.js +138 -0
- package/src/shared/hooks/index.js +2 -0
- package/src/shared/hooks/useCopyToClipboard.js +43 -0
- package/src/shared/hooks/useTheme.js +60 -0
- package/src/shared/services/bootstrap.js +12 -0
- package/src/shared/services/initializeApp.js +268 -0
- package/src/shared/utils/api.js +93 -0
- package/src/shared/utils/apiKey.js +98 -0
- package/src/shared/utils/clineAuth.js +37 -0
- package/src/shared/utils/cn.js +11 -0
- package/src/shared/utils/connectionStatus.js +162 -0
- package/src/shared/utils/index.js +40 -0
- package/src/shared/utils/machine.js +6 -0
- package/src/shared/utils/machineId.js +66 -0
- package/src/shared/utils/providerModelsFetcher.js +30 -0
- package/src/sse/handlers/chat.js +261 -0
- package/src/sse/handlers/embeddings.js +141 -0
- package/src/sse/handlers/fetch.js +213 -0
- package/src/sse/handlers/imageGeneration.js +142 -0
- package/src/sse/handlers/search.js +206 -0
- package/src/sse/handlers/stt.js +88 -0
- package/src/sse/handlers/tts.js +114 -0
- package/src/sse/services/auth.js +346 -0
- package/src/sse/services/codexGateway.js +215 -0
- package/src/sse/services/model.js +99 -0
- package/src/sse/services/tokenRefresh.js +319 -0
- package/src/sse/utils/logger.js +75 -0
- package/src/store/headerSearchStore.js +19 -0
- package/src/store/index.js +6 -0
- package/src/store/notificationStore.js +45 -0
- package/src/store/providerStore.js +55 -0
- package/src/store/settingsStore.js +51 -0
- package/src/store/themeStore.js +54 -0
- package/src/store/userStore.js +20 -0
- package/start.sh +4 -0
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Card, Button, Toggle, Input } from "@/shared/components";
|
|
6
|
+
import { ConfirmModal } from "@/shared/components/Modal";
|
|
7
|
+
import LanguageSwitcher from "@/shared/components/LanguageSwitcher";
|
|
8
|
+
import { useTheme } from "@/shared/hooks/useTheme";
|
|
9
|
+
import { cn } from "@/shared/utils/cn";
|
|
10
|
+
import { APP_CONFIG } from "@/shared/constants/config";
|
|
11
|
+
import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
|
|
12
|
+
import { LOCALE_FLAGS } from "@/shared/constants/locales";
|
|
13
|
+
|
|
14
|
+
function getLocaleFromCookie() {
|
|
15
|
+
if (typeof document === "undefined") return "en";
|
|
16
|
+
const cookie = document.cookie
|
|
17
|
+
.split(";")
|
|
18
|
+
.find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`));
|
|
19
|
+
const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : "en";
|
|
20
|
+
return normalizeLocale(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function ProfilePage() {
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const { theme, setTheme, isDark } = useTheme();
|
|
26
|
+
const [locale, setLocale] = useState("en");
|
|
27
|
+
const [langOpen, setLangOpen] = useState(false);
|
|
28
|
+
const [shutdownOpen, setShutdownOpen] = useState(false);
|
|
29
|
+
const [isShuttingDown, setIsShuttingDown] = useState(false);
|
|
30
|
+
const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" });
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" });
|
|
33
|
+
const [passStatus, setPassStatus] = useState({ type: "", message: "" });
|
|
34
|
+
const [passLoading, setPassLoading] = useState(false);
|
|
35
|
+
const [dbLoading, setDbLoading] = useState(false);
|
|
36
|
+
const [dbStatus, setDbStatus] = useState({ type: "", message: "" });
|
|
37
|
+
const [oidcForm, setOidcForm] = useState({
|
|
38
|
+
authMode: "password",
|
|
39
|
+
oidcIssuerUrl: "",
|
|
40
|
+
oidcClientId: "",
|
|
41
|
+
oidcScopes: "openid profile email",
|
|
42
|
+
oidcLoginLabel: "Sign in with OIDC",
|
|
43
|
+
});
|
|
44
|
+
const [oidcClientSecret, setOidcClientSecret] = useState("");
|
|
45
|
+
const [oidcStatus, setOidcStatus] = useState({ type: "", message: "" });
|
|
46
|
+
const [oidcLoading, setOidcLoading] = useState(false);
|
|
47
|
+
const [oidcTestLoading, setOidcTestLoading] = useState(false);
|
|
48
|
+
const [oidcTestStatus, setOidcTestStatus] = useState({ type: "", message: "" });
|
|
49
|
+
const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback");
|
|
50
|
+
const [oidcExpanded, setOidcExpanded] = useState(false);
|
|
51
|
+
const importFileRef = useRef(null);
|
|
52
|
+
const [proxyForm, setProxyForm] = useState({
|
|
53
|
+
outboundProxyEnabled: false,
|
|
54
|
+
outboundProxyUrl: "",
|
|
55
|
+
outboundNoProxy: "",
|
|
56
|
+
});
|
|
57
|
+
const [proxyStatus, setProxyStatus] = useState({ type: "", message: "" });
|
|
58
|
+
const [proxyLoading, setProxyLoading] = useState(false);
|
|
59
|
+
const [proxyTestLoading, setProxyTestLoading] = useState(false);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setLocale(getLocaleFromCookie());
|
|
63
|
+
}, [langOpen]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
fetch("/api/settings")
|
|
67
|
+
.then((res) => res.json())
|
|
68
|
+
.then((data) => {
|
|
69
|
+
setSettings(data);
|
|
70
|
+
setOidcForm({
|
|
71
|
+
authMode: data?.authMode || "password",
|
|
72
|
+
oidcIssuerUrl: data?.oidcIssuerUrl || "",
|
|
73
|
+
oidcClientId: data?.oidcClientId || "",
|
|
74
|
+
oidcScopes: data?.oidcScopes || "openid profile email",
|
|
75
|
+
oidcLoginLabel: data?.oidcLoginLabel || "Sign in with OIDC",
|
|
76
|
+
});
|
|
77
|
+
setOidcClientSecret("");
|
|
78
|
+
if (data?.authMode === "oidc" || data?.authMode === "both") setOidcExpanded(true);
|
|
79
|
+
setProxyForm({
|
|
80
|
+
outboundProxyEnabled: data?.outboundProxyEnabled === true,
|
|
81
|
+
outboundProxyUrl: data?.outboundProxyUrl || "",
|
|
82
|
+
outboundNoProxy: data?.outboundNoProxy || "",
|
|
83
|
+
});
|
|
84
|
+
setLoading(false);
|
|
85
|
+
})
|
|
86
|
+
.catch((err) => {
|
|
87
|
+
console.error("Failed to fetch settings:", err);
|
|
88
|
+
setLoading(false);
|
|
89
|
+
});
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (typeof window !== "undefined") {
|
|
94
|
+
setOidcRedirectUri(`${window.location.origin}/api/auth/oidc/callback`);
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const updateOutboundProxy = async (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
if (settings.outboundProxyEnabled !== true) return;
|
|
101
|
+
setProxyLoading(true);
|
|
102
|
+
setProxyStatus({ type: "", message: "" });
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch("/api/settings", {
|
|
106
|
+
method: "PATCH",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
outboundProxyUrl: proxyForm.outboundProxyUrl,
|
|
110
|
+
outboundNoProxy: proxyForm.outboundNoProxy,
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
setSettings((prev) => ({ ...prev, ...data }));
|
|
117
|
+
setProxyStatus({ type: "success", message: "Proxy settings applied" });
|
|
118
|
+
} else {
|
|
119
|
+
setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" });
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
setProxyStatus({ type: "error", message: "An error occurred" });
|
|
123
|
+
} finally {
|
|
124
|
+
setProxyLoading(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const testOutboundProxy = async () => {
|
|
129
|
+
if (settings.outboundProxyEnabled !== true) return;
|
|
130
|
+
|
|
131
|
+
const proxyUrl = (proxyForm.outboundProxyUrl || "").trim();
|
|
132
|
+
if (!proxyUrl) {
|
|
133
|
+
setProxyStatus({ type: "error", message: "Please enter a Proxy URL to test" });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setProxyTestLoading(true);
|
|
138
|
+
setProxyStatus({ type: "", message: "" });
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch("/api/settings/proxy-test", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({ proxyUrl }),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const data = await res.json();
|
|
148
|
+
if (res.ok && data?.ok) {
|
|
149
|
+
setProxyStatus({
|
|
150
|
+
type: "success",
|
|
151
|
+
message: `Proxy test OK (${data.status}) in ${data.elapsedMs}ms`,
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
setProxyStatus({
|
|
155
|
+
type: "error",
|
|
156
|
+
message: data?.error || "Proxy test failed",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
setProxyStatus({ type: "error", message: "An error occurred" });
|
|
161
|
+
} finally {
|
|
162
|
+
setProxyTestLoading(false);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const updateOutboundProxyEnabled = async (outboundProxyEnabled) => {
|
|
167
|
+
setProxyLoading(true);
|
|
168
|
+
setProxyStatus({ type: "", message: "" });
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch("/api/settings", {
|
|
172
|
+
method: "PATCH",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ outboundProxyEnabled }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
if (res.ok) {
|
|
179
|
+
setSettings((prev) => ({ ...prev, ...data }));
|
|
180
|
+
setProxyForm((prev) => ({ ...prev, outboundProxyEnabled: data?.outboundProxyEnabled === true }));
|
|
181
|
+
setProxyStatus({
|
|
182
|
+
type: "success",
|
|
183
|
+
message: outboundProxyEnabled ? "Proxy enabled" : "Proxy disabled",
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" });
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
setProxyStatus({ type: "error", message: "An error occurred" });
|
|
190
|
+
} finally {
|
|
191
|
+
setProxyLoading(false);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handlePasswordChange = async (e) => {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
if (passwords.new !== passwords.confirm) {
|
|
198
|
+
setPassStatus({ type: "error", message: "Passwords do not match" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setPassLoading(true);
|
|
203
|
+
setPassStatus({ type: "", message: "" });
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch("/api/settings", {
|
|
207
|
+
method: "PATCH",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
currentPassword: passwords.current,
|
|
211
|
+
newPassword: passwords.new,
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const data = await res.json();
|
|
216
|
+
|
|
217
|
+
if (res.ok) {
|
|
218
|
+
setPassStatus({ type: "success", message: "Password updated successfully" });
|
|
219
|
+
setPasswords({ current: "", new: "", confirm: "" });
|
|
220
|
+
} else {
|
|
221
|
+
setPassStatus({ type: "error", message: data.error || "Failed to update password" });
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
setPassStatus({ type: "error", message: "An error occurred" });
|
|
225
|
+
} finally {
|
|
226
|
+
setPassLoading(false);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const updateFallbackStrategy = async (strategy) => {
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch("/api/settings", {
|
|
233
|
+
method: "PATCH",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify({ fallbackStrategy: strategy }),
|
|
236
|
+
});
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
setSettings(prev => ({ ...prev, fallbackStrategy: strategy }));
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error("Failed to update settings:", err);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const updateComboStrategy = async (strategy) => {
|
|
246
|
+
try {
|
|
247
|
+
const res = await fetch("/api/settings", {
|
|
248
|
+
method: "PATCH",
|
|
249
|
+
headers: { "Content-Type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ comboStrategy: strategy }),
|
|
251
|
+
});
|
|
252
|
+
if (res.ok) {
|
|
253
|
+
setSettings(prev => ({ ...prev, comboStrategy: strategy }));
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error("Failed to update combo strategy:", err);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const updateStickyLimit = async (limit) => {
|
|
261
|
+
const numLimit = parseInt(limit);
|
|
262
|
+
if (isNaN(numLimit) || numLimit < 1) return;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const res = await fetch("/api/settings", {
|
|
266
|
+
method: "PATCH",
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
body: JSON.stringify({ stickyRoundRobinLimit: numLimit }),
|
|
269
|
+
});
|
|
270
|
+
if (res.ok) {
|
|
271
|
+
setSettings(prev => ({ ...prev, stickyRoundRobinLimit: numLimit }));
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error("Failed to update sticky limit:", err);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const updateComboStickyLimit = async (limit) => {
|
|
279
|
+
const numLimit = parseInt(limit);
|
|
280
|
+
if (isNaN(numLimit) || numLimit < 1) return;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch("/api/settings", {
|
|
284
|
+
method: "PATCH",
|
|
285
|
+
headers: { "Content-Type": "application/json" },
|
|
286
|
+
body: JSON.stringify({ comboStickyRoundRobinLimit: numLimit }),
|
|
287
|
+
});
|
|
288
|
+
if (res.ok) {
|
|
289
|
+
setSettings(prev => ({ ...prev, comboStickyRoundRobinLimit: numLimit }));
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error("Failed to update combo sticky limit:", err);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const updateRequireLogin = async (requireLogin) => {
|
|
297
|
+
try {
|
|
298
|
+
const res = await fetch("/api/settings", {
|
|
299
|
+
method: "PATCH",
|
|
300
|
+
headers: { "Content-Type": "application/json" },
|
|
301
|
+
body: JSON.stringify({ requireLogin }),
|
|
302
|
+
});
|
|
303
|
+
if (res.ok) {
|
|
304
|
+
setSettings(prev => ({ ...prev, requireLogin }));
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error("Failed to update require login:", err);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const updateOidcForm = (field, value) => {
|
|
312
|
+
setOidcForm((prev) => ({ ...prev, [field]: value }));
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const saveOidcSettings = async (authMode = oidcForm.authMode || "password") => {
|
|
316
|
+
const issuerUrl = oidcForm.oidcIssuerUrl.trim();
|
|
317
|
+
const clientId = oidcForm.oidcClientId.trim();
|
|
318
|
+
const scopes = oidcForm.oidcScopes.trim();
|
|
319
|
+
const loginLabel = oidcForm.oidcLoginLabel.trim();
|
|
320
|
+
const secret = oidcClientSecret.trim();
|
|
321
|
+
|
|
322
|
+
if (authMode !== "password" && (!issuerUrl || !clientId || !secret) && !settings.oidcConfigured) {
|
|
323
|
+
setOidcStatus({ type: "error", message: "Issuer URL, client ID, and client secret are required to enable OIDC." });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setOidcLoading(true);
|
|
328
|
+
setOidcStatus({ type: "", message: "" });
|
|
329
|
+
setOidcTestStatus({ type: "", message: "" });
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const payload = {
|
|
333
|
+
authMode,
|
|
334
|
+
oidcIssuerUrl: issuerUrl,
|
|
335
|
+
oidcClientId: clientId,
|
|
336
|
+
oidcScopes: scopes || "openid profile email",
|
|
337
|
+
oidcLoginLabel: loginLabel || "Sign in with OIDC",
|
|
338
|
+
};
|
|
339
|
+
if (secret) {
|
|
340
|
+
payload.oidcClientSecret = secret;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const res = await fetch("/api/settings", {
|
|
344
|
+
method: "PATCH",
|
|
345
|
+
headers: { "Content-Type": "application/json" },
|
|
346
|
+
body: JSON.stringify(payload),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const data = await res.json();
|
|
350
|
+
if (res.ok) {
|
|
351
|
+
setSettings((prev) => ({ ...prev, ...data }));
|
|
352
|
+
setOidcForm({
|
|
353
|
+
authMode: data?.authMode || authMode,
|
|
354
|
+
oidcIssuerUrl: data?.oidcIssuerUrl || issuerUrl,
|
|
355
|
+
oidcClientId: data?.oidcClientId || clientId,
|
|
356
|
+
oidcScopes: data?.oidcScopes || scopes || "openid profile email",
|
|
357
|
+
oidcLoginLabel: data?.oidcLoginLabel || loginLabel || "Sign in with OIDC",
|
|
358
|
+
});
|
|
359
|
+
setOidcClientSecret("");
|
|
360
|
+
setOidcStatus({
|
|
361
|
+
type: "success",
|
|
362
|
+
message:
|
|
363
|
+
authMode === "oidc"
|
|
364
|
+
? "OIDC login enabled"
|
|
365
|
+
: authMode === "both"
|
|
366
|
+
? "Password and OIDC login enabled"
|
|
367
|
+
: "OIDC settings saved",
|
|
368
|
+
});
|
|
369
|
+
} else {
|
|
370
|
+
setOidcStatus({ type: "error", message: data.error || "Failed to save OIDC settings" });
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
setOidcStatus({ type: "error", message: "An error occurred" });
|
|
374
|
+
} finally {
|
|
375
|
+
setOidcLoading(false);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const testOidcConnection = async () => {
|
|
380
|
+
const issuerUrl = oidcForm.oidcIssuerUrl.trim();
|
|
381
|
+
const clientId = oidcForm.oidcClientId.trim();
|
|
382
|
+
const scopes = oidcForm.oidcScopes.trim();
|
|
383
|
+
const secret = oidcClientSecret.trim();
|
|
384
|
+
|
|
385
|
+
if (!issuerUrl || !clientId) {
|
|
386
|
+
setOidcTestStatus({ type: "error", message: "Issuer URL and client ID are required to test the connection." });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
setOidcTestLoading(true);
|
|
391
|
+
setOidcStatus({ type: "", message: "" });
|
|
392
|
+
setOidcTestStatus({ type: "", message: "" });
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const saveRes = await fetch("/api/settings", {
|
|
396
|
+
method: "PATCH",
|
|
397
|
+
headers: { "Content-Type": "application/json" },
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
authMode: oidcForm.authMode || settings.authMode || "password",
|
|
400
|
+
oidcIssuerUrl: issuerUrl,
|
|
401
|
+
oidcClientId: clientId,
|
|
402
|
+
oidcScopes: scopes || "openid profile email",
|
|
403
|
+
oidcLoginLabel: oidcForm.oidcLoginLabel.trim() || "Sign in with OIDC",
|
|
404
|
+
...(secret ? { oidcClientSecret: secret } : {}),
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const saved = await saveRes.json().catch(() => ({}));
|
|
409
|
+
if (!saveRes.ok) {
|
|
410
|
+
setOidcTestStatus({
|
|
411
|
+
type: "error",
|
|
412
|
+
message: saved.error || "Failed to save OIDC settings before testing",
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const res = await fetch("/api/auth/oidc/test", {
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: { "Content-Type": "application/json" },
|
|
420
|
+
body: JSON.stringify({
|
|
421
|
+
issuerUrl: saved.oidcIssuerUrl || issuerUrl,
|
|
422
|
+
clientId: saved.oidcClientId || clientId,
|
|
423
|
+
scopes: saved.oidcScopes || scopes || "openid profile email",
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const data = await res.json().catch(() => ({}));
|
|
428
|
+
if (res.ok && data?.ok) {
|
|
429
|
+
const statusMessage = data.clientSecretTested
|
|
430
|
+
? data.clientSecretValid === true
|
|
431
|
+
? `Connection OK. Discovery loaded from ${data.issuerUrl}. Client secret validated too.`
|
|
432
|
+
: `Connection OK. Discovery loaded from ${data.issuerUrl}. Client secret was not checked.`
|
|
433
|
+
: `Connection OK. Discovery loaded from ${data.issuerUrl}.`;
|
|
434
|
+
setOidcTestStatus({
|
|
435
|
+
type: "success",
|
|
436
|
+
message: statusMessage,
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
setOidcTestStatus({ type: "error", message: data.error || "OIDC connection test failed" });
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
setOidcTestStatus({ type: "error", message: "An error occurred" });
|
|
443
|
+
} finally {
|
|
444
|
+
setOidcTestLoading(false);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const updateObservabilityEnabled = async (enabled) => {
|
|
449
|
+
try {
|
|
450
|
+
const res = await fetch("/api/settings", {
|
|
451
|
+
method: "PATCH",
|
|
452
|
+
headers: { "Content-Type": "application/json" },
|
|
453
|
+
body: JSON.stringify({ enableObservability: enabled }),
|
|
454
|
+
});
|
|
455
|
+
if (res.ok) {
|
|
456
|
+
setSettings(prev => ({ ...prev, enableObservability: enabled }));
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error("Failed to update enableObservability:", err);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const reloadSettings = async () => {
|
|
464
|
+
try {
|
|
465
|
+
const res = await fetch("/api/settings");
|
|
466
|
+
if (!res.ok) return;
|
|
467
|
+
const data = await res.json();
|
|
468
|
+
setSettings(data);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
console.error("Failed to reload settings:", err);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const handleExportDatabase = async () => {
|
|
475
|
+
setDbLoading(true);
|
|
476
|
+
setDbStatus({ type: "", message: "" });
|
|
477
|
+
try {
|
|
478
|
+
const res = await fetch("/api/settings/database");
|
|
479
|
+
if (!res.ok) {
|
|
480
|
+
const data = await res.json().catch(() => ({}));
|
|
481
|
+
throw new Error(data.error || "Failed to export database");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const payload = await res.json();
|
|
485
|
+
const content = JSON.stringify(payload, null, 2);
|
|
486
|
+
const blob = new Blob([content], { type: "application/json" });
|
|
487
|
+
const url = URL.createObjectURL(blob);
|
|
488
|
+
const anchor = document.createElement("a");
|
|
489
|
+
const stamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
490
|
+
anchor.href = url;
|
|
491
|
+
anchor.download = `9router-backup-${stamp}.json`;
|
|
492
|
+
document.body.appendChild(anchor);
|
|
493
|
+
anchor.click();
|
|
494
|
+
document.body.removeChild(anchor);
|
|
495
|
+
URL.revokeObjectURL(url);
|
|
496
|
+
|
|
497
|
+
setDbStatus({ type: "success", message: "Database backup downloaded" });
|
|
498
|
+
} catch (err) {
|
|
499
|
+
setDbStatus({ type: "error", message: err.message || "Failed to export database" });
|
|
500
|
+
} finally {
|
|
501
|
+
setDbLoading(false);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const handleImportDatabase = async (event) => {
|
|
506
|
+
const file = event.target.files?.[0];
|
|
507
|
+
if (!file) return;
|
|
508
|
+
|
|
509
|
+
setDbLoading(true);
|
|
510
|
+
setDbStatus({ type: "", message: "" });
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const raw = await file.text();
|
|
514
|
+
const payload = JSON.parse(raw);
|
|
515
|
+
|
|
516
|
+
const res = await fetch("/api/settings/database", {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: { "Content-Type": "application/json" },
|
|
519
|
+
body: JSON.stringify(payload),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const data = await res.json().catch(() => ({}));
|
|
523
|
+
if (!res.ok) {
|
|
524
|
+
throw new Error(data.error || "Failed to import database");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await reloadSettings();
|
|
528
|
+
setDbStatus({ type: "success", message: "Database imported successfully" });
|
|
529
|
+
} catch (err) {
|
|
530
|
+
setDbStatus({ type: "error", message: err.message || "Invalid backup file" });
|
|
531
|
+
} finally {
|
|
532
|
+
if (importFileRef.current) {
|
|
533
|
+
importFileRef.current.value = "";
|
|
534
|
+
}
|
|
535
|
+
setDbLoading(false);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const observabilityEnabled = settings.enableObservability === true;
|
|
540
|
+
|
|
541
|
+
const handleShutdown = async () => {
|
|
542
|
+
setIsShuttingDown(true);
|
|
543
|
+
try {
|
|
544
|
+
await fetch("/api/version/shutdown", { method: "POST" });
|
|
545
|
+
} catch (e) {
|
|
546
|
+
// Expected to fail as server shuts down; ignore error
|
|
547
|
+
}
|
|
548
|
+
setIsShuttingDown(false);
|
|
549
|
+
setShutdownOpen(false);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const handleLogout = async () => {
|
|
553
|
+
try {
|
|
554
|
+
const res = await fetch("/api/auth/logout", { method: "POST" });
|
|
555
|
+
if (res.ok) {
|
|
556
|
+
router.push("/login");
|
|
557
|
+
router.refresh();
|
|
558
|
+
}
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.error("Failed to logout:", err);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<div className="max-w-2xl mx-auto px-4 sm:px-0">
|
|
566
|
+
<div className="flex flex-col gap-6">
|
|
567
|
+
{/* Local Mode Info */}
|
|
568
|
+
<Card>
|
|
569
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
|
570
|
+
<div className="flex items-center gap-3 sm:gap-4">
|
|
571
|
+
<div className="size-10 sm:size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center shrink-0">
|
|
572
|
+
<span className="material-symbols-outlined text-xl sm:text-2xl">computer</span>
|
|
573
|
+
</div>
|
|
574
|
+
<div>
|
|
575
|
+
<h2 className="text-lg sm:text-xl font-semibold">Local Mode</h2>
|
|
576
|
+
<p className="text-sm text-text-muted">Running on your machine</p>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5 w-full sm:w-auto">
|
|
580
|
+
{["light", "dark", "system"].map((option) => (
|
|
581
|
+
<button
|
|
582
|
+
key={option}
|
|
583
|
+
type="button"
|
|
584
|
+
onClick={() => setTheme(option)}
|
|
585
|
+
className={cn(
|
|
586
|
+
"flex items-center justify-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-md font-medium transition-all flex-1 sm:flex-initial",
|
|
587
|
+
theme === option
|
|
588
|
+
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
|
589
|
+
: "text-text-muted hover:text-text-main"
|
|
590
|
+
)}
|
|
591
|
+
>
|
|
592
|
+
<span className="material-symbols-outlined text-[18px]">
|
|
593
|
+
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
|
594
|
+
</span>
|
|
595
|
+
<span className="capitalize text-xs sm:text-sm">{option}</span>
|
|
596
|
+
</button>
|
|
597
|
+
))}
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
<div className="flex flex-col gap-3 pt-4 border-t border-border">
|
|
601
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg bg-bg border border-border gap-2">
|
|
602
|
+
<div>
|
|
603
|
+
<p className="font-medium text-sm sm:text-base">Database Location</p>
|
|
604
|
+
<p className="text-xs sm:text-sm text-text-muted font-mono break-all">~/.9router/db/data.sqlite</p>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
<div className="flex flex-col sm:flex-row gap-2">
|
|
608
|
+
<Button
|
|
609
|
+
variant="secondary"
|
|
610
|
+
icon="download"
|
|
611
|
+
onClick={handleExportDatabase}
|
|
612
|
+
loading={dbLoading}
|
|
613
|
+
className="w-full sm:w-auto"
|
|
614
|
+
>
|
|
615
|
+
Download Backup
|
|
616
|
+
</Button>
|
|
617
|
+
<Button
|
|
618
|
+
variant="outline"
|
|
619
|
+
icon="upload"
|
|
620
|
+
onClick={() => importFileRef.current?.click()}
|
|
621
|
+
disabled={dbLoading}
|
|
622
|
+
className="w-full sm:w-auto"
|
|
623
|
+
>
|
|
624
|
+
Import Backup
|
|
625
|
+
</Button>
|
|
626
|
+
<input
|
|
627
|
+
ref={importFileRef}
|
|
628
|
+
type="file"
|
|
629
|
+
accept="application/json,.json"
|
|
630
|
+
className="hidden"
|
|
631
|
+
onChange={handleImportDatabase}
|
|
632
|
+
/>
|
|
633
|
+
</div>
|
|
634
|
+
{dbStatus.message && (
|
|
635
|
+
<p className={`text-sm ${dbStatus.type === "error" ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
|
|
636
|
+
{dbStatus.message}
|
|
637
|
+
</p>
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
</Card>
|
|
641
|
+
|
|
642
|
+
{/* Language */}
|
|
643
|
+
<Card>
|
|
644
|
+
<div className="flex items-center gap-3 mb-4">
|
|
645
|
+
<div className="size-10 rounded-lg bg-blue-500/10 text-blue-500 flex items-center justify-center shrink-0">
|
|
646
|
+
<span className="material-symbols-outlined text-[20px]">language</span>
|
|
647
|
+
</div>
|
|
648
|
+
<h3 className="text-base sm:text-lg font-semibold">Language</h3>
|
|
649
|
+
</div>
|
|
650
|
+
<button
|
|
651
|
+
onClick={() => setLangOpen(true)}
|
|
652
|
+
className="flex items-center justify-between w-full p-3 rounded-lg bg-bg border border-border hover:border-primary/50 transition-colors"
|
|
653
|
+
data-i18n-skip="true"
|
|
654
|
+
>
|
|
655
|
+
<span className="text-sm text-text-muted">Display language</span>
|
|
656
|
+
<span className="text-2xl">{LOCALE_FLAGS[locale] || "🌐"}</span>
|
|
657
|
+
</button>
|
|
658
|
+
</Card>
|
|
659
|
+
|
|
660
|
+
{/* Security */}
|
|
661
|
+
<Card>
|
|
662
|
+
<div className="flex items-center gap-3 mb-4">
|
|
663
|
+
<div className="p-2 rounded-lg bg-primary/10 text-primary shrink-0">
|
|
664
|
+
<span className="material-symbols-outlined text-[20px]">shield</span>
|
|
665
|
+
</div>
|
|
666
|
+
<h3 className="text-base sm:text-lg font-semibold">Security</h3>
|
|
667
|
+
</div>
|
|
668
|
+
<div className="flex flex-col gap-4">
|
|
669
|
+
<div className="flex items-start sm:items-center justify-between gap-4">
|
|
670
|
+
<div className="flex-1 min-w-0">
|
|
671
|
+
<p className="font-medium text-sm sm:text-base">Require login</p>
|
|
672
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
673
|
+
When ON, dashboard requires password. When OFF, access without login.
|
|
674
|
+
</p>
|
|
675
|
+
</div>
|
|
676
|
+
<Toggle
|
|
677
|
+
checked={settings.requireLogin === true}
|
|
678
|
+
onChange={() => updateRequireLogin(!settings.requireLogin)}
|
|
679
|
+
disabled={loading}
|
|
680
|
+
/>
|
|
681
|
+
</div>
|
|
682
|
+
{settings.requireLogin === true && (
|
|
683
|
+
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4 pt-4 border-t border-border/50">
|
|
684
|
+
{settings.hasPassword && (
|
|
685
|
+
<div className="flex flex-col gap-2">
|
|
686
|
+
<label className="text-xs sm:text-sm font-medium">Current Password</label>
|
|
687
|
+
<Input
|
|
688
|
+
type="password"
|
|
689
|
+
placeholder="Enter current password"
|
|
690
|
+
value={passwords.current}
|
|
691
|
+
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
|
|
692
|
+
required
|
|
693
|
+
/>
|
|
694
|
+
</div>
|
|
695
|
+
)}
|
|
696
|
+
{/* {!settings.hasPassword && (
|
|
697
|
+
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
|
698
|
+
<p className="text-sm text-blue-600 dark:text-blue-400">
|
|
699
|
+
Setting password for the first time. Leave current password empty or use default: <code className="bg-blue-500/20 px-1 rounded">123456</code>
|
|
700
|
+
</p>
|
|
701
|
+
</div>
|
|
702
|
+
)} */}
|
|
703
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
704
|
+
<div className="flex flex-col gap-2">
|
|
705
|
+
<label className="text-xs sm:text-sm font-medium">New Password</label>
|
|
706
|
+
<Input
|
|
707
|
+
type="password"
|
|
708
|
+
placeholder="Enter new password"
|
|
709
|
+
value={passwords.new}
|
|
710
|
+
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
|
|
711
|
+
required
|
|
712
|
+
/>
|
|
713
|
+
</div>
|
|
714
|
+
<div className="flex flex-col gap-2">
|
|
715
|
+
<label className="text-xs sm:text-sm font-medium">Confirm New Password</label>
|
|
716
|
+
<Input
|
|
717
|
+
type="password"
|
|
718
|
+
placeholder="Confirm new password"
|
|
719
|
+
value={passwords.confirm}
|
|
720
|
+
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
|
|
721
|
+
required
|
|
722
|
+
/>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
{passStatus.message && (
|
|
727
|
+
<p className={`text-xs sm:text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
|
728
|
+
{passStatus.message}
|
|
729
|
+
</p>
|
|
730
|
+
)}
|
|
731
|
+
|
|
732
|
+
<div className="pt-2">
|
|
733
|
+
<Button type="submit" variant="primary" loading={passLoading} className="w-full sm:w-auto">
|
|
734
|
+
{settings.hasPassword ? "Update Password" : "Set Password"}
|
|
735
|
+
</Button>
|
|
736
|
+
</div>
|
|
737
|
+
</form>
|
|
738
|
+
)}
|
|
739
|
+
</div>
|
|
740
|
+
</Card>
|
|
741
|
+
|
|
742
|
+
{/* OIDC */}
|
|
743
|
+
<Card>
|
|
744
|
+
<button
|
|
745
|
+
type="button"
|
|
746
|
+
onClick={() => setOidcExpanded((v) => !v)}
|
|
747
|
+
className="w-full flex items-center gap-3 text-left"
|
|
748
|
+
>
|
|
749
|
+
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-500 shrink-0">
|
|
750
|
+
<span className="material-symbols-outlined text-[20px]">lock_open</span>
|
|
751
|
+
</div>
|
|
752
|
+
<div className="flex-1 min-w-0">
|
|
753
|
+
<h3 className="text-base sm:text-lg font-semibold">OIDC Dashboard Login</h3>
|
|
754
|
+
<p className="text-xs text-text-muted">
|
|
755
|
+
{settings.authMode === "oidc" ? "OIDC active" : settings.authMode === "both" ? "Password + OIDC active" : "Optional SSO via Authentik/Keycloak/Google"}
|
|
756
|
+
</p>
|
|
757
|
+
</div>
|
|
758
|
+
<span className="material-symbols-outlined text-text-muted shrink-0">
|
|
759
|
+
{oidcExpanded ? "expand_less" : "expand_more"}
|
|
760
|
+
</span>
|
|
761
|
+
</button>
|
|
762
|
+
{oidcExpanded && (
|
|
763
|
+
<div className="flex flex-col gap-4 mt-4">
|
|
764
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
765
|
+
Use Authentik or any OIDC provider to sign in to the dashboard. You can enable password-only, OIDC-only, or both for the dashboard; model API access still uses API keys.
|
|
766
|
+
</p>
|
|
767
|
+
|
|
768
|
+
<div className="flex flex-col gap-2">
|
|
769
|
+
<label className="font-medium text-sm sm:text-base">Auth Mode</label>
|
|
770
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
771
|
+
{[
|
|
772
|
+
{
|
|
773
|
+
value: "password",
|
|
774
|
+
title: "Password only",
|
|
775
|
+
desc: "Keep the legacy password login.",
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
value: "oidc",
|
|
779
|
+
title: "OIDC only",
|
|
780
|
+
desc: "Require OIDC for dashboard access.",
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
value: "both",
|
|
784
|
+
title: "Both",
|
|
785
|
+
desc: "Allow either password or OIDC.",
|
|
786
|
+
},
|
|
787
|
+
].map((option) => {
|
|
788
|
+
const active = oidcForm.authMode === option.value;
|
|
789
|
+
return (
|
|
790
|
+
<button
|
|
791
|
+
key={option.value}
|
|
792
|
+
type="button"
|
|
793
|
+
onClick={() => updateOidcForm("authMode", option.value)}
|
|
794
|
+
className={cn(
|
|
795
|
+
"text-left rounded-lg border p-3 transition-colors",
|
|
796
|
+
active
|
|
797
|
+
? "border-primary bg-primary/5"
|
|
798
|
+
: "border-border bg-bg hover:bg-black/5 dark:hover:bg-white/5"
|
|
799
|
+
)}
|
|
800
|
+
disabled={loading || oidcLoading}
|
|
801
|
+
>
|
|
802
|
+
<p className="font-medium text-sm sm:text-base">{option.title}</p>
|
|
803
|
+
<p className="text-xs sm:text-sm text-text-muted mt-1">{option.desc}</p>
|
|
804
|
+
</button>
|
|
805
|
+
);
|
|
806
|
+
})}
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<div className="grid grid-cols-1 gap-4">
|
|
811
|
+
<div className="flex flex-col gap-2">
|
|
812
|
+
<label className="font-medium text-sm sm:text-base">Issuer URL</label>
|
|
813
|
+
<Input
|
|
814
|
+
placeholder="https://auth.example.com/application/o/9router/"
|
|
815
|
+
value={oidcForm.oidcIssuerUrl}
|
|
816
|
+
onChange={(e) => updateOidcForm("oidcIssuerUrl", e.target.value)}
|
|
817
|
+
disabled={loading || oidcLoading}
|
|
818
|
+
/>
|
|
819
|
+
</div>
|
|
820
|
+
|
|
821
|
+
<div className="flex flex-col gap-2">
|
|
822
|
+
<label className="font-medium text-sm sm:text-base">Client ID</label>
|
|
823
|
+
<Input
|
|
824
|
+
placeholder="9router-dashboard"
|
|
825
|
+
value={oidcForm.oidcClientId}
|
|
826
|
+
onChange={(e) => updateOidcForm("oidcClientId", e.target.value)}
|
|
827
|
+
disabled={loading || oidcLoading}
|
|
828
|
+
/>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<div className="flex flex-col gap-2">
|
|
832
|
+
<label className="font-medium text-sm sm:text-base">Client Secret</label>
|
|
833
|
+
<Input
|
|
834
|
+
type="password"
|
|
835
|
+
placeholder="Leave blank to keep existing secret"
|
|
836
|
+
value={oidcClientSecret}
|
|
837
|
+
onChange={(e) => setOidcClientSecret(e.target.value)}
|
|
838
|
+
disabled={loading || oidcLoading}
|
|
839
|
+
/>
|
|
840
|
+
<p className="text-xs sm:text-sm text-text-muted">This value is write-only after saving.</p>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div className="flex flex-col gap-2">
|
|
844
|
+
<label className="font-medium text-sm sm:text-base">Scopes</label>
|
|
845
|
+
<Input
|
|
846
|
+
placeholder="openid profile email"
|
|
847
|
+
value={oidcForm.oidcScopes}
|
|
848
|
+
onChange={(e) => updateOidcForm("oidcScopes", e.target.value)}
|
|
849
|
+
disabled={loading || oidcLoading}
|
|
850
|
+
/>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<div className="flex flex-col gap-2">
|
|
854
|
+
<label className="font-medium text-sm sm:text-base">Login Button Label</label>
|
|
855
|
+
<Input
|
|
856
|
+
placeholder="Sign in with OIDC"
|
|
857
|
+
value={oidcForm.oidcLoginLabel}
|
|
858
|
+
onChange={(e) => updateOidcForm("oidcLoginLabel", e.target.value)}
|
|
859
|
+
disabled={loading || oidcLoading}
|
|
860
|
+
/>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<div className="rounded-lg border border-border bg-bg p-3 text-xs sm:text-sm text-text-muted">
|
|
865
|
+
<p className="font-medium text-text-main mb-1">Redirect URI</p>
|
|
866
|
+
<code className="block break-all font-mono">{oidcRedirectUri}</code>
|
|
867
|
+
</div>
|
|
868
|
+
|
|
869
|
+
<div className="flex flex-col sm:flex-row gap-2 pt-2 border-t border-border/50">
|
|
870
|
+
<Button type="button" variant="primary" loading={oidcLoading} onClick={() => saveOidcSettings()} className="w-full sm:w-auto">
|
|
871
|
+
Save auth mode
|
|
872
|
+
</Button>
|
|
873
|
+
<Button type="button" variant="outline" loading={oidcTestLoading} onClick={testOidcConnection} className="w-full sm:w-auto">
|
|
874
|
+
Test connection
|
|
875
|
+
</Button>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
{oidcTestStatus.message && (
|
|
879
|
+
<p className={`text-xs sm:text-sm ${oidcTestStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
|
880
|
+
{oidcTestStatus.message}
|
|
881
|
+
</p>
|
|
882
|
+
)}
|
|
883
|
+
|
|
884
|
+
{oidcStatus.message && (
|
|
885
|
+
<p className={`text-xs sm:text-sm ${oidcStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
|
886
|
+
{oidcStatus.message}
|
|
887
|
+
</p>
|
|
888
|
+
)}
|
|
889
|
+
|
|
890
|
+
{settings.authMode === "oidc" && (
|
|
891
|
+
<p className="text-xs sm:text-sm text-amber-600 dark:text-amber-400">
|
|
892
|
+
OIDC login is currently active. Password login is disabled until you switch back.
|
|
893
|
+
</p>
|
|
894
|
+
)}
|
|
895
|
+
|
|
896
|
+
{settings.authMode === "both" && (
|
|
897
|
+
<p className="text-xs sm:text-sm text-amber-600 dark:text-amber-400">
|
|
898
|
+
Password and OIDC login are both active.
|
|
899
|
+
</p>
|
|
900
|
+
)}
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
</Card>
|
|
904
|
+
|
|
905
|
+
{/* Routing Preferences */}
|
|
906
|
+
<Card>
|
|
907
|
+
<div className="flex items-center gap-3 mb-4">
|
|
908
|
+
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500 shrink-0">
|
|
909
|
+
<span className="material-symbols-outlined text-[20px]">route</span>
|
|
910
|
+
</div>
|
|
911
|
+
<h3 className="text-base sm:text-lg font-semibold">Routing Strategy</h3>
|
|
912
|
+
</div>
|
|
913
|
+
<div className="flex flex-col gap-4">
|
|
914
|
+
<div className="flex items-start sm:items-center justify-between gap-4">
|
|
915
|
+
<div className="flex-1 min-w-0">
|
|
916
|
+
<p className="font-medium text-sm sm:text-base">Round Robin</p>
|
|
917
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
918
|
+
Cycle through accounts to distribute load
|
|
919
|
+
</p>
|
|
920
|
+
</div>
|
|
921
|
+
<Toggle
|
|
922
|
+
checked={settings.fallbackStrategy === "round-robin"}
|
|
923
|
+
onChange={() => updateFallbackStrategy(settings.fallbackStrategy === "round-robin" ? "fill-first" : "round-robin")}
|
|
924
|
+
disabled={loading}
|
|
925
|
+
/>
|
|
926
|
+
</div>
|
|
927
|
+
|
|
928
|
+
{/* Sticky Round Robin Limit */}
|
|
929
|
+
{settings.fallbackStrategy === "round-robin" && (
|
|
930
|
+
<div className="flex items-start sm:items-center justify-between gap-4 pt-2 border-t border-border/50">
|
|
931
|
+
<div className="flex-1 min-w-0">
|
|
932
|
+
<p className="font-medium text-sm sm:text-base">Sticky Limit</p>
|
|
933
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
934
|
+
Calls per account before switching
|
|
935
|
+
</p>
|
|
936
|
+
</div>
|
|
937
|
+
<Input
|
|
938
|
+
type="number"
|
|
939
|
+
min="1"
|
|
940
|
+
max="10"
|
|
941
|
+
value={settings.stickyRoundRobinLimit || 3}
|
|
942
|
+
onChange={(e) => updateStickyLimit(e.target.value)}
|
|
943
|
+
disabled={loading}
|
|
944
|
+
className="w-16 sm:w-20 text-center shrink-0"
|
|
945
|
+
/>
|
|
946
|
+
</div>
|
|
947
|
+
)}
|
|
948
|
+
|
|
949
|
+
{/* Combo Round Robin */}
|
|
950
|
+
<div className="flex items-start sm:items-center justify-between gap-4 pt-4 border-t border-border/50">
|
|
951
|
+
<div className="flex-1 min-w-0">
|
|
952
|
+
<p className="font-medium text-sm sm:text-base">Combo Round Robin</p>
|
|
953
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
954
|
+
Cycle through providers in combos instead of always starting with first
|
|
955
|
+
</p>
|
|
956
|
+
</div>
|
|
957
|
+
<Toggle
|
|
958
|
+
checked={settings.comboStrategy === "round-robin"}
|
|
959
|
+
onChange={() => updateComboStrategy(settings.comboStrategy === "round-robin" ? "fallback" : "round-robin")}
|
|
960
|
+
disabled={loading}
|
|
961
|
+
/>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
{/* Combo Sticky Round Robin Limit */}
|
|
965
|
+
{settings.comboStrategy === "round-robin" && (
|
|
966
|
+
<div className="flex items-center justify-between pt-2 border-t border-border/50">
|
|
967
|
+
<div>
|
|
968
|
+
<p className="font-medium">Combo Sticky Limit</p>
|
|
969
|
+
<p className="text-sm text-text-muted">
|
|
970
|
+
Calls per combo model before switching
|
|
971
|
+
</p>
|
|
972
|
+
</div>
|
|
973
|
+
<Input
|
|
974
|
+
type="number"
|
|
975
|
+
min="1"
|
|
976
|
+
max="100"
|
|
977
|
+
value={settings.comboStickyRoundRobinLimit || 1}
|
|
978
|
+
onChange={(e) => updateComboStickyLimit(e.target.value)}
|
|
979
|
+
disabled={loading}
|
|
980
|
+
className="w-20 text-center"
|
|
981
|
+
/>
|
|
982
|
+
</div>
|
|
983
|
+
)}
|
|
984
|
+
|
|
985
|
+
<p className="text-xs text-text-muted italic pt-2 border-t border-border/50">
|
|
986
|
+
{settings.fallbackStrategy === "round-robin"
|
|
987
|
+
? `Currently distributing requests across all available accounts with ${settings.stickyRoundRobinLimit || 3} calls per account.`
|
|
988
|
+
: "Currently using accounts in priority order (Fill First)."}
|
|
989
|
+
{settings.comboStrategy === "round-robin"
|
|
990
|
+
? ` Combos rotate after ${settings.comboStickyRoundRobinLimit || 1} call${(settings.comboStickyRoundRobinLimit || 1) === 1 ? "" : "s"} per model.`
|
|
991
|
+
: " Combos always start with their first model."}
|
|
992
|
+
</p>
|
|
993
|
+
</div>
|
|
994
|
+
</Card>
|
|
995
|
+
|
|
996
|
+
{/* Network */}
|
|
997
|
+
<Card>
|
|
998
|
+
<div className="flex items-center gap-3 mb-4">
|
|
999
|
+
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500 shrink-0">
|
|
1000
|
+
<span className="material-symbols-outlined text-[20px]">wifi</span>
|
|
1001
|
+
</div>
|
|
1002
|
+
<h3 className="text-base sm:text-lg font-semibold">Network</h3>
|
|
1003
|
+
</div>
|
|
1004
|
+
|
|
1005
|
+
<div className="flex flex-col gap-4">
|
|
1006
|
+
<div className="flex items-start sm:items-center justify-between gap-4">
|
|
1007
|
+
<div className="flex-1 min-w-0">
|
|
1008
|
+
<p className="font-medium text-sm sm:text-base">Outbound Proxy</p>
|
|
1009
|
+
<p className="text-xs sm:text-sm text-text-muted">Enable proxy for OAuth + provider outbound requests.</p>
|
|
1010
|
+
</div>
|
|
1011
|
+
<Toggle
|
|
1012
|
+
checked={settings.outboundProxyEnabled === true}
|
|
1013
|
+
onChange={() => updateOutboundProxyEnabled(!(settings.outboundProxyEnabled === true))}
|
|
1014
|
+
disabled={loading || proxyLoading}
|
|
1015
|
+
/>
|
|
1016
|
+
</div>
|
|
1017
|
+
|
|
1018
|
+
{settings.outboundProxyEnabled === true && (
|
|
1019
|
+
<form onSubmit={updateOutboundProxy} className="flex flex-col gap-4 pt-2 border-t border-border/50">
|
|
1020
|
+
<div className="flex flex-col gap-2">
|
|
1021
|
+
<label className="font-medium text-sm sm:text-base">Proxy URL</label>
|
|
1022
|
+
<Input
|
|
1023
|
+
placeholder="http://127.0.0.1:7897"
|
|
1024
|
+
value={proxyForm.outboundProxyUrl}
|
|
1025
|
+
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundProxyUrl: e.target.value }))}
|
|
1026
|
+
disabled={loading || proxyLoading}
|
|
1027
|
+
/>
|
|
1028
|
+
<p className="text-xs sm:text-sm text-text-muted">Leave empty to inherit existing env proxy (if any).</p>
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
<div className="flex flex-col gap-2 pt-2 border-t border-border/50">
|
|
1032
|
+
<label className="font-medium text-sm sm:text-base">No Proxy</label>
|
|
1033
|
+
<Input
|
|
1034
|
+
placeholder="localhost,127.0.0.1"
|
|
1035
|
+
value={proxyForm.outboundNoProxy}
|
|
1036
|
+
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundNoProxy: e.target.value }))}
|
|
1037
|
+
disabled={loading || proxyLoading}
|
|
1038
|
+
/>
|
|
1039
|
+
<p className="text-xs sm:text-sm text-text-muted">Comma-separated hostnames/domains to bypass the proxy.</p>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<div className="pt-2 border-t border-border/50 flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
|
1043
|
+
<Button
|
|
1044
|
+
type="button"
|
|
1045
|
+
variant="secondary"
|
|
1046
|
+
loading={proxyTestLoading}
|
|
1047
|
+
disabled={loading || proxyLoading}
|
|
1048
|
+
onClick={testOutboundProxy}
|
|
1049
|
+
className="w-full sm:w-auto"
|
|
1050
|
+
>
|
|
1051
|
+
Test proxy URL
|
|
1052
|
+
</Button>
|
|
1053
|
+
<Button type="submit" variant="primary" loading={proxyLoading} className="w-full sm:w-auto">
|
|
1054
|
+
Apply
|
|
1055
|
+
</Button>
|
|
1056
|
+
</div>
|
|
1057
|
+
</form>
|
|
1058
|
+
)}
|
|
1059
|
+
|
|
1060
|
+
{proxyStatus.message && (
|
|
1061
|
+
<p className={`text-xs sm:text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"} pt-2 border-t border-border/50`}>
|
|
1062
|
+
{proxyStatus.message}
|
|
1063
|
+
</p>
|
|
1064
|
+
)}
|
|
1065
|
+
</div>
|
|
1066
|
+
</Card>
|
|
1067
|
+
|
|
1068
|
+
{/* Observability Settings */}
|
|
1069
|
+
<Card>
|
|
1070
|
+
<div className="flex items-center gap-3 mb-4">
|
|
1071
|
+
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500 shrink-0">
|
|
1072
|
+
<span className="material-symbols-outlined text-[20px]">monitoring</span>
|
|
1073
|
+
</div>
|
|
1074
|
+
<h3 className="text-base sm:text-lg font-semibold">Observability</h3>
|
|
1075
|
+
</div>
|
|
1076
|
+
<div className="flex items-start sm:items-center justify-between gap-4">
|
|
1077
|
+
<div className="flex-1 min-w-0">
|
|
1078
|
+
<p className="font-medium text-sm sm:text-base">Enable Observability</p>
|
|
1079
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
1080
|
+
Record request details for inspection in the logs view
|
|
1081
|
+
</p>
|
|
1082
|
+
</div>
|
|
1083
|
+
<Toggle
|
|
1084
|
+
checked={observabilityEnabled}
|
|
1085
|
+
onChange={updateObservabilityEnabled}
|
|
1086
|
+
disabled={loading}
|
|
1087
|
+
/>
|
|
1088
|
+
</div>
|
|
1089
|
+
</Card>
|
|
1090
|
+
|
|
1091
|
+
{/* Account actions */}
|
|
1092
|
+
<div className="flex flex-col sm:flex-row gap-2">
|
|
1093
|
+
<Button
|
|
1094
|
+
variant="outline"
|
|
1095
|
+
fullWidth
|
|
1096
|
+
icon="power_settings_new"
|
|
1097
|
+
onClick={() => setShutdownOpen(true)}
|
|
1098
|
+
className="text-red-500 border-red-200 hover:bg-red-50 hover:border-red-300"
|
|
1099
|
+
>
|
|
1100
|
+
Shutdown
|
|
1101
|
+
</Button>
|
|
1102
|
+
<Button
|
|
1103
|
+
variant="outline"
|
|
1104
|
+
fullWidth
|
|
1105
|
+
icon="logout"
|
|
1106
|
+
onClick={handleLogout}
|
|
1107
|
+
>
|
|
1108
|
+
Logout
|
|
1109
|
+
</Button>
|
|
1110
|
+
</div>
|
|
1111
|
+
|
|
1112
|
+
{/* App Info */}
|
|
1113
|
+
<div className="text-center text-xs sm:text-sm text-text-muted py-4">
|
|
1114
|
+
<p>{APP_CONFIG.name} v{APP_CONFIG.version}</p>
|
|
1115
|
+
<p className="mt-1">Local Mode - All data stored on your machine</p>
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
|
|
1119
|
+
<LanguageSwitcher
|
|
1120
|
+
hideTrigger
|
|
1121
|
+
isOpen={langOpen}
|
|
1122
|
+
onClose={(next) => {
|
|
1123
|
+
setLangOpen(false);
|
|
1124
|
+
setLocale(next);
|
|
1125
|
+
}}
|
|
1126
|
+
/>
|
|
1127
|
+
<ConfirmModal
|
|
1128
|
+
isOpen={shutdownOpen}
|
|
1129
|
+
onClose={() => setShutdownOpen(false)}
|
|
1130
|
+
onConfirm={handleShutdown}
|
|
1131
|
+
title="Close Proxy"
|
|
1132
|
+
message="Are you sure you want to close the proxy server?"
|
|
1133
|
+
confirmText="Close"
|
|
1134
|
+
cancelText="Cancel"
|
|
1135
|
+
variant="danger"
|
|
1136
|
+
loading={isShuttingDown}
|
|
1137
|
+
/>
|
|
1138
|
+
</div>
|
|
1139
|
+
);
|
|
1140
|
+
}
|