Add service test buttons (build 271261335)
This commit is contained in:
@@ -1 +1 @@
|
||||
271261322
|
||||
271261335
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..runtime import get_runtime_settings
|
||||
@@ -93,3 +93,42 @@ async def services_status() -> Dict[str, Any]:
|
||||
overall = "degraded"
|
||||
|
||||
return {"overall": overall, "services": services}
|
||||
|
||||
|
||||
@router.post("/services/{service}/test")
|
||||
async def test_service(service: str) -> Dict[str, Any]:
|
||||
runtime = get_runtime_settings()
|
||||
jellyseerr = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
sonarr = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
|
||||
radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key)
|
||||
qbittorrent = QBittorrentClient(
|
||||
runtime.qbittorrent_base_url, runtime.qbittorrent_username, runtime.qbittorrent_password
|
||||
)
|
||||
jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
|
||||
|
||||
service_key = service.strip().lower()
|
||||
checks = {
|
||||
"jellyseerr": (
|
||||
"Jellyseerr",
|
||||
jellyseerr.configured(),
|
||||
lambda: jellyseerr.get_recent_requests(take=1, skip=0),
|
||||
),
|
||||
"sonarr": ("Sonarr", sonarr.configured(), sonarr.get_system_status),
|
||||
"radarr": ("Radarr", radarr.configured(), radarr.get_system_status),
|
||||
"prowlarr": ("Prowlarr", prowlarr.configured(), prowlarr.get_health),
|
||||
"qbittorrent": ("qBittorrent", qbittorrent.configured(), qbittorrent.get_app_version),
|
||||
"jellyfin": ("Jellyfin", jellyfin.configured(), jellyfin.get_system_info),
|
||||
}
|
||||
|
||||
if service_key not in checks:
|
||||
raise HTTPException(status_code=404, detail="Unknown service")
|
||||
|
||||
name, configured, func = checks[service_key]
|
||||
result = await _check(name, configured, func)
|
||||
if name == "Prowlarr" and result.get("status") == "up":
|
||||
health = result.get("detail")
|
||||
if isinstance(health, list) and health:
|
||||
result["status"] = "degraded"
|
||||
result["message"] = "Health warnings"
|
||||
return result
|
||||
|
||||
@@ -1415,6 +1415,24 @@ button span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.system-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.system-test-message {
|
||||
font-size: 11px;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.system-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.system-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -1444,10 +1462,28 @@ button span {
|
||||
}
|
||||
|
||||
.system-state {
|
||||
margin-left: auto;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.system-test {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--ink-muted);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.system-test:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.system-test:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pipeline-map {
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
|
||||
@@ -30,6 +30,8 @@ export default function HomePage() {
|
||||
>(null)
|
||||
const [servicesLoading, setServicesLoading] = useState(false)
|
||||
const [servicesError, setServicesError] = useState<string | null>(null)
|
||||
const [serviceTesting, setServiceTesting] = useState<Record<string, boolean>>({})
|
||||
const [serviceTestResults, setServiceTestResults] = useState<Record<string, string | null>>({})
|
||||
|
||||
const submit = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
@@ -42,6 +44,61 @@ export default function HomePage() {
|
||||
void runSearch(trimmed)
|
||||
}
|
||||
|
||||
const toServiceSlug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
|
||||
const updateServiceStatus = (name: string, status: string, message?: string) => {
|
||||
setServicesStatus((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
services: prev.services.map((service) =>
|
||||
service.name === name ? { ...service, status, message } : service
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const testService = async (name: string) => {
|
||||
const slug = toServiceSlug(name)
|
||||
setServiceTesting((prev) => ({ ...prev, [name]: true }))
|
||||
setServiceTestResults((prev) => ({ ...prev, [name]: null }))
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(`${baseUrl}/status/services/${slug}/test`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
clearToken()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
const text = await response.text()
|
||||
throw new Error(text || `Service test failed: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
const status = data?.status ?? 'unknown'
|
||||
const message =
|
||||
data?.message ||
|
||||
(status === 'up'
|
||||
? 'API OK'
|
||||
: status === 'down'
|
||||
? 'API unreachable'
|
||||
: status === 'degraded'
|
||||
? 'Health warnings'
|
||||
: status === 'not_configured'
|
||||
? 'Not configured'
|
||||
: 'Unknown')
|
||||
setServiceTestResults((prev) => ({ ...prev, [name]: message }))
|
||||
updateServiceStatus(name, status, data?.message)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setServiceTestResults((prev) => ({ ...prev, [name]: 'Test failed' }))
|
||||
} finally {
|
||||
setServiceTesting((prev) => ({ ...prev, [name]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
router.push('/login')
|
||||
@@ -214,21 +271,37 @@ export default function HomePage() {
|
||||
return order.map((name) => {
|
||||
const item = items.find((entry) => entry.name === name)
|
||||
const status = item?.status ?? 'unknown'
|
||||
const testing = serviceTesting[name] ?? false
|
||||
return (
|
||||
<div key={name} className={`system-item system-${status}`}>
|
||||
<span className="system-dot" />
|
||||
<span className="system-name">{name}</span>
|
||||
<span className="system-state">
|
||||
{status === 'up'
|
||||
? 'Up'
|
||||
: status === 'down'
|
||||
? 'Down'
|
||||
: status === 'degraded'
|
||||
? 'Needs attention'
|
||||
: status === 'not_configured'
|
||||
? 'Not configured'
|
||||
: 'Unknown'}
|
||||
</span>
|
||||
<div className="system-meta">
|
||||
<span className="system-name">{name}</span>
|
||||
{serviceTestResults[name] && (
|
||||
<span className="system-test-message">{serviceTestResults[name]}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-actions">
|
||||
<span className="system-state">
|
||||
{status === 'up'
|
||||
? 'Up'
|
||||
: status === 'down'
|
||||
? 'Down'
|
||||
: status === 'degraded'
|
||||
? 'Needs attention'
|
||||
: status === 'not_configured'
|
||||
? 'Not configured'
|
||||
: 'Unknown'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="system-test"
|
||||
onClick={() => void testService(name)}
|
||||
disabled={testing}
|
||||
>
|
||||
{testing ? 'Testing...' : 'Test'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user