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,1555 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components";
|
|
6
|
+
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
|
7
|
+
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
|
|
8
|
+
|
|
9
|
+
// Locales that unlock wenyan (classical Chinese) caveman levels
|
|
10
|
+
const WENYAN_LOCALES = ["zh-CN", "zh-TW"];
|
|
11
|
+
|
|
12
|
+
const TUNNEL_BENEFITS = [
|
|
13
|
+
{ icon: "public", title: "Access Anywhere", desc: "Use your API from any network" },
|
|
14
|
+
{ icon: "group", title: "Share Endpoint", desc: "Share URL with team members" },
|
|
15
|
+
{ icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" },
|
|
16
|
+
{ icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const TUNNEL_PING_INTERVAL_MS = 2000;
|
|
20
|
+
const TUNNEL_PING_MAX_MS = 300000;
|
|
21
|
+
const STATUS_POLL_FAST_MS = 5000;
|
|
22
|
+
const STATUS_POLL_SLOW_MS = 30000;
|
|
23
|
+
const REACHABLE_MISS_THRESHOLD = 5;
|
|
24
|
+
const CLIENT_PING_FAST_MS = 10000;
|
|
25
|
+
const CLIENT_PING_SLOW_MS = 60000;
|
|
26
|
+
const CLIENT_PING_TIMEOUT_MS = 5000;
|
|
27
|
+
|
|
28
|
+
// Browser-side health probe: must reach origin (not just CF/TS edge).
|
|
29
|
+
// cors mode → res.ok=false for 5xx (e.g. Cloudflare 530 when origin dead).
|
|
30
|
+
// /api/health route sets Access-Control-Allow-Origin: * → CORS works through tunnel.
|
|
31
|
+
async function clientPingUrl(url) {
|
|
32
|
+
if (!url) return false;
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${url}/api/health`, {
|
|
35
|
+
mode: "cors",
|
|
36
|
+
cache: "no-store",
|
|
37
|
+
signal: AbortSignal.timeout(CLIENT_PING_TIMEOUT_MS),
|
|
38
|
+
});
|
|
39
|
+
return res.ok;
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Race multiple URLs: resolve true as soon as any one passes ping.
|
|
44
|
+
async function clientPingAny(...urls) {
|
|
45
|
+
const checks = urls.filter(Boolean).map(clientPingUrl);
|
|
46
|
+
if (!checks.length) return false;
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let pending = checks.length;
|
|
49
|
+
checks.forEach((p) => p.then((ok) => {
|
|
50
|
+
if (ok) resolve(true);
|
|
51
|
+
else if (--pending === 0) resolve(false);
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const CAVEMAN_LEVELS = [
|
|
57
|
+
{ id: "lite", label: "Lite", desc: "Drop filler, keep grammar" },
|
|
58
|
+
{ id: "full", label: "Full", desc: "Drop articles, fragments OK" },
|
|
59
|
+
{ id: "ultra", label: "Ultra", desc: "Telegraphic, max compression" },
|
|
60
|
+
{ id: "wenyan-lite", label: "文 Lite", desc: "Classical Chinese, light compression", wenyan: true },
|
|
61
|
+
{ id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true },
|
|
62
|
+
{ id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true },
|
|
63
|
+
];
|
|
64
|
+
export default function APIPageClient({ machineId }) {
|
|
65
|
+
const [keys, setKeys] = useState([]);
|
|
66
|
+
const [loading, setLoading] = useState(true);
|
|
67
|
+
const [showAddModal, setShowAddModal] = useState(false);
|
|
68
|
+
const [newKeyName, setNewKeyName] = useState("");
|
|
69
|
+
const [createdKey, setCreatedKey] = useState(null);
|
|
70
|
+
const [confirmState, setConfirmState] = useState(null);
|
|
71
|
+
|
|
72
|
+
const [requireApiKey, setRequireApiKey] = useState(false);
|
|
73
|
+
const [requireLogin, setRequireLogin] = useState(true);
|
|
74
|
+
const [hasPassword, setHasPassword] = useState(true);
|
|
75
|
+
const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false);
|
|
76
|
+
const [rtkEnabled, setRtkEnabledState] = useState(true);
|
|
77
|
+
const [cavemanEnabled, setCavemanEnabled] = useState(false);
|
|
78
|
+
const [cavemanLevel, setCavemanLevel] = useState("full");
|
|
79
|
+
const [locale, setLocale] = useState("en");
|
|
80
|
+
|
|
81
|
+
// Cloudflare Tunnel state
|
|
82
|
+
const [tunnelChecking, setTunnelChecking] = useState(true);
|
|
83
|
+
const [tunnelEnabled, setTunnelEnabled] = useState(false);
|
|
84
|
+
const [tunnelReachable, setTunnelReachable] = useState(false);
|
|
85
|
+
const [tunnelUrl, setTunnelUrl] = useState("");
|
|
86
|
+
const [tunnelPublicUrl, setTunnelPublicUrl] = useState("");
|
|
87
|
+
const [tunnelLoading, setTunnelLoading] = useState(false);
|
|
88
|
+
const [tunnelProgress, setTunnelProgress] = useState("");
|
|
89
|
+
const [tunnelStatus, setTunnelStatus] = useState(null);
|
|
90
|
+
const [showEnableTunnelModal, setShowEnableTunnelModal] = useState(false);
|
|
91
|
+
const [showDisableTunnelModal, setShowDisableTunnelModal] = useState(false);
|
|
92
|
+
|
|
93
|
+
// Tailscale state
|
|
94
|
+
const [tsEnabled, setTsEnabled] = useState(false);
|
|
95
|
+
const [tsReachable, setTsReachable] = useState(false);
|
|
96
|
+
const [tsUrl, setTsUrl] = useState("");
|
|
97
|
+
const [tsLoading, setTsLoading] = useState(false);
|
|
98
|
+
const [tsProgress, setTsProgress] = useState("");
|
|
99
|
+
const [tsStatus, setTsStatus] = useState(null);
|
|
100
|
+
const [tsAuthUrl, setTsAuthUrl] = useState("");
|
|
101
|
+
const [tsAuthLabel, setTsAuthLabel] = useState("");
|
|
102
|
+
const [tsInstalled, setTsInstalled] = useState(null); // null=checking, true/false
|
|
103
|
+
const [tsInstalling, setTsInstalling] = useState(false);
|
|
104
|
+
const [tsInstallLog, setTsInstallLog] = useState([]);
|
|
105
|
+
const [tsSudoPassword, setTsSudoPassword] = useState("");
|
|
106
|
+
const [tsConnecting, setTsConnecting] = useState(false);
|
|
107
|
+
const [showTsModal, setShowTsModal] = useState(false);
|
|
108
|
+
const [showDisableTsModal, setShowDisableTsModal] = useState(false);
|
|
109
|
+
const tsLogRef = useRef(null);
|
|
110
|
+
|
|
111
|
+
// Debounce reachable=false: server may briefly return false during background refresh.
|
|
112
|
+
// Only flip UI to "reconnecting" after N consecutive misses to avoid spinner flicker.
|
|
113
|
+
const tunnelMissRef = useRef(0);
|
|
114
|
+
const tsMissRef = useRef(0);
|
|
115
|
+
// Browser-side reachable cache (independent of backend DNS quirks)
|
|
116
|
+
const tunnelClientReachableRef = useRef(false);
|
|
117
|
+
const tsClientReachableRef = useRef(false);
|
|
118
|
+
// Track whether reachable=true was ever observed in this session.
|
|
119
|
+
// Distinguishes "Checking..." (initial cold cache) from "Reconnecting..." (lost connection).
|
|
120
|
+
const tunnelEverReachableRef = useRef(false);
|
|
121
|
+
const tsEverReachableRef = useRef(false);
|
|
122
|
+
const [tunnelEverReachable, setTunnelEverReachable] = useState(false);
|
|
123
|
+
const [tsEverReachable, setTsEverReachable] = useState(false);
|
|
124
|
+
|
|
125
|
+
// API key visibility toggle state
|
|
126
|
+
const [visibleKeys, setVisibleKeys] = useState(new Set());
|
|
127
|
+
|
|
128
|
+
// Client-side local/remote detection (UI hint only, not a security gate)
|
|
129
|
+
const [isRemoteHost, setIsRemoteHost] = useState(false);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (typeof window !== "undefined")
|
|
132
|
+
setIsRemoteHost(!["localhost", "127.0.0.1", "::1"].includes(window.location.hostname));
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
// Track app UI locale to gate wenyan caveman levels
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
setLocale(getCurrentLocale());
|
|
138
|
+
return onLocaleChange(() => setLocale(getCurrentLocale()));
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
const isWenyanLocale = WENYAN_LOCALES.includes(locale);
|
|
142
|
+
const visibleCavemanLevels = isWenyanLocale
|
|
143
|
+
? CAVEMAN_LEVELS
|
|
144
|
+
: CAVEMAN_LEVELS.filter((lvl) => !lvl.wenyan);
|
|
145
|
+
|
|
146
|
+
// Reset wenyan level to "ultra" when leaving a Chinese locale
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const current = CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel);
|
|
149
|
+
if (current?.wenyan && !isWenyanLocale) {
|
|
150
|
+
setCavemanLevel("ultra");
|
|
151
|
+
patchSetting({ cavemanLevel: "ultra" });
|
|
152
|
+
}
|
|
153
|
+
}, [isWenyanLocale, cavemanLevel]);
|
|
154
|
+
|
|
155
|
+
const { copied, copy } = useCopyToClipboard();
|
|
156
|
+
|
|
157
|
+
// Security gate: block remote exposure while dashboard uses default password or login is off.
|
|
158
|
+
const isLoginUnsafe = !requireLogin || !hasPassword;
|
|
159
|
+
const unsafeReason = !requireLogin
|
|
160
|
+
? "Enable \"Require login\" and set a custom password before activating the tunnel."
|
|
161
|
+
: "Change the default dashboard password before activating the tunnel.";
|
|
162
|
+
|
|
163
|
+
// Auto-scroll install log
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (tsLogRef.current) tsLogRef.current.scrollTop = tsLogRef.current.scrollHeight;
|
|
166
|
+
}, [tsInstallLog]);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
fetchData();
|
|
170
|
+
loadSettings();
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
// Status poll: only while degraded (not yet reachable). Stop once healthy to avoid spam.
|
|
174
|
+
// Visibility re-check: refresh once when tab becomes visible.
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const anyEnabled = tunnelEnabled || tsEnabled;
|
|
177
|
+
if (!anyEnabled) return;
|
|
178
|
+
const tunnelHealthy = !tunnelEnabled || tunnelReachable;
|
|
179
|
+
const tsHealthy = !tsEnabled || tsReachable;
|
|
180
|
+
const allHealthy = tunnelHealthy && tsHealthy;
|
|
181
|
+
const onVisible = () => { if (!document.hidden) syncTunnelStatus(); };
|
|
182
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
183
|
+
if (allHealthy) return () => document.removeEventListener("visibilitychange", onVisible);
|
|
184
|
+
const timer = setInterval(() => { if (!document.hidden) syncTunnelStatus(); }, STATUS_POLL_FAST_MS);
|
|
185
|
+
return () => {
|
|
186
|
+
clearInterval(timer);
|
|
187
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
188
|
+
};
|
|
189
|
+
}, [tunnelEnabled, tsEnabled, tunnelReachable, tsReachable]);
|
|
190
|
+
|
|
191
|
+
// Browser-side periodic ping: probes tunnel/tailscale URLs directly so UI stays
|
|
192
|
+
// "reachable" even when backend DNS (1.1.1.1) hiccups on *.ts.net or *.trycloudflare.com.
|
|
193
|
+
// Adaptive: slow when healthy, fast when degraded; pause when tab hidden.
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const probeBoth = async () => {
|
|
196
|
+
if (document.hidden) return;
|
|
197
|
+
if (tunnelEnabled && (tunnelUrl || tunnelPublicUrl)) {
|
|
198
|
+
const ok = await clientPingAny(tunnelPublicUrl, tunnelUrl);
|
|
199
|
+
tunnelClientReachableRef.current = ok;
|
|
200
|
+
if (ok) { tunnelMissRef.current = 0; setTunnelReachable(true); if (!tunnelEverReachableRef.current) { tunnelEverReachableRef.current = true; setTunnelEverReachable(true); } }
|
|
201
|
+
else { tunnelMissRef.current += 1; if (tunnelMissRef.current >= REACHABLE_MISS_THRESHOLD) setTunnelReachable(false); }
|
|
202
|
+
} else {
|
|
203
|
+
tunnelClientReachableRef.current = false;
|
|
204
|
+
}
|
|
205
|
+
if (tsEnabled && tsUrl) {
|
|
206
|
+
const ok = await clientPingUrl(tsUrl);
|
|
207
|
+
tsClientReachableRef.current = ok;
|
|
208
|
+
if (ok) { tsMissRef.current = 0; setTsReachable(true); if (!tsEverReachableRef.current) { tsEverReachableRef.current = true; setTsEverReachable(true); } }
|
|
209
|
+
else { tsMissRef.current += 1; if (tsMissRef.current >= REACHABLE_MISS_THRESHOLD) setTsReachable(false); }
|
|
210
|
+
} else {
|
|
211
|
+
tsClientReachableRef.current = false;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const anyEnabled = (tunnelEnabled && (tunnelUrl || tunnelPublicUrl)) || (tsEnabled && tsUrl);
|
|
215
|
+
if (!anyEnabled) return;
|
|
216
|
+
probeBoth();
|
|
217
|
+
const tunnelHealthy = !tunnelEnabled || tunnelReachable;
|
|
218
|
+
const tsHealthy = !tsEnabled || tsReachable;
|
|
219
|
+
if (tunnelHealthy && tsHealthy) return;
|
|
220
|
+
const id = setInterval(probeBoth, CLIENT_PING_FAST_MS);
|
|
221
|
+
return () => clearInterval(id);
|
|
222
|
+
}, [tunnelEnabled, tunnelUrl, tunnelPublicUrl, tsEnabled, tsUrl, tunnelReachable, tsReachable]);
|
|
223
|
+
|
|
224
|
+
// Client-side reachable only (server no longer probes; watchdog handles backend health).
|
|
225
|
+
// Miss-debounce: only flip to false after N consecutive misses.
|
|
226
|
+
const updateReachable = useCallback((_unused, clientRef, missRef, setter, everRef, everSetter) => {
|
|
227
|
+
const reachable = clientRef.current;
|
|
228
|
+
if (reachable) {
|
|
229
|
+
missRef.current = 0;
|
|
230
|
+
setter(true);
|
|
231
|
+
if (!everRef.current) {
|
|
232
|
+
everRef.current = true;
|
|
233
|
+
everSetter(true);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
missRef.current += 1;
|
|
237
|
+
if (missRef.current >= REACHABLE_MISS_THRESHOLD) setter(false);
|
|
238
|
+
}
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
// Trust user intent (settingsEnabled): UI stays "enabled" while watchdog restarts process
|
|
242
|
+
const syncTunnelStatus = async () => {
|
|
243
|
+
try {
|
|
244
|
+
const statusRes = await fetch("/api/tunnel/status", { cache: "no-store" });
|
|
245
|
+
if (!statusRes.ok) return;
|
|
246
|
+
const data = await statusRes.json();
|
|
247
|
+
const tEnabled = data.tunnel?.settingsEnabled ?? data.tunnel?.enabled ?? false;
|
|
248
|
+
const tUrl = data.tunnel?.tunnelUrl || "";
|
|
249
|
+
setTunnelUrl(tUrl);
|
|
250
|
+
setTunnelPublicUrl(data.tunnel?.publicUrl || "");
|
|
251
|
+
setTunnelEnabled(tEnabled);
|
|
252
|
+
updateReachable(null, tunnelClientReachableRef, tunnelMissRef, setTunnelReachable, tunnelEverReachableRef, setTunnelEverReachable);
|
|
253
|
+
|
|
254
|
+
const tsEn = data.tailscale?.settingsEnabled ?? data.tailscale?.enabled ?? false;
|
|
255
|
+
const tsUrlVal = data.tailscale?.tunnelUrl || "";
|
|
256
|
+
setTsUrl(tsUrlVal);
|
|
257
|
+
setTsEnabled(tsEn);
|
|
258
|
+
updateReachable(null, tsClientReachableRef, tsMissRef, setTsReachable, tsEverReachableRef, setTsEverReachable);
|
|
259
|
+
} catch { /* ignore poll errors */ }
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const loadSettings = async () => {
|
|
263
|
+
setTunnelChecking(true);
|
|
264
|
+
try {
|
|
265
|
+
const [settingsRes, statusRes] = await Promise.all([
|
|
266
|
+
fetch("/api/settings"),
|
|
267
|
+
fetch("/api/tunnel/status", { cache: "no-store" })
|
|
268
|
+
]);
|
|
269
|
+
if (settingsRes.ok) {
|
|
270
|
+
const data = await settingsRes.json();
|
|
271
|
+
setRequireApiKey(data.requireApiKey || false);
|
|
272
|
+
setRequireLogin(data.requireLogin !== false);
|
|
273
|
+
setHasPassword(data.hasPassword || false);
|
|
274
|
+
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
|
|
275
|
+
setRtkEnabledState(data.rtkEnabled !== false);
|
|
276
|
+
setCavemanEnabled(!!data.cavemanEnabled);
|
|
277
|
+
setCavemanLevel(data.cavemanLevel || "full");
|
|
278
|
+
}
|
|
279
|
+
if (statusRes.ok) {
|
|
280
|
+
const data = await statusRes.json();
|
|
281
|
+
const tEnabled = data.tunnel?.settingsEnabled ?? data.tunnel?.enabled ?? false;
|
|
282
|
+
const tUrl = data.tunnel?.tunnelUrl || "";
|
|
283
|
+
setTunnelUrl(tUrl);
|
|
284
|
+
setTunnelPublicUrl(data.tunnel?.publicUrl || "");
|
|
285
|
+
setTunnelEnabled(tEnabled);
|
|
286
|
+
updateReachable(null, tunnelClientReachableRef, tunnelMissRef, setTunnelReachable, tunnelEverReachableRef, setTunnelEverReachable);
|
|
287
|
+
|
|
288
|
+
const tsEn = data.tailscale?.settingsEnabled ?? data.tailscale?.enabled ?? false;
|
|
289
|
+
const tsUrlVal = data.tailscale?.tunnelUrl || "";
|
|
290
|
+
setTsUrl(tsUrlVal);
|
|
291
|
+
setTsEnabled(tsEn);
|
|
292
|
+
updateReachable(null, tsClientReachableRef, tsMissRef, setTsReachable, tsEverReachableRef, setTsEverReachable);
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.log("Error loading settings:", error);
|
|
296
|
+
} finally {
|
|
297
|
+
setTunnelChecking(false);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const handleTunnelDashboardAccess = async (value) => {
|
|
302
|
+
try {
|
|
303
|
+
const res = await fetch("/api/settings", {
|
|
304
|
+
method: "PATCH",
|
|
305
|
+
headers: { "Content-Type": "application/json" },
|
|
306
|
+
body: JSON.stringify({ tunnelDashboardAccess: value }),
|
|
307
|
+
});
|
|
308
|
+
if (res.ok) setTunnelDashboardAccess(value);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.log("Error updating tunnelDashboardAccess:", error);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const handleRequireApiKey = async (value) => {
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch("/api/settings", {
|
|
317
|
+
method: "PATCH",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
body: JSON.stringify({ requireApiKey: value }),
|
|
320
|
+
});
|
|
321
|
+
if (res.ok) setRequireApiKey(value);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.log("Error updating requireApiKey:", error);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const handleRtkEnabled = async (value) => {
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch("/api/settings", {
|
|
330
|
+
method: "PATCH",
|
|
331
|
+
headers: { "Content-Type": "application/json" },
|
|
332
|
+
body: JSON.stringify({ rtkEnabled: value }),
|
|
333
|
+
});
|
|
334
|
+
if (res.ok) setRtkEnabledState(value);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.log("Error updating rtkEnabled:", error);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const patchSetting = async (patch) => {
|
|
341
|
+
try {
|
|
342
|
+
await fetch("/api/settings", {
|
|
343
|
+
method: "PATCH",
|
|
344
|
+
headers: { "Content-Type": "application/json" },
|
|
345
|
+
body: JSON.stringify(patch),
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.log("Error updating setting:", error);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const handleCavemanEnabled = (value) => {
|
|
353
|
+
setCavemanEnabled(value);
|
|
354
|
+
patchSetting({ cavemanEnabled: value });
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const handleCavemanLevel = (level) => {
|
|
358
|
+
setCavemanLevel(level);
|
|
359
|
+
patchSetting({ cavemanLevel: level });
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const fetchData = async () => {
|
|
363
|
+
try {
|
|
364
|
+
const keysRes = await fetch("/api/keys");
|
|
365
|
+
const keysData = await keysRes.json();
|
|
366
|
+
if (keysRes.ok) {
|
|
367
|
+
setKeys(keysData.keys || []);
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.log("Error fetching data:", error);
|
|
371
|
+
} finally {
|
|
372
|
+
setLoading(false);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// u2500u2500u2500 Cloudflare Tunnel handlers
|
|
377
|
+
// Ping tunnel health until reachable. Race multiple URLs (shortlink + direct) — 1 OK is enough.
|
|
378
|
+
const pingTunnelHealth = async (...urls) => {
|
|
379
|
+
setTunnelLoading(true);
|
|
380
|
+
setTunnelProgress("Waiting for tunnel ready...");
|
|
381
|
+
const targets = urls.filter(Boolean).map((u) => `${u}/api/health`);
|
|
382
|
+
const start = Date.now();
|
|
383
|
+
while (Date.now() - start < TUNNEL_PING_MAX_MS) {
|
|
384
|
+
await new Promise((r) => setTimeout(r, TUNNEL_PING_INTERVAL_MS));
|
|
385
|
+
const ok = await Promise.any(targets.map(async (h) => {
|
|
386
|
+
const p = await fetch(h, { mode: "cors", cache: "no-store" });
|
|
387
|
+
if (p.ok) return true;
|
|
388
|
+
throw new Error("not ready");
|
|
389
|
+
})).catch(() => false);
|
|
390
|
+
if (ok) {
|
|
391
|
+
setTunnelEnabled(true);
|
|
392
|
+
setTunnelLoading(false);
|
|
393
|
+
setTunnelProgress("");
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
// Every 5 pings (~10s), check if backend process still alive
|
|
397
|
+
if ((Date.now() - start) % 10000 < TUNNEL_PING_INTERVAL_MS) {
|
|
398
|
+
try {
|
|
399
|
+
const statusRes = await fetch("/api/tunnel/status");
|
|
400
|
+
if (statusRes.ok) {
|
|
401
|
+
const status = await statusRes.json();
|
|
402
|
+
if (!status.tunnel?.enabled) {
|
|
403
|
+
setTunnelStatus({ type: "error", message: "Tunnel process stopped unexpectedly." });
|
|
404
|
+
setTunnelLoading(false);
|
|
405
|
+
setTunnelProgress("");
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch { /* ignore */ }
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
setTunnelStatus({ type: "error", message: "Tunnel created but not reachable. Please try again." });
|
|
413
|
+
setTunnelLoading(false);
|
|
414
|
+
setTunnelProgress("");
|
|
415
|
+
return false;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const handleEnableTunnel = async () => {
|
|
419
|
+
setShowEnableTunnelModal(false);
|
|
420
|
+
setTunnelLoading(true);
|
|
421
|
+
setTunnelStatus(null);
|
|
422
|
+
setTunnelProgress("Creating tunnel...");
|
|
423
|
+
|
|
424
|
+
// Poll download progress while enable request is pending
|
|
425
|
+
let polling = true;
|
|
426
|
+
const pollProgress = async () => {
|
|
427
|
+
while (polling) {
|
|
428
|
+
try {
|
|
429
|
+
const r = await fetch("/api/tunnel/status");
|
|
430
|
+
if (r.ok) {
|
|
431
|
+
const s = await r.json();
|
|
432
|
+
if (s.download?.downloading) {
|
|
433
|
+
setTunnelProgress(`Downloading cloudflared... ${s.download.progress}%`);
|
|
434
|
+
} else if (polling) {
|
|
435
|
+
setTunnelProgress("Creating tunnel...");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch { /* ignore */ }
|
|
439
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
pollProgress();
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const res = await fetch("/api/tunnel/enable", { method: "POST" });
|
|
446
|
+
polling = false;
|
|
447
|
+
const data = await res.json();
|
|
448
|
+
if (!res.ok) {
|
|
449
|
+
setTunnelStatus({ type: "error", message: data.error || "Failed to enable tunnel" });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const url = data.tunnelUrl;
|
|
454
|
+
if (!url) {
|
|
455
|
+
setTunnelStatus({ type: "error", message: "No tunnel URL returned" });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
setTunnelUrl(url);
|
|
460
|
+
setTunnelPublicUrl(data.publicUrl || "");
|
|
461
|
+
await pingTunnelHealth(data.publicUrl, url);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
setTunnelStatus({ type: "error", message: error.message });
|
|
464
|
+
} finally {
|
|
465
|
+
polling = false;
|
|
466
|
+
setTunnelLoading(false);
|
|
467
|
+
setTunnelProgress("");
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const handleDisableTunnel = async () => {
|
|
472
|
+
setTunnelLoading(true);
|
|
473
|
+
setTunnelStatus(null);
|
|
474
|
+
try {
|
|
475
|
+
const res = await fetch("/api/tunnel/disable", { method: "POST" });
|
|
476
|
+
const data = await res.json();
|
|
477
|
+
if (res.ok) {
|
|
478
|
+
setTunnelEnabled(false);
|
|
479
|
+
setTunnelUrl("");
|
|
480
|
+
setShowDisableTunnelModal(false);
|
|
481
|
+
setTunnelStatus({ type: "success", message: "Tunnel disabled" });
|
|
482
|
+
} else {
|
|
483
|
+
setTunnelStatus({ type: "error", message: data.error || "Failed to disable tunnel" });
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
setTunnelStatus({ type: "error", message: error.message });
|
|
487
|
+
} finally {
|
|
488
|
+
setTunnelLoading(false);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// u2500u2500u2500 Tailscale handlers
|
|
493
|
+
const checkTailscaleInstalled = async () => {
|
|
494
|
+
setTsInstalled(null);
|
|
495
|
+
try {
|
|
496
|
+
const res = await fetch("/api/tunnel/tailscale-check");
|
|
497
|
+
if (res.ok) {
|
|
498
|
+
const data = await res.json();
|
|
499
|
+
setTsInstalled(data.installed);
|
|
500
|
+
return data;
|
|
501
|
+
}
|
|
502
|
+
} catch { /* ignore */ }
|
|
503
|
+
setTsInstalled(false);
|
|
504
|
+
return { installed: false };
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const handleInstallTailscale = async () => {
|
|
508
|
+
setTsInstalling(true);
|
|
509
|
+
setTsStatus(null);
|
|
510
|
+
setTsInstallLog([]);
|
|
511
|
+
try {
|
|
512
|
+
const res = await fetch("/api/tunnel/tailscale-install", {
|
|
513
|
+
method: "POST",
|
|
514
|
+
headers: { "Content-Type": "application/json" },
|
|
515
|
+
body: JSON.stringify({ sudoPassword: tsSudoPassword }),
|
|
516
|
+
});
|
|
517
|
+
setTsSudoPassword("");
|
|
518
|
+
|
|
519
|
+
const reader = res.body.getReader();
|
|
520
|
+
const decoder = new TextDecoder();
|
|
521
|
+
let buffer = "";
|
|
522
|
+
|
|
523
|
+
while (true) {
|
|
524
|
+
const { done, value } = await reader.read();
|
|
525
|
+
if (done) break;
|
|
526
|
+
buffer += decoder.decode(value, { stream: true });
|
|
527
|
+
const parts = buffer.split("\n\n");
|
|
528
|
+
buffer = parts.pop() || "";
|
|
529
|
+
for (const part of parts) {
|
|
530
|
+
const lines = part.split("\n");
|
|
531
|
+
let event = "progress";
|
|
532
|
+
let data = null;
|
|
533
|
+
for (const line of lines) {
|
|
534
|
+
if (line.startsWith("event: ")) event = line.slice(7).trim();
|
|
535
|
+
if (line.startsWith("data: ")) {
|
|
536
|
+
try { data = JSON.parse(line.slice(6)); } catch { /* skip */ }
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (!data) continue;
|
|
540
|
+
if (event === "progress") {
|
|
541
|
+
setTsInstallLog((prev) => [...prev.slice(-50), data.message]);
|
|
542
|
+
} else if (event === "done") {
|
|
543
|
+
setTsInstalled(true);
|
|
544
|
+
setTsInstalling(false);
|
|
545
|
+
setShowTsModal(false);
|
|
546
|
+
handleConnectTailscale();
|
|
547
|
+
return;
|
|
548
|
+
} else if (event === "error") {
|
|
549
|
+
setTsStatus({ type: "error", message: data.error || "Install failed" });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} catch (e) {
|
|
554
|
+
setTsStatus({ type: "error", message: e.message });
|
|
555
|
+
} finally {
|
|
556
|
+
setTsInstalling(false);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Ping Tailscale health until reachable
|
|
561
|
+
const pingTsHealth = async (url) => {
|
|
562
|
+
setTsProgress("Waiting for Tailscale ready...");
|
|
563
|
+
const healthUrl = `${url}/api/health`;
|
|
564
|
+
const start = Date.now();
|
|
565
|
+
while (Date.now() - start < TUNNEL_PING_MAX_MS) {
|
|
566
|
+
await new Promise((r) => setTimeout(r, TUNNEL_PING_INTERVAL_MS));
|
|
567
|
+
try {
|
|
568
|
+
const ping = await fetch(healthUrl, { mode: "no-cors", cache: "no-store" });
|
|
569
|
+
if (ping.ok || ping.type === "opaque") return true;
|
|
570
|
+
} catch { /* not ready yet */ }
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// Show inline login button instead of auto-opening popup (browsers block popups
|
|
576
|
+
// opened after async work because the user gesture is lost).
|
|
577
|
+
const requestUserAuth = (url, label) => {
|
|
578
|
+
setTsAuthUrl(url);
|
|
579
|
+
setTsAuthLabel(label);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const clearUserAuth = () => {
|
|
583
|
+
setTsAuthUrl("");
|
|
584
|
+
setTsAuthLabel("");
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const handleConnectTailscale = async () => {
|
|
588
|
+
setShowTsModal(false);
|
|
589
|
+
setTsConnecting(true);
|
|
590
|
+
setTsLoading(true);
|
|
591
|
+
setTsStatus(null);
|
|
592
|
+
setTsProgress("Connecting...");
|
|
593
|
+
clearUserAuth();
|
|
594
|
+
try {
|
|
595
|
+
const res = await fetch("/api/tunnel/tailscale-enable", { method: "POST" });
|
|
596
|
+
const data = await res.json();
|
|
597
|
+
|
|
598
|
+
if (res.ok && data.success) {
|
|
599
|
+
setTsUrl(data.tunnelUrl || "");
|
|
600
|
+
const reachable = await pingTsHealth(data.tunnelUrl);
|
|
601
|
+
setTsEnabled(true);
|
|
602
|
+
setTsStatus(reachable ? null : { type: "warning", message: "Connected but not reachable yet." });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (data.needsLogin && data.authUrl) {
|
|
607
|
+
requestUserAuth(data.authUrl, "Open Login Page");
|
|
608
|
+
setTsProgress("Login required — click \"Open Login Page\" to continue");
|
|
609
|
+
for (let i = 0; i < 40; i++) {
|
|
610
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
611
|
+
try {
|
|
612
|
+
const r2 = await fetch("/api/tunnel/tailscale-check");
|
|
613
|
+
if (r2.ok) {
|
|
614
|
+
const check = await r2.json();
|
|
615
|
+
if (check.loggedIn) {
|
|
616
|
+
clearUserAuth();
|
|
617
|
+
setTsProgress("Starting funnel...");
|
|
618
|
+
const res2 = await fetch("/api/tunnel/tailscale-enable", { method: "POST" });
|
|
619
|
+
const data2 = await res2.json();
|
|
620
|
+
if (res2.ok && data2.success) {
|
|
621
|
+
setTsUrl(data2.tunnelUrl || "");
|
|
622
|
+
const ok2 = await pingTsHealth(data2.tunnelUrl);
|
|
623
|
+
setTsEnabled(true);
|
|
624
|
+
setTsStatus(ok2 ? null : { type: "warning", message: "Connected but not reachable yet." });
|
|
625
|
+
} else if (data2.funnelNotEnabled && data2.enableUrl) {
|
|
626
|
+
await pollFunnelEnable(data2.enableUrl);
|
|
627
|
+
} else {
|
|
628
|
+
setTsStatus({ type: "error", message: data2.error || "Failed to start funnel" });
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch { /* retry */ }
|
|
634
|
+
}
|
|
635
|
+
clearUserAuth();
|
|
636
|
+
setTsStatus({ type: "error", message: "Login timed out. Please try again." });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (data.funnelNotEnabled && data.enableUrl) {
|
|
641
|
+
await pollFunnelEnable(data.enableUrl);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
setTsStatus({ type: "error", message: data.error || "Failed to connect" });
|
|
646
|
+
} catch (error) {
|
|
647
|
+
setTsStatus({ type: "error", message: error.message });
|
|
648
|
+
} finally {
|
|
649
|
+
setTsLoading(false);
|
|
650
|
+
setTsConnecting(false);
|
|
651
|
+
setTsProgress("");
|
|
652
|
+
clearUserAuth();
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const pollFunnelEnable = async (enableUrl) => {
|
|
657
|
+
requestUserAuth(enableUrl, "Open Funnel Settings");
|
|
658
|
+
setTsProgress("Click \"Open Funnel Settings\" to enable Funnel...");
|
|
659
|
+
for (let i = 0; i < 40; i++) {
|
|
660
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
661
|
+
try {
|
|
662
|
+
const res = await fetch("/api/tunnel/tailscale-enable", { method: "POST" });
|
|
663
|
+
const data = await res.json();
|
|
664
|
+
if (res.ok && data.success) {
|
|
665
|
+
clearUserAuth();
|
|
666
|
+
setTsUrl(data.tunnelUrl || "");
|
|
667
|
+
const ok3 = await pingTsHealth(data.tunnelUrl);
|
|
668
|
+
setTsEnabled(true);
|
|
669
|
+
setTsStatus(ok3 ? null : { type: "warning", message: "Connected but not reachable yet." });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (data.funnelNotEnabled) continue;
|
|
673
|
+
if (data.error) {
|
|
674
|
+
clearUserAuth();
|
|
675
|
+
setTsStatus({ type: "error", message: data.error });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
} catch { /* retry */ }
|
|
679
|
+
}
|
|
680
|
+
clearUserAuth();
|
|
681
|
+
setTsStatus({ type: "error", message: "Timed out waiting for Funnel to be enabled." });
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const handleDisableTailscale = async () => {
|
|
685
|
+
setTsLoading(true);
|
|
686
|
+
setTsStatus(null);
|
|
687
|
+
try {
|
|
688
|
+
const res = await fetch("/api/tunnel/tailscale-disable", { method: "POST" });
|
|
689
|
+
const data = await res.json();
|
|
690
|
+
if (res.ok) {
|
|
691
|
+
setTsEnabled(false);
|
|
692
|
+
setTsUrl("");
|
|
693
|
+
setShowDisableTsModal(false);
|
|
694
|
+
setTsStatus({ type: "success", message: "Tailscale disabled" });
|
|
695
|
+
} else {
|
|
696
|
+
setTsStatus({ type: "error", message: data.error || "Failed to disable Tailscale" });
|
|
697
|
+
}
|
|
698
|
+
} catch (e) {
|
|
699
|
+
setTsStatus({ type: "error", message: e.message });
|
|
700
|
+
} finally {
|
|
701
|
+
setTsLoading(false);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const handleOpenTsModal = async () => {
|
|
706
|
+
setTsStatus(null);
|
|
707
|
+
setTsInstallLog([]);
|
|
708
|
+
const data = await checkTailscaleInstalled();
|
|
709
|
+
if (data?.installed && data?.hasCachedPassword) {
|
|
710
|
+
handleConnectTailscale();
|
|
711
|
+
} else {
|
|
712
|
+
setShowTsModal(true);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const handleCreateKey = async () => {
|
|
717
|
+
if (!newKeyName.trim()) return;
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
const res = await fetch("/api/keys", {
|
|
721
|
+
method: "POST",
|
|
722
|
+
headers: { "Content-Type": "application/json" },
|
|
723
|
+
body: JSON.stringify({ name: newKeyName }),
|
|
724
|
+
});
|
|
725
|
+
const data = await res.json();
|
|
726
|
+
|
|
727
|
+
if (res.ok) {
|
|
728
|
+
setCreatedKey(data.key);
|
|
729
|
+
await fetchData();
|
|
730
|
+
setNewKeyName("");
|
|
731
|
+
setShowAddModal(false);
|
|
732
|
+
}
|
|
733
|
+
} catch (error) {
|
|
734
|
+
console.log("Error creating key:", error);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const handleDeleteKey = async (id) => {
|
|
739
|
+
setConfirmState({
|
|
740
|
+
title: "Delete API Key",
|
|
741
|
+
message: "Delete this API key?",
|
|
742
|
+
onConfirm: async () => {
|
|
743
|
+
setConfirmState(null);
|
|
744
|
+
try {
|
|
745
|
+
const res = await fetch(`/api/keys/${id}`, { method: "DELETE" });
|
|
746
|
+
if (res.ok) {
|
|
747
|
+
setKeys(keys.filter((k) => k.id !== id));
|
|
748
|
+
setVisibleKeys(prev => {
|
|
749
|
+
const next = new Set(prev);
|
|
750
|
+
next.delete(id);
|
|
751
|
+
return next;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.log("Error deleting key:", error);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const handleToggleKey = async (id, isActive) => {
|
|
762
|
+
try {
|
|
763
|
+
const res = await fetch(`/api/keys/${id}`, {
|
|
764
|
+
method: "PUT",
|
|
765
|
+
headers: { "Content-Type": "application/json" },
|
|
766
|
+
body: JSON.stringify({ isActive }),
|
|
767
|
+
});
|
|
768
|
+
if (res.ok) {
|
|
769
|
+
setKeys(prev => prev.map(k => k.id === id ? { ...k, isActive } : k));
|
|
770
|
+
}
|
|
771
|
+
} catch (error) {
|
|
772
|
+
console.log("Error toggling key:", error);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const maskKey = (fullKey) => {
|
|
777
|
+
if (!fullKey) return "";
|
|
778
|
+
return fullKey.length > 8 ? fullKey.slice(0, 8) + "..." : fullKey;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const toggleKeyVisibility = (keyId) => {
|
|
782
|
+
setVisibleKeys(prev => {
|
|
783
|
+
const next = new Set(prev);
|
|
784
|
+
if (next.has(keyId)) next.delete(keyId);
|
|
785
|
+
else next.add(keyId);
|
|
786
|
+
return next;
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const [baseUrl, setBaseUrl] = useState("/v1");
|
|
791
|
+
|
|
792
|
+
// Hydration fix: Only access window on client side
|
|
793
|
+
useEffect(() => {
|
|
794
|
+
if (typeof window !== "undefined") {
|
|
795
|
+
setBaseUrl(`${window.location.origin}/v1`);
|
|
796
|
+
}
|
|
797
|
+
}, []);
|
|
798
|
+
|
|
799
|
+
if (loading) {
|
|
800
|
+
return (
|
|
801
|
+
<div className="flex flex-col gap-8">
|
|
802
|
+
<CardSkeleton />
|
|
803
|
+
<CardSkeleton />
|
|
804
|
+
</div>
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const currentEndpoint = baseUrl;
|
|
809
|
+
|
|
810
|
+
return (
|
|
811
|
+
<div className="flex flex-col gap-8">
|
|
812
|
+
{/* Endpoint Card */}
|
|
813
|
+
<Card>
|
|
814
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
815
|
+
<span className="material-symbols-outlined text-primary">api</span>
|
|
816
|
+
API Endpoint
|
|
817
|
+
</h2>
|
|
818
|
+
|
|
819
|
+
{/* Endpoint rows */}
|
|
820
|
+
<div className="flex flex-col gap-2">
|
|
821
|
+
{/* Local */}
|
|
822
|
+
<EndpointRow
|
|
823
|
+
label="Local"
|
|
824
|
+
url={currentEndpoint}
|
|
825
|
+
copyId="local_url"
|
|
826
|
+
copied={copied}
|
|
827
|
+
onCopy={copy}
|
|
828
|
+
/>
|
|
829
|
+
{/* Cloudflare Tunnel */}
|
|
830
|
+
<div className="flex items-center gap-2">
|
|
831
|
+
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
|
832
|
+
tunnelEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
|
833
|
+
}`}>Tunnel</span>
|
|
834
|
+
{tunnelEnabled && !tunnelLoading && tunnelReachable ? (
|
|
835
|
+
<>
|
|
836
|
+
<Input value={`${tunnelPublicUrl || tunnelUrl}/v1`} readOnly className="flex-1 font-mono text-sm" />
|
|
837
|
+
<button
|
|
838
|
+
onClick={() => copy(`${tunnelPublicUrl || tunnelUrl}/v1`, "tunnel_url")}
|
|
839
|
+
className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
|
|
840
|
+
>
|
|
841
|
+
<span className="material-symbols-outlined text-[18px]">{copied === "tunnel_url" ? "check" : "content_copy"}</span>
|
|
842
|
+
</button>
|
|
843
|
+
<button
|
|
844
|
+
onClick={() => setShowDisableTunnelModal(true)}
|
|
845
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
846
|
+
title="Disable Tunnel"
|
|
847
|
+
>
|
|
848
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
849
|
+
</button>
|
|
850
|
+
</>
|
|
851
|
+
) : tunnelEnabled && !tunnelLoading && !tunnelReachable ? (
|
|
852
|
+
<>
|
|
853
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-amber-300 dark:border-amber-800 bg-amber-500/5 text-sm text-amber-600 dark:text-amber-400">
|
|
854
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
855
|
+
{tunnelEverReachable ? "Tunnel reconnecting..." : "Tunnel checking..."}
|
|
856
|
+
</div>
|
|
857
|
+
<button
|
|
858
|
+
onClick={() => setShowDisableTunnelModal(true)}
|
|
859
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
860
|
+
title="Disable Tunnel"
|
|
861
|
+
>
|
|
862
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
863
|
+
</button>
|
|
864
|
+
</>
|
|
865
|
+
) : tunnelLoading ? (
|
|
866
|
+
<>
|
|
867
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
|
|
868
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
869
|
+
{tunnelProgress || "Creating tunnel..."}
|
|
870
|
+
</div>
|
|
871
|
+
<button
|
|
872
|
+
onClick={() => { setTunnelLoading(false); setTunnelProgress(""); }}
|
|
873
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
874
|
+
title="Stop"
|
|
875
|
+
>
|
|
876
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
877
|
+
</button>
|
|
878
|
+
</>
|
|
879
|
+
) : tunnelStatus?.type === "error" ? (
|
|
880
|
+
<>
|
|
881
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400">
|
|
882
|
+
<span className="material-symbols-outlined text-sm">error</span>
|
|
883
|
+
{tunnelStatus.message}
|
|
884
|
+
</div>
|
|
885
|
+
<Button size="sm" icon="cloud_upload" onClick={() => setShowEnableTunnelModal(true)}>Enable</Button>
|
|
886
|
+
</>
|
|
887
|
+
) : tunnelChecking ? (
|
|
888
|
+
<>
|
|
889
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
|
|
890
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
891
|
+
Checking...
|
|
892
|
+
</div>
|
|
893
|
+
<button
|
|
894
|
+
onClick={() => setTunnelChecking(false)}
|
|
895
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
896
|
+
title="Stop"
|
|
897
|
+
>
|
|
898
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
899
|
+
</button>
|
|
900
|
+
</>
|
|
901
|
+
) : (
|
|
902
|
+
<Button
|
|
903
|
+
size="sm"
|
|
904
|
+
icon="cloud_upload"
|
|
905
|
+
onClick={() => {
|
|
906
|
+
if (isLoginUnsafe) {
|
|
907
|
+
setTunnelStatus({ type: "error", message: `Security required: ${unsafeReason}` });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!requireApiKey) {
|
|
911
|
+
setTunnelStatus({ type: "error", message: "Security required: Enable \"Require API key\" before activating the tunnel." });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
setShowEnableTunnelModal(true);
|
|
915
|
+
}}
|
|
916
|
+
>
|
|
917
|
+
Enable
|
|
918
|
+
</Button>
|
|
919
|
+
)}
|
|
920
|
+
</div>
|
|
921
|
+
{/* Tailscale */}
|
|
922
|
+
<div className="flex items-center gap-2">
|
|
923
|
+
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
|
924
|
+
tsEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
|
925
|
+
}`}>Tailscale</span>
|
|
926
|
+
{tsEnabled && !tsLoading && tsReachable ? (
|
|
927
|
+
<>
|
|
928
|
+
<Input value={`${tsUrl}/v1`} readOnly className="flex-1 font-mono text-sm" />
|
|
929
|
+
<button
|
|
930
|
+
onClick={() => copy(`${tsUrl}/v1`, "ts_url")}
|
|
931
|
+
className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
|
|
932
|
+
>
|
|
933
|
+
<span className="material-symbols-outlined text-[18px]">{copied === "ts_url" ? "check" : "content_copy"}</span>
|
|
934
|
+
</button>
|
|
935
|
+
<button
|
|
936
|
+
onClick={() => setShowDisableTsModal(true)}
|
|
937
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
938
|
+
title="Disable Tailscale"
|
|
939
|
+
>
|
|
940
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
941
|
+
</button>
|
|
942
|
+
</>
|
|
943
|
+
) : tsEnabled && !tsLoading && !tsReachable ? (
|
|
944
|
+
<>
|
|
945
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-amber-300 dark:border-amber-800 bg-amber-500/5 text-sm text-amber-600 dark:text-amber-400">
|
|
946
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
947
|
+
{tsEverReachable ? "Tailscale reconnecting..." : "Tailscale checking..."}
|
|
948
|
+
</div>
|
|
949
|
+
<button
|
|
950
|
+
onClick={() => setShowDisableTsModal(true)}
|
|
951
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
952
|
+
title="Disable Tailscale"
|
|
953
|
+
>
|
|
954
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
955
|
+
</button>
|
|
956
|
+
</>
|
|
957
|
+
) : (tsLoading || tsConnecting) ? (
|
|
958
|
+
<>
|
|
959
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
|
|
960
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
961
|
+
{tsProgress || "Connecting..."}
|
|
962
|
+
</div>
|
|
963
|
+
{tsAuthUrl && (
|
|
964
|
+
<Button
|
|
965
|
+
size="sm"
|
|
966
|
+
icon="open_in_new"
|
|
967
|
+
onClick={() => window.open(tsAuthUrl, "tailscale_auth", "width=600,height=700,noopener,noreferrer")}
|
|
968
|
+
>
|
|
969
|
+
{tsAuthLabel || "Open"}
|
|
970
|
+
</Button>
|
|
971
|
+
)}
|
|
972
|
+
<button
|
|
973
|
+
onClick={() => { setTsLoading(false); setTsConnecting(false); setTsProgress(""); clearUserAuth(); }}
|
|
974
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
|
975
|
+
title="Stop"
|
|
976
|
+
>
|
|
977
|
+
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
978
|
+
</button>
|
|
979
|
+
</>
|
|
980
|
+
) : tsStatus?.type === "error" ? (
|
|
981
|
+
<>
|
|
982
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400">
|
|
983
|
+
<span className="material-symbols-outlined text-sm">error</span>
|
|
984
|
+
{tsStatus.message}
|
|
985
|
+
</div>
|
|
986
|
+
<Button size="sm" icon="vpn_lock" onClick={handleOpenTsModal}>Enable</Button>
|
|
987
|
+
</>
|
|
988
|
+
) : (
|
|
989
|
+
<Button
|
|
990
|
+
size="sm"
|
|
991
|
+
icon="vpn_lock"
|
|
992
|
+
onClick={() => {
|
|
993
|
+
if (isLoginUnsafe) {
|
|
994
|
+
setTsStatus({ type: "error", message: `Security required: ${unsafeReason}` });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
handleOpenTsModal();
|
|
998
|
+
}}
|
|
999
|
+
className="bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!"
|
|
1000
|
+
>
|
|
1001
|
+
Enable
|
|
1002
|
+
</Button>
|
|
1003
|
+
)}
|
|
1004
|
+
</div>
|
|
1005
|
+
</div>
|
|
1006
|
+
|
|
1007
|
+
{/* Pre-enable security gate banner */}
|
|
1008
|
+
{isLoginUnsafe && !tunnelEnabled && !tsEnabled && (
|
|
1009
|
+
<div className="mt-4">
|
|
1010
|
+
<SecurityWarning
|
|
1011
|
+
message={unsafeReason}
|
|
1012
|
+
action={{ label: "Open settings", href: "/dashboard/profile" }}
|
|
1013
|
+
/>
|
|
1014
|
+
</div>
|
|
1015
|
+
)}
|
|
1016
|
+
|
|
1017
|
+
{/* Security warnings when tunnel or tailscale is active */}
|
|
1018
|
+
{(tunnelEnabled || tsEnabled) && (
|
|
1019
|
+
<div className="mt-4 flex flex-col gap-2">
|
|
1020
|
+
{!requireApiKey && (
|
|
1021
|
+
<SecurityWarning
|
|
1022
|
+
message="Require API key is disabled — your endpoint is publicly accessible without authentication."
|
|
1023
|
+
action={{ label: "Enable", href: "#require-api-key" }}
|
|
1024
|
+
/>
|
|
1025
|
+
)}
|
|
1026
|
+
{(!requireLogin || !hasPassword) && (
|
|
1027
|
+
<SecurityWarning
|
|
1028
|
+
message={
|
|
1029
|
+
!requireLogin
|
|
1030
|
+
? "Require login is disabled — anyone can access your dashboard via tunnel."
|
|
1031
|
+
: "Dashboard uses the default password — change it in Profile settings."
|
|
1032
|
+
}
|
|
1033
|
+
action={{
|
|
1034
|
+
label: !requireLogin ? "Enable" : "Change password",
|
|
1035
|
+
href: "/dashboard/profile",
|
|
1036
|
+
}}
|
|
1037
|
+
/>
|
|
1038
|
+
)}
|
|
1039
|
+
</div>
|
|
1040
|
+
)}
|
|
1041
|
+
|
|
1042
|
+
{/* Tunnel dashboard access option */}
|
|
1043
|
+
{(tunnelEnabled || tsEnabled) && (
|
|
1044
|
+
<div className="mt-4 pt-4 border-t border-border flex items-center gap-3">
|
|
1045
|
+
<Toggle
|
|
1046
|
+
checked={tunnelDashboardAccess}
|
|
1047
|
+
onChange={() => handleTunnelDashboardAccess(!tunnelDashboardAccess)}
|
|
1048
|
+
/>
|
|
1049
|
+
<div className="flex items-center gap-1.5">
|
|
1050
|
+
<p className="font-medium text-sm">Allow dashboard access via tunnel</p>
|
|
1051
|
+
<Tooltip text="When enabled, the dashboard can be accessed through your tunnel or Tailscale URL (login still required). When disabled, dashboard access via tunnel/Tailscale is completely blocked." />
|
|
1052
|
+
</div>
|
|
1053
|
+
</div>
|
|
1054
|
+
)}
|
|
1055
|
+
</Card>
|
|
1056
|
+
|
|
1057
|
+
{/* Token Saver (RTK + Caveman) */}
|
|
1058
|
+
<Card id="rtk">
|
|
1059
|
+
<div className="flex items-center justify-between mb-2">
|
|
1060
|
+
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
1061
|
+
<span className="material-symbols-outlined text-primary">bolt</span>
|
|
1062
|
+
Token Saver
|
|
1063
|
+
</h2>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div className="flex items-center justify-between pt-2 pb-4 border-b border-border gap-4">
|
|
1066
|
+
<div className="min-w-0 flex-1">
|
|
1067
|
+
<p className="font-medium">
|
|
1068
|
+
Compress tool output{" "}
|
|
1069
|
+
<a
|
|
1070
|
+
href="https://github.com/rtk-ai/rtk"
|
|
1071
|
+
target="_blank"
|
|
1072
|
+
rel="noreferrer"
|
|
1073
|
+
className="text-xs font-normal text-primary underline hover:opacity-80"
|
|
1074
|
+
>
|
|
1075
|
+
(RTK)
|
|
1076
|
+
</a>
|
|
1077
|
+
</p>
|
|
1078
|
+
<p className="text-sm text-text-muted">
|
|
1079
|
+
git/grep/ls/tree/logs → 60-90% fewer input tokens
|
|
1080
|
+
</p>
|
|
1081
|
+
</div>
|
|
1082
|
+
<Toggle
|
|
1083
|
+
checked={rtkEnabled}
|
|
1084
|
+
onChange={() => handleRtkEnabled(!rtkEnabled)}
|
|
1085
|
+
/>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div className="flex items-center justify-between pt-4 gap-4 flex-wrap">
|
|
1088
|
+
<div className="min-w-0 flex-1">
|
|
1089
|
+
<p className="font-medium">
|
|
1090
|
+
Compress LLM output{" "}
|
|
1091
|
+
<a
|
|
1092
|
+
href="https://github.com/JuliusBrussee/caveman"
|
|
1093
|
+
target="_blank"
|
|
1094
|
+
rel="noreferrer"
|
|
1095
|
+
className="text-xs font-normal text-primary underline hover:opacity-80"
|
|
1096
|
+
>
|
|
1097
|
+
(Caveman)
|
|
1098
|
+
</a>
|
|
1099
|
+
</p>
|
|
1100
|
+
<p className="text-sm text-text-muted">
|
|
1101
|
+
Terse-style system prompt → ~65% fewer output tokens (up to 87%)
|
|
1102
|
+
</p>
|
|
1103
|
+
</div>
|
|
1104
|
+
<div className="flex items-center gap-3 shrink-0">
|
|
1105
|
+
{cavemanEnabled && (
|
|
1106
|
+
<div className="flex flex-col items-end gap-1">
|
|
1107
|
+
<div className="flex items-center gap-1.5">
|
|
1108
|
+
{visibleCavemanLevels.map((lvl) => (
|
|
1109
|
+
<button
|
|
1110
|
+
key={lvl.id}
|
|
1111
|
+
onClick={() => handleCavemanLevel(lvl.id)}
|
|
1112
|
+
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
|
|
1113
|
+
cavemanLevel === lvl.id
|
|
1114
|
+
? "bg-primary text-white border-primary"
|
|
1115
|
+
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
|
|
1116
|
+
}`}
|
|
1117
|
+
title={lvl.desc}
|
|
1118
|
+
>
|
|
1119
|
+
{lvl.label}
|
|
1120
|
+
</button>
|
|
1121
|
+
))}
|
|
1122
|
+
</div>
|
|
1123
|
+
<p className="text-xs text-primary">
|
|
1124
|
+
{CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel)?.desc}
|
|
1125
|
+
</p>
|
|
1126
|
+
</div>
|
|
1127
|
+
)}
|
|
1128
|
+
<Toggle
|
|
1129
|
+
checked={cavemanEnabled}
|
|
1130
|
+
onChange={() => handleCavemanEnabled(!cavemanEnabled)}
|
|
1131
|
+
/>
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
</Card>
|
|
1135
|
+
|
|
1136
|
+
{/* API Keys */}
|
|
1137
|
+
<Card id="require-api-key">
|
|
1138
|
+
<div className="flex items-center justify-between mb-4">
|
|
1139
|
+
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
1140
|
+
<span className="material-symbols-outlined text-primary">vpn_key</span>
|
|
1141
|
+
API Keys
|
|
1142
|
+
</h2>
|
|
1143
|
+
<Button icon="add" onClick={() => setShowAddModal(true)}>
|
|
1144
|
+
Create Key
|
|
1145
|
+
</Button>
|
|
1146
|
+
</div>
|
|
1147
|
+
|
|
1148
|
+
<div className="flex items-center justify-between pb-4 mb-4 border-b border-border">
|
|
1149
|
+
<div>
|
|
1150
|
+
<p className="font-medium">Require API key</p>
|
|
1151
|
+
<p className="text-sm text-text-muted">
|
|
1152
|
+
Requests without a valid key will be rejected
|
|
1153
|
+
</p>
|
|
1154
|
+
</div>
|
|
1155
|
+
<Toggle
|
|
1156
|
+
checked={requireApiKey}
|
|
1157
|
+
onChange={() => handleRequireApiKey(!requireApiKey)}
|
|
1158
|
+
/>
|
|
1159
|
+
</div>
|
|
1160
|
+
|
|
1161
|
+
{isRemoteHost && !requireApiKey && (
|
|
1162
|
+
<div className="mb-4 -mt-2">
|
|
1163
|
+
<SecurityWarning message="Endpoint is exposed without an API key." />
|
|
1164
|
+
</div>
|
|
1165
|
+
)}
|
|
1166
|
+
|
|
1167
|
+
{keys.length === 0 ? (
|
|
1168
|
+
<div className="text-center py-12">
|
|
1169
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
|
1170
|
+
<span className="material-symbols-outlined text-[32px]">vpn_key</span>
|
|
1171
|
+
</div>
|
|
1172
|
+
<p className="text-text-main font-medium mb-1">No API keys yet</p>
|
|
1173
|
+
<p className="text-sm text-text-muted mb-4">Create your first API key to get started</p>
|
|
1174
|
+
<Button icon="add" onClick={() => setShowAddModal(true)}>
|
|
1175
|
+
Create Key
|
|
1176
|
+
</Button>
|
|
1177
|
+
</div>
|
|
1178
|
+
) : (
|
|
1179
|
+
<div className="flex flex-col">
|
|
1180
|
+
{keys.map((key) => (
|
|
1181
|
+
<div
|
|
1182
|
+
key={key.id}
|
|
1183
|
+
className={`group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0 ${key.isActive === false ? "opacity-60" : ""}`}
|
|
1184
|
+
>
|
|
1185
|
+
<div className="flex-1 min-w-0">
|
|
1186
|
+
<p className="text-sm font-medium">{key.name}</p>
|
|
1187
|
+
<div className="flex items-center gap-2 mt-1">
|
|
1188
|
+
<code className="text-xs text-text-muted font-mono">
|
|
1189
|
+
{visibleKeys.has(key.id) ? key.key : maskKey(key.key)}
|
|
1190
|
+
</code>
|
|
1191
|
+
<button
|
|
1192
|
+
onClick={() => toggleKeyVisibility(key.id)}
|
|
1193
|
+
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
|
|
1194
|
+
title={visibleKeys.has(key.id) ? "Hide key" : "Show key"}
|
|
1195
|
+
>
|
|
1196
|
+
<span className="material-symbols-outlined text-[14px]">
|
|
1197
|
+
{visibleKeys.has(key.id) ? "visibility_off" : "visibility"}
|
|
1198
|
+
</span>
|
|
1199
|
+
</button>
|
|
1200
|
+
<button
|
|
1201
|
+
onClick={() => copy(key.key, key.id)}
|
|
1202
|
+
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
|
|
1203
|
+
>
|
|
1204
|
+
<span className="material-symbols-outlined text-[14px]">
|
|
1205
|
+
{copied === key.id ? "check" : "content_copy"}
|
|
1206
|
+
</span>
|
|
1207
|
+
</button>
|
|
1208
|
+
</div>
|
|
1209
|
+
<p className="text-xs text-text-muted mt-1">
|
|
1210
|
+
Created {new Date(key.createdAt).toLocaleDateString()}
|
|
1211
|
+
</p>
|
|
1212
|
+
{key.isActive === false && (
|
|
1213
|
+
<p className="text-xs text-orange-500 mt-1">Paused</p>
|
|
1214
|
+
)}
|
|
1215
|
+
</div>
|
|
1216
|
+
<div className="flex items-center gap-2">
|
|
1217
|
+
<Toggle
|
|
1218
|
+
size="sm"
|
|
1219
|
+
checked={key.isActive ?? true}
|
|
1220
|
+
onChange={(checked) => {
|
|
1221
|
+
if (key.isActive && !checked) {
|
|
1222
|
+
setConfirmState({
|
|
1223
|
+
title: "Pause API Key",
|
|
1224
|
+
message: `Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`,
|
|
1225
|
+
onConfirm: async () => {
|
|
1226
|
+
setConfirmState(null);
|
|
1227
|
+
handleToggleKey(key.id, checked);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
} else {
|
|
1231
|
+
handleToggleKey(key.id, checked);
|
|
1232
|
+
}
|
|
1233
|
+
}}
|
|
1234
|
+
title={key.isActive ? "Pause key" : "Resume key"}
|
|
1235
|
+
/>
|
|
1236
|
+
<button
|
|
1237
|
+
onClick={() => handleDeleteKey(key.id)}
|
|
1238
|
+
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
|
|
1239
|
+
>
|
|
1240
|
+
<span className="material-symbols-outlined text-[18px]">delete</span>
|
|
1241
|
+
</button>
|
|
1242
|
+
</div>
|
|
1243
|
+
</div>
|
|
1244
|
+
))}
|
|
1245
|
+
</div>
|
|
1246
|
+
)}
|
|
1247
|
+
</Card>
|
|
1248
|
+
|
|
1249
|
+
{/* Add Key Modal */}
|
|
1250
|
+
<Modal
|
|
1251
|
+
isOpen={showAddModal}
|
|
1252
|
+
title="Create API Key"
|
|
1253
|
+
onClose={() => {
|
|
1254
|
+
setShowAddModal(false);
|
|
1255
|
+
setNewKeyName("");
|
|
1256
|
+
}}
|
|
1257
|
+
>
|
|
1258
|
+
<div className="flex flex-col gap-4">
|
|
1259
|
+
<Input
|
|
1260
|
+
label="Key Name"
|
|
1261
|
+
value={newKeyName}
|
|
1262
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
1263
|
+
placeholder="Production Key"
|
|
1264
|
+
/>
|
|
1265
|
+
<div className="flex gap-2">
|
|
1266
|
+
<Button onClick={handleCreateKey} fullWidth disabled={!newKeyName.trim()}>
|
|
1267
|
+
Create
|
|
1268
|
+
</Button>
|
|
1269
|
+
<Button
|
|
1270
|
+
onClick={() => {
|
|
1271
|
+
setShowAddModal(false);
|
|
1272
|
+
setNewKeyName("");
|
|
1273
|
+
}}
|
|
1274
|
+
variant="ghost"
|
|
1275
|
+
fullWidth
|
|
1276
|
+
>
|
|
1277
|
+
Cancel
|
|
1278
|
+
</Button>
|
|
1279
|
+
</div>
|
|
1280
|
+
</div>
|
|
1281
|
+
</Modal>
|
|
1282
|
+
|
|
1283
|
+
{/* Created Key Modal */}
|
|
1284
|
+
<Modal
|
|
1285
|
+
isOpen={!!createdKey}
|
|
1286
|
+
title="API Key Created"
|
|
1287
|
+
onClose={() => setCreatedKey(null)}
|
|
1288
|
+
>
|
|
1289
|
+
<div className="flex flex-col gap-4">
|
|
1290
|
+
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
|
1291
|
+
<p className="text-sm text-yellow-800 dark:text-yellow-200 mb-2 font-medium">
|
|
1292
|
+
Save this key now!
|
|
1293
|
+
</p>
|
|
1294
|
+
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
|
1295
|
+
This is the only time you will see this key. Store it securely.
|
|
1296
|
+
</p>
|
|
1297
|
+
</div>
|
|
1298
|
+
<div className="flex gap-2">
|
|
1299
|
+
<Input
|
|
1300
|
+
value={createdKey || ""}
|
|
1301
|
+
readOnly
|
|
1302
|
+
className="flex-1 font-mono text-sm"
|
|
1303
|
+
/>
|
|
1304
|
+
<Button
|
|
1305
|
+
variant="secondary"
|
|
1306
|
+
icon={copied === "created_key" ? "check" : "content_copy"}
|
|
1307
|
+
onClick={() => copy(createdKey, "created_key")}
|
|
1308
|
+
>
|
|
1309
|
+
{copied === "created_key" ? "Copied!" : "Copy"}
|
|
1310
|
+
</Button>
|
|
1311
|
+
</div>
|
|
1312
|
+
<Button onClick={() => setCreatedKey(null)} fullWidth>
|
|
1313
|
+
Done
|
|
1314
|
+
</Button>
|
|
1315
|
+
</div>
|
|
1316
|
+
</Modal>
|
|
1317
|
+
|
|
1318
|
+
{/* Enable Tunnel Modal */}
|
|
1319
|
+
<Modal
|
|
1320
|
+
isOpen={showEnableTunnelModal}
|
|
1321
|
+
title="Enable Tunnel"
|
|
1322
|
+
onClose={() => setShowEnableTunnelModal(false)}
|
|
1323
|
+
>
|
|
1324
|
+
<div className="flex flex-col gap-4">
|
|
1325
|
+
<div className="bg-surface-2 border border-border-subtle rounded-lg p-4">
|
|
1326
|
+
<div className="flex items-start gap-3">
|
|
1327
|
+
<span className="material-symbols-outlined text-primary">cloud_upload</span>
|
|
1328
|
+
<div>
|
|
1329
|
+
<p className="text-sm text-text-main font-medium mb-1">
|
|
1330
|
+
Cloudflare Tunnel
|
|
1331
|
+
</p>
|
|
1332
|
+
<p className="text-sm text-text-muted">
|
|
1333
|
+
Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.
|
|
1334
|
+
</p>
|
|
1335
|
+
</div>
|
|
1336
|
+
</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
|
|
1339
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1340
|
+
{TUNNEL_BENEFITS.map((benefit) => (
|
|
1341
|
+
<div key={benefit.title} className="flex flex-col items-center text-center p-3 rounded-lg bg-sidebar/50">
|
|
1342
|
+
<span className="material-symbols-outlined text-xl text-primary mb-1">{benefit.icon}</span>
|
|
1343
|
+
<p className="text-xs font-semibold">{benefit.title}</p>
|
|
1344
|
+
<p className="text-xs text-text-muted">{benefit.desc}</p>
|
|
1345
|
+
</div>
|
|
1346
|
+
))}
|
|
1347
|
+
</div>
|
|
1348
|
+
|
|
1349
|
+
<p className="text-xs text-text-muted">
|
|
1350
|
+
Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.
|
|
1351
|
+
</p>
|
|
1352
|
+
|
|
1353
|
+
<div className="flex gap-2">
|
|
1354
|
+
<Button onClick={handleEnableTunnel} fullWidth>
|
|
1355
|
+
Start Tunnel
|
|
1356
|
+
</Button>
|
|
1357
|
+
<Button onClick={() => setShowEnableTunnelModal(false)} variant="ghost" fullWidth>Cancel</Button>
|
|
1358
|
+
</div>
|
|
1359
|
+
</div>
|
|
1360
|
+
</Modal>
|
|
1361
|
+
|
|
1362
|
+
{/* Disable Cloudflare Tunnel Modal */}
|
|
1363
|
+
<Modal
|
|
1364
|
+
isOpen={showDisableTunnelModal}
|
|
1365
|
+
title="Disable Tunnel"
|
|
1366
|
+
onClose={() => !tunnelLoading && setShowDisableTunnelModal(false)}
|
|
1367
|
+
>
|
|
1368
|
+
<div className="flex flex-col gap-4">
|
|
1369
|
+
<p className="text-sm text-text-muted">The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.</p>
|
|
1370
|
+
<div className="flex gap-2">
|
|
1371
|
+
<Button onClick={handleDisableTunnel} fullWidth disabled={tunnelLoading} variant="danger">
|
|
1372
|
+
{tunnelLoading ? "Disabling..." : "Disable"}
|
|
1373
|
+
</Button>
|
|
1374
|
+
<Button onClick={() => setShowDisableTunnelModal(false)} variant="ghost" fullWidth disabled={tunnelLoading}>Cancel</Button>
|
|
1375
|
+
</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
</Modal>
|
|
1378
|
+
|
|
1379
|
+
{/* Tailscale Modal */}
|
|
1380
|
+
<Modal
|
|
1381
|
+
isOpen={showTsModal}
|
|
1382
|
+
title="Tailscale Funnel"
|
|
1383
|
+
onClose={() => { if (!tsInstalling) { setShowTsModal(false); setTsSudoPassword(""); setTsStatus(null); } }}
|
|
1384
|
+
>
|
|
1385
|
+
<div className="flex flex-col gap-4">
|
|
1386
|
+
{/* Checking state */}
|
|
1387
|
+
{tsInstalled === null && (
|
|
1388
|
+
<p className="text-sm text-text-muted flex items-center gap-2">
|
|
1389
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
1390
|
+
Checking...
|
|
1391
|
+
</p>
|
|
1392
|
+
)}
|
|
1393
|
+
|
|
1394
|
+
{/* Not installed */}
|
|
1395
|
+
{tsInstalled === false && !tsInstalling && (
|
|
1396
|
+
<div className="flex flex-col gap-3">
|
|
1397
|
+
<p className="text-sm text-text-muted">Tailscale is not installed. Install it to enable Funnel.</p>
|
|
1398
|
+
<div className="flex gap-2">
|
|
1399
|
+
<Button onClick={handleInstallTailscale} fullWidth>
|
|
1400
|
+
Install Tailscale
|
|
1401
|
+
</Button>
|
|
1402
|
+
<Button onClick={() => setShowTsModal(false)} variant="ghost" fullWidth>Cancel</Button>
|
|
1403
|
+
</div>
|
|
1404
|
+
</div>
|
|
1405
|
+
)}
|
|
1406
|
+
|
|
1407
|
+
{/* Installing with progress log */}
|
|
1408
|
+
{tsInstalling && (
|
|
1409
|
+
<div className="flex flex-col gap-2">
|
|
1410
|
+
<div className="flex items-center gap-2 text-sm text-text-muted">
|
|
1411
|
+
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
|
1412
|
+
Installing Tailscale...
|
|
1413
|
+
</div>
|
|
1414
|
+
{tsInstallLog.length > 0 && (
|
|
1415
|
+
<div ref={tsLogRef} className="bg-black/5 dark:bg-white/5 rounded p-2 max-h-40 overflow-y-auto font-mono text-xs text-text-muted">
|
|
1416
|
+
{tsInstallLog.map((line, i) => (
|
|
1417
|
+
<div key={i}>{line}</div>
|
|
1418
|
+
))}
|
|
1419
|
+
</div>
|
|
1420
|
+
)}
|
|
1421
|
+
</div>
|
|
1422
|
+
)}
|
|
1423
|
+
|
|
1424
|
+
{/* Installed: show Connect button */}
|
|
1425
|
+
{tsInstalled === true && !tsInstalling && (
|
|
1426
|
+
<div className="flex flex-col gap-3">
|
|
1427
|
+
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
|
1428
|
+
<span className="material-symbols-outlined text-[16px]">check_circle</span>
|
|
1429
|
+
Tailscale installed
|
|
1430
|
+
</div>
|
|
1431
|
+
<div className="flex gap-2">
|
|
1432
|
+
<Button
|
|
1433
|
+
onClick={() => handleConnectTailscale()}
|
|
1434
|
+
fullWidth
|
|
1435
|
+
>
|
|
1436
|
+
Connect
|
|
1437
|
+
</Button>
|
|
1438
|
+
<Button onClick={() => setShowTsModal(false)} variant="ghost" fullWidth>Cancel</Button>
|
|
1439
|
+
</div>
|
|
1440
|
+
</div>
|
|
1441
|
+
)}
|
|
1442
|
+
|
|
1443
|
+
{tsStatus && <StatusAlert status={tsStatus} />}
|
|
1444
|
+
</div>
|
|
1445
|
+
</Modal>
|
|
1446
|
+
|
|
1447
|
+
{/* Disable Tailscale Modal */}
|
|
1448
|
+
<Modal
|
|
1449
|
+
isOpen={showDisableTsModal}
|
|
1450
|
+
title="Disable Tailscale"
|
|
1451
|
+
onClose={() => !tsLoading && setShowDisableTsModal(false)}
|
|
1452
|
+
>
|
|
1453
|
+
<div className="flex flex-col gap-4">
|
|
1454
|
+
<p className="text-sm text-text-muted">Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.</p>
|
|
1455
|
+
<div className="flex gap-2">
|
|
1456
|
+
<Button onClick={handleDisableTailscale} fullWidth disabled={tsLoading} variant="danger">
|
|
1457
|
+
{tsLoading ? "Disabling..." : "Disable"}
|
|
1458
|
+
</Button>
|
|
1459
|
+
<Button onClick={() => setShowDisableTsModal(false)} variant="ghost" fullWidth disabled={tsLoading}>Cancel</Button>
|
|
1460
|
+
</div>
|
|
1461
|
+
</div>
|
|
1462
|
+
</Modal>
|
|
1463
|
+
|
|
1464
|
+
{/* Confirm Modal */}
|
|
1465
|
+
<ConfirmModal
|
|
1466
|
+
isOpen={!!confirmState}
|
|
1467
|
+
onClose={() => setConfirmState(null)}
|
|
1468
|
+
onConfirm={confirmState?.onConfirm}
|
|
1469
|
+
title={confirmState?.title || "Confirm"}
|
|
1470
|
+
message={confirmState?.message}
|
|
1471
|
+
variant="danger"
|
|
1472
|
+
/>
|
|
1473
|
+
</div>
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/** Reusable endpoint row component */
|
|
1478
|
+
function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) {
|
|
1479
|
+
return (
|
|
1480
|
+
<div className="flex items-center gap-2">
|
|
1481
|
+
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
|
1482
|
+
(badge === "CF" || badge === "TS") ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
|
1483
|
+
}`}>{label}</span>
|
|
1484
|
+
<Input value={url} readOnly className="flex-1 font-mono text-sm" />
|
|
1485
|
+
<button
|
|
1486
|
+
onClick={() => onCopy(url, copyId)}
|
|
1487
|
+
className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
|
|
1488
|
+
>
|
|
1489
|
+
<span className="material-symbols-outlined text-[18px]">{copied === copyId ? "check" : "content_copy"}</span>
|
|
1490
|
+
</button>
|
|
1491
|
+
{actions}
|
|
1492
|
+
</div>
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/** Reusable status alert */
|
|
1497
|
+
function StatusAlert({ status, className = "" }) {
|
|
1498
|
+
// Render URLs in message as clickable links
|
|
1499
|
+
const renderMessage = (msg) => {
|
|
1500
|
+
const parts = msg.split(/(https?:\/\/[^\s]+)/g);
|
|
1501
|
+
return parts.map((part, i) =>
|
|
1502
|
+
/^https?:\/\//.test(part)
|
|
1503
|
+
? <a key={i} href={part} target="_blank" rel="noreferrer" className="underline font-medium">{part}</a>
|
|
1504
|
+
: part
|
|
1505
|
+
);
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
return (
|
|
1509
|
+
<div className={`p-2 rounded text-sm ${className} ${status.type === "success" ? "bg-green-500/10 text-green-600 dark:text-green-400" :
|
|
1510
|
+
status.type === "warning" ? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400" :
|
|
1511
|
+
status.type === "info" ? "bg-blue-500/10 text-blue-600 dark:text-blue-400" :
|
|
1512
|
+
"bg-red-500/10 text-red-600 dark:text-red-400"
|
|
1513
|
+
}`}>
|
|
1514
|
+
{renderMessage(status.message)}
|
|
1515
|
+
</div>
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** Inline tooltip, Claude Code CLI style */
|
|
1520
|
+
function Tooltip({ text }) {
|
|
1521
|
+
return (
|
|
1522
|
+
<span className="relative group inline-flex items-center">
|
|
1523
|
+
<span className="material-symbols-outlined text-[14px] text-text-muted cursor-help">help</span>
|
|
1524
|
+
<span className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 z-50 w-64 rounded bg-gray-900 dark:bg-gray-800 text-white text-xs px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
|
|
1525
|
+
{text}
|
|
1526
|
+
</span>
|
|
1527
|
+
</span>
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/** Security warning banner with optional action link */
|
|
1532
|
+
function SecurityWarning({ message, action }) {
|
|
1533
|
+
return (
|
|
1534
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-700 dark:text-amber-400">
|
|
1535
|
+
<span className="material-symbols-outlined text-[16px] shrink-0 mt-0.5">warning</span>
|
|
1536
|
+
<p className="text-xs flex-1">{message}</p>
|
|
1537
|
+
{action && (
|
|
1538
|
+
<a
|
|
1539
|
+
href={action.href}
|
|
1540
|
+
className="text-xs font-medium underline shrink-0 hover:opacity-80"
|
|
1541
|
+
onClick={action.href.startsWith("#") ? (e) => {
|
|
1542
|
+
e.preventDefault();
|
|
1543
|
+
document.getElementById(action.href.slice(1))?.scrollIntoView({ behavior: "smooth" });
|
|
1544
|
+
} : undefined}
|
|
1545
|
+
>
|
|
1546
|
+
{action.label}
|
|
1547
|
+
</a>
|
|
1548
|
+
)}
|
|
1549
|
+
</div>
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
APIPageClient.propTypes = {
|
|
1554
|
+
machineId: PropTypes.string.isRequired,
|
|
1555
|
+
};
|