diff --git a/dashboard/backend/routers/system.py b/dashboard/backend/routers/system.py
index 6a6da9e..4f7ede8 100644
--- a/dashboard/backend/routers/system.py
+++ b/dashboard/backend/routers/system.py
@@ -1,3 +1,4 @@
+import asyncio
import os
import platform
import shutil
@@ -9,7 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request
import auth_service as auth
import config
-from config import OPENCLAW_GATEWAY_TOKEN, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, VOLUME_ROOT
+from config import GITEA_TOKEN, GITEA_URL, OPENCLAW_GATEWAY_TOKEN, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, VOLUME_ROOT
router = APIRouter()
@@ -151,6 +152,61 @@ def openclaw_token(request: Request):
return {"token": OPENCLAW_GATEWAY_TOKEN}
+@router.get("/service-health")
+async def service_health(current_user=Depends(auth.get_current_user)):
+ """Ping all external services and return their status with latency."""
+
+ async def _ping(key: str, url: str) -> dict:
+ start = time.monotonic()
+ try:
+ async with httpx.AsyncClient(timeout=5.0) as client:
+ r = await client.head(url, follow_redirects=True)
+ latency_ms = round((time.monotonic() - start) * 1000)
+ status = "up" if r.status_code < 500 else "down"
+ except httpx.TimeoutException:
+ latency_ms = round((time.monotonic() - start) * 1000)
+ status = "down"
+ except Exception:
+ latency_ms = round((time.monotonic() - start) * 1000)
+ status = "error"
+ return key, {"url": url, "status": status, "latency_ms": latency_ms}
+
+ results = await asyncio.gather(
+ *(_ping(key, url) for key, url in config.EXTERNAL_SERVICES.items())
+ )
+ return {"services": dict(results)}
+
+
+_CI_HEADERS = {"Authorization": f"token {GITEA_TOKEN}"} if GITEA_TOKEN else {}
+
+
+@router.get("/ci-runs")
+async def ci_runs():
+ """Fetch recent Gitea Actions CI runs for jimmy/nas-tools."""
+ async with httpx.AsyncClient(timeout=10) as c:
+ r = await c.get(
+ f"{GITEA_URL}/api/v1/repos/jimmy/nas-tools/actions/runs",
+ headers=_CI_HEADERS,
+ params={"limit": 5},
+ )
+ r.raise_for_status()
+ data = r.json()
+ runs = []
+ for wf in data.get("workflow_runs", []):
+ runs.append(
+ {
+ "id": wf.get("id"),
+ "status": wf.get("status"),
+ "event": wf.get("event"),
+ "head_branch": wf.get("head_branch"),
+ "head_sha": (wf.get("head_sha") or "")[:12],
+ "display_title": wf.get("display_title"),
+ "run_number": wf.get("run_number"),
+ }
+ )
+ return {"workflow_runs": runs}
+
+
@router.get("/external-services")
def external_services():
"""Return external service URLs and network config for the frontend sidebar."""
diff --git a/dashboard/frontend/src/App.svelte b/dashboard/frontend/src/App.svelte
index ed7057d..34599a0 100644
--- a/dashboard/frontend/src/App.svelte
+++ b/dashboard/frontend/src/App.svelte
@@ -1,5 +1,6 @@
+
+
System status at a glance
+ +System status at a glance
+Pin your favorite services
+CPU
@@ -65,7 +193,6 @@{stats?.cpu_count || 0} cores · Load {stats?.load_avg?.[0] || 0}
Memory
@@ -77,7 +204,6 @@{fmtBytes(stats?.memory?.used)} / {fmtBytes(stats?.memory?.total)}
Disk
@@ -89,7 +215,6 @@{fmtBytes(stats?.disk?.free)} free of {fmtBytes(stats?.disk?.total)}
Uptime
@@ -102,50 +227,74 @@+{containers.length - 5} more
- {/if} ++{repos.length - 5} more
- {/if} -