feat: add API Status Checker tab in Advanced Settings with connection status and navigation
Some checks failed
Release Binaries / release (push) Has been cancelled

This commit is contained in:
Gemini AI
2025-12-29 00:50:27 +04:00
Unverified
parent 229f86c229
commit 721da6f2ee
2 changed files with 343 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ import QwenCodeSettings from "./settings/QwenCodeSettings"
import ZAISettings from "./settings/ZAISettings"
import OpenCodeZenSettings from "./settings/OpenCodeZenSettings"
import AntigravitySettings from "./settings/AntigravitySettings"
import ApiStatusChecker from "./settings/ApiStatusChecker"
interface AdvancedSettingsModalProps {
open: boolean
@@ -17,7 +18,7 @@ interface AdvancedSettingsModalProps {
}
const AdvancedSettingsModal: Component<AdvancedSettingsModalProps> = (props) => {
const [activeTab, setActiveTab] = createSignal("general")
const [activeTab, setActiveTab] = createSignal("api-status")
return (
<Dialog open={props.open} onOpenChange={(open) => !open && props.onClose()}>
@@ -31,6 +32,15 @@ const AdvancedSettingsModal: Component<AdvancedSettingsModalProps> = (props) =>
<div class="border-b" style={{ "border-color": "var(--border-base)" }}>
<div class="flex w-full px-6 overflow-x-auto">
<button
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "api-status"
? "border-green-500 text-green-400"
: "border-transparent hover:border-gray-300"
}`}
onClick={() => setActiveTab("api-status")}
>
📊 API Status
</button>
<button
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "zen"
? "border-orange-500 text-orange-400"
@@ -89,6 +99,20 @@ const AdvancedSettingsModal: Component<AdvancedSettingsModalProps> = (props) =>
</div>
<div class="flex-1 overflow-y-auto">
<Show when={activeTab() === "api-status"}>
<div class="p-6">
<ApiStatusChecker
onSettingsClick={(apiId) => {
if (apiId === "opencode-zen") setActiveTab("zen")
else if (apiId === "ollama-cloud") setActiveTab("ollama")
else if (apiId === "zai") setActiveTab("zai")
else if (apiId === "qwen-oauth") setActiveTab("qwen")
else if (apiId === "antigravity") setActiveTab("antigravity")
}}
/>
</div>
</Show>
<Show when={activeTab() === "zen"}>
<OpenCodeZenSettings />
</Show>

View File

@@ -0,0 +1,318 @@
import { Component, createSignal, onMount, For, Show, createEffect, on } from "solid-js"
import { CheckCircle, XCircle, Loader, RefreshCw, Settings, AlertTriangle } from "lucide-solid"
interface ApiStatus {
id: string
name: string
icon: string
enabled: boolean
connected: boolean
checking: boolean
error?: string
lastChecked?: number
}
interface ApiStatusCheck {
id: string
name: string
icon: string
checkEnabled: () => Promise<boolean>
testConnection: () => Promise<boolean>
}
const API_CHECKS: ApiStatusCheck[] = [
{
id: "opencode-zen",
name: "OpenCode Zen",
icon: "🆓",
checkEnabled: async () => true, // Always available
testConnection: async () => {
try {
const res = await fetch("/api/opencode-zen/test")
if (!res.ok) return false
const data = await res.json()
return data.connected === true
} catch {
return false
}
},
},
{
id: "ollama-cloud",
name: "Ollama Cloud",
icon: "🦙",
checkEnabled: async () => {
try {
const res = await fetch("/api/ollama/config")
if (!res.ok) return false
const data = await res.json()
return data.config?.enabled === true
} catch {
return false
}
},
testConnection: async () => {
try {
const res = await fetch("/api/ollama/test", { method: "POST" })
if (!res.ok) return false
const data = await res.json()
return data.connected === true
} catch {
return false
}
},
},
{
id: "zai",
name: "Z.AI Plan",
icon: "🧠",
checkEnabled: async () => {
try {
const res = await fetch("/api/zai/config")
if (!res.ok) return false
const data = await res.json()
return data.config?.enabled === true
} catch {
return false
}
},
testConnection: async () => {
try {
const res = await fetch("/api/zai/test", { method: "POST" })
if (!res.ok) return false
const data = await res.json()
return data.connected === true
} catch {
return false
}
},
},
{
id: "qwen-oauth",
name: "Qwen Code",
icon: "🔷",
checkEnabled: async () => {
const token = localStorage.getItem("qwen_oauth_token")
return token !== null && token.length > 0
},
testConnection: async () => {
try {
const tokenStr = localStorage.getItem("qwen_oauth_token")
if (!tokenStr) return false
const token = JSON.parse(tokenStr)
// Check if token is expired
const expiresAt = (token.created_at || 0) + (token.expires_in || 0) * 1000
return Date.now() < expiresAt
} catch {
return false
}
},
},
{
id: "antigravity",
name: "Antigravity",
icon: "🚀",
checkEnabled: async () => {
const token = localStorage.getItem("antigravity_oauth_token")
return token !== null && token.length > 0
},
testConnection: async () => {
try {
const tokenStr = localStorage.getItem("antigravity_oauth_token")
if (!tokenStr) return false
const token = JSON.parse(tokenStr)
const expiresAt = (token.created_at || 0) + (token.expires_in || 0) * 1000
return Date.now() < expiresAt
} catch {
return false
}
},
},
]
interface ApiStatusCheckerProps {
onSettingsClick?: (apiId: string) => void
compact?: boolean
}
const ApiStatusChecker: Component<ApiStatusCheckerProps> = (props) => {
const [statuses, setStatuses] = createSignal<ApiStatus[]>([])
const [isChecking, setIsChecking] = createSignal(false)
const [lastFullCheck, setLastFullCheck] = createSignal<number>(0)
const checkAllApis = async () => {
setIsChecking(true)
const results: ApiStatus[] = []
for (const api of API_CHECKS) {
setStatuses((prev) => {
const existing = prev.find((s) => s.id === api.id)
if (existing) {
return prev.map((s) => (s.id === api.id ? { ...s, checking: true } : s))
}
return [...prev, { id: api.id, name: api.name, icon: api.icon, enabled: false, connected: false, checking: true }]
})
try {
const enabled = await api.checkEnabled()
let connected = false
let error: string | undefined
if (enabled) {
try {
connected = await api.testConnection()
} catch (e) {
error = e instanceof Error ? e.message : "Connection test failed"
}
}
results.push({
id: api.id,
name: api.name,
icon: api.icon,
enabled,
connected,
checking: false,
error,
lastChecked: Date.now(),
})
} catch (e) {
results.push({
id: api.id,
name: api.name,
icon: api.icon,
enabled: false,
connected: false,
checking: false,
error: e instanceof Error ? e.message : "Check failed",
lastChecked: Date.now(),
})
}
}
setStatuses(results)
setLastFullCheck(Date.now())
setIsChecking(false)
}
onMount(() => {
checkAllApis()
})
const getStatusIcon = (status: ApiStatus) => {
if (status.checking) {
return <Loader class="w-4 h-4 animate-spin text-gray-400" />
}
if (!status.enabled) {
return <div class="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600" />
}
if (status.connected) {
return <CheckCircle class="w-4 h-4 text-green-500" />
}
if (status.error) {
return <XCircle class="w-4 h-4 text-red-500" />
}
return <AlertTriangle class="w-4 h-4 text-yellow-500" />
}
const getStatusText = (status: ApiStatus) => {
if (status.checking) return "Checking..."
if (!status.enabled) return "Not configured"
if (status.connected) return "Connected"
if (status.error) return status.error
return "Connection failed"
}
const enabledCount = () => statuses().filter((s) => s.enabled && s.connected).length
const totalConfigured = () => statuses().filter((s) => s.enabled).length
if (props.compact) {
return (
<div class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<span class="text-xs text-gray-500">APIs:</span>
<div class="flex items-center gap-1">
<For each={statuses()}>
{(status) => (
<div
class="cursor-pointer hover:scale-110 transition-transform"
title={`${status.name}: ${getStatusText(status)}`}
onClick={() => props.onSettingsClick?.(status.id)}
>
<span class="text-sm">{status.icon}</span>
<Show when={status.enabled}>
<span
class={`inline-block w-1.5 h-1.5 rounded-full ml-0.5 ${status.connected ? "bg-green-500" : status.checking ? "bg-yellow-500" : "bg-red-500"
}`}
/>
</Show>
</div>
)}
</For>
</div>
<button
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
onClick={checkAllApis}
disabled={isChecking()}
title="Refresh API status"
>
<RefreshCw class={`w-3 h-3 ${isChecking() ? "animate-spin" : ""}`} />
</button>
</div>
)
}
return (
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold">API Connections</h3>
<p class="text-sm text-gray-500">
{enabledCount()} of {totalConfigured()} APIs connected
</p>
</div>
<button
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-500 hover:bg-blue-600 text-white rounded-lg disabled:opacity-50"
onClick={checkAllApis}
disabled={isChecking()}
>
<RefreshCw class={`w-4 h-4 ${isChecking() ? "animate-spin" : ""}`} />
{isChecking() ? "Checking..." : "Refresh All"}
</button>
</div>
<div class="grid gap-3">
<For each={statuses()}>
{(status) => (
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<span class="text-xl">{status.icon}</span>
<div>
<div class="font-medium">{status.name}</div>
<div class="text-xs text-gray-500">{getStatusText(status)}</div>
</div>
</div>
<div class="flex items-center gap-2">
{getStatusIcon(status)}
<button
class="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
onClick={() => props.onSettingsClick?.(status.id)}
title="Configure"
>
<Settings class="w-4 h-4" />
</button>
</div>
</div>
)}
</For>
</div>
<Show when={lastFullCheck() > 0}>
<p class="text-xs text-gray-400 text-center">
Last checked: {new Date(lastFullCheck()).toLocaleTimeString()}
</p>
</Show>
</div>
)
}
export default ApiStatusChecker