Clarify qBittorrent status and fix status pill contrast
This commit is contained in:
@@ -52,6 +52,17 @@ class QBittorrentClient(ApiClient):
|
|||||||
response = await client.post(f"{self.base_url}{path}", data=data)
|
response = await client.post(f"{self.base_url}{path}", data=data)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
async def is_webui_reachable(self) -> bool:
|
||||||
|
if not self.base_url:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||||||
|
response = await client.get(self.base_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return True
|
||||||
|
except httpx.HTTPError:
|
||||||
|
return False
|
||||||
|
|
||||||
async def get_torrents(self) -> Optional[Any]:
|
async def get_torrents(self) -> Optional[Any]:
|
||||||
return await self._get("/api/v2/torrents/info")
|
return await self._get("/api/v2/torrents/info")
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,35 @@ async def _check(name: str, configured: bool, func) -> Dict[str, Any]:
|
|||||||
return {"name": name, "status": "down", "message": str(exc)}
|
return {"name": name, "status": "down", "message": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_qbittorrent(qbittorrent: QBittorrentClient) -> Dict[str, Any]:
|
||||||
|
if not qbittorrent.base_url:
|
||||||
|
return {"name": "qBittorrent", "status": "not_configured"}
|
||||||
|
if not qbittorrent.username or not qbittorrent.password:
|
||||||
|
reachable = await qbittorrent.is_webui_reachable()
|
||||||
|
return {
|
||||||
|
"name": "qBittorrent",
|
||||||
|
"status": "degraded" if reachable else "not_configured",
|
||||||
|
"message": "qBittorrent credentials are incomplete" if reachable else "qBittorrent is not fully configured",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
result = await qbittorrent.get_app_version()
|
||||||
|
return {"name": "qBittorrent", "status": "up", "detail": result}
|
||||||
|
except RuntimeError as exc:
|
||||||
|
if "login failed" in str(exc).lower():
|
||||||
|
reachable = await qbittorrent.is_webui_reachable()
|
||||||
|
if reachable:
|
||||||
|
return {
|
||||||
|
"name": "qBittorrent",
|
||||||
|
"status": "degraded",
|
||||||
|
"message": "qBittorrent is reachable but the saved credentials were rejected",
|
||||||
|
}
|
||||||
|
return {"name": "qBittorrent", "status": "down", "message": str(exc)}
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
return {"name": "qBittorrent", "status": "down", "message": str(exc)}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"name": "qBittorrent", "status": "down", "message": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/services")
|
@router.get("/services")
|
||||||
async def services_status() -> Dict[str, Any]:
|
async def services_status() -> Dict[str, Any]:
|
||||||
runtime = get_runtime_settings()
|
runtime = get_runtime_settings()
|
||||||
@@ -71,13 +100,7 @@ async def services_status() -> Dict[str, Any]:
|
|||||||
prowlarr_status["status"] = "degraded"
|
prowlarr_status["status"] = "degraded"
|
||||||
prowlarr_status["message"] = "Health warnings"
|
prowlarr_status["message"] = "Health warnings"
|
||||||
services.append(prowlarr_status)
|
services.append(prowlarr_status)
|
||||||
services.append(
|
services.append(await _check_qbittorrent(qbittorrent))
|
||||||
await _check(
|
|
||||||
"qBittorrent",
|
|
||||||
qbittorrent.configured(),
|
|
||||||
qbittorrent.get_app_version,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
services.append(
|
services.append(
|
||||||
await _check(
|
await _check(
|
||||||
"Jellyfin",
|
"Jellyfin",
|
||||||
@@ -122,10 +145,12 @@ async def test_service(service: str) -> Dict[str, Any]:
|
|||||||
"sonarr": ("Sonarr", sonarr.configured(), sonarr.get_system_status),
|
"sonarr": ("Sonarr", sonarr.configured(), sonarr.get_system_status),
|
||||||
"radarr": ("Radarr", radarr.configured(), radarr.get_system_status),
|
"radarr": ("Radarr", radarr.configured(), radarr.get_system_status),
|
||||||
"prowlarr": ("Prowlarr", prowlarr.configured(), prowlarr.get_health),
|
"prowlarr": ("Prowlarr", prowlarr.configured(), prowlarr.get_health),
|
||||||
"qbittorrent": ("qBittorrent", qbittorrent.configured(), qbittorrent.get_app_version),
|
|
||||||
"jellyfin": ("Jellyfin", jellyfin.configured(), jellyfin.get_system_info),
|
"jellyfin": ("Jellyfin", jellyfin.configured(), jellyfin.get_system_info),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if service_key == "qbittorrent":
|
||||||
|
return await _check_qbittorrent(qbittorrent)
|
||||||
|
|
||||||
if service_key not in checks:
|
if service_key not in checks:
|
||||||
raise HTTPException(status_code=404, detail="Unknown service")
|
raise HTTPException(status_code=404, detail="Unknown service")
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from backend.app.config import settings
|
|||||||
from backend.app.network_security import request_trusts_forwarded_headers, validate_notification_target_url
|
from backend.app.network_security import request_trusts_forwarded_headers, validate_notification_target_url
|
||||||
from backend.app.routers import auth as auth_router
|
from backend.app.routers import auth as auth_router
|
||||||
from backend.app.routers import portal as portal_router
|
from backend.app.routers import portal as portal_router
|
||||||
|
from backend.app.routers import status as status_router
|
||||||
from backend.app.security import PASSWORD_POLICY_MESSAGE, validate_password_policy
|
from backend.app.security import PASSWORD_POLICY_MESSAGE, validate_password_policy
|
||||||
from backend.app.services import password_reset
|
from backend.app.services import password_reset
|
||||||
|
|
||||||
@@ -94,6 +95,28 @@ class NetworkSecurityTests(unittest.TestCase):
|
|||||||
settings.magent_proxy_trusted_proxies = original_proxies
|
settings.magent_proxy_trusted_proxies = original_proxies
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceStatusTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_qbittorrent_incomplete_credentials_report_degraded_when_reachable(self) -> None:
|
||||||
|
client = status_router.QBittorrentClient("http://10.0.0.2:8080", "admin", None)
|
||||||
|
with patch.object(client, "is_webui_reachable", new=AsyncMock(return_value=True)):
|
||||||
|
result = await status_router._check_qbittorrent(client)
|
||||||
|
|
||||||
|
self.assertEqual(result["status"], "degraded")
|
||||||
|
self.assertIn("credentials", result["message"].lower())
|
||||||
|
|
||||||
|
async def test_qbittorrent_rejected_credentials_report_degraded_when_reachable(self) -> None:
|
||||||
|
client = status_router.QBittorrentClient("http://10.0.0.2:8080", "admin", "secret")
|
||||||
|
with patch.object(
|
||||||
|
client,
|
||||||
|
"get_app_version",
|
||||||
|
new=AsyncMock(side_effect=RuntimeError("qBittorrent login failed")),
|
||||||
|
), patch.object(client, "is_webui_reachable", new=AsyncMock(return_value=True)):
|
||||||
|
result = await status_router._check_qbittorrent(client)
|
||||||
|
|
||||||
|
self.assertEqual(result["status"], "degraded")
|
||||||
|
self.assertIn("credentials", result["message"].lower())
|
||||||
|
|
||||||
|
|
||||||
class DatabaseEmailTests(TempDatabaseMixin, unittest.TestCase):
|
class DatabaseEmailTests(TempDatabaseMixin, unittest.TestCase):
|
||||||
def test_set_user_email_is_case_insensitive(self) -> None:
|
def test_set_user_email_is_case_insensitive(self) -> None:
|
||||||
created = db.create_user_if_missing(
|
created = db.create_user_if_missing(
|
||||||
|
|||||||
@@ -3565,12 +3565,14 @@ button:disabled {
|
|||||||
.user-grid-pill.is-blocked {
|
.user-grid-pill.is-blocked {
|
||||||
background: rgba(244, 114, 114, 0.14);
|
background: rgba(244, 114, 114, 0.14);
|
||||||
border-color: rgba(244, 114, 114, 0.24);
|
border-color: rgba(244, 114, 114, 0.24);
|
||||||
|
color: #ffd5d5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-pill-degraded,
|
.system-pill-degraded,
|
||||||
.user-grid-pill.is-disabled {
|
.user-grid-pill.is-disabled {
|
||||||
background: rgba(208, 166, 92, 0.14);
|
background: rgba(208, 166, 92, 0.14);
|
||||||
border-color: rgba(208, 166, 92, 0.22);
|
border-color: rgba(208, 166, 92, 0.22);
|
||||||
|
color: #ffe3a6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-dot {
|
.system-dot {
|
||||||
|
|||||||
Reference in New Issue
Block a user