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 @@ + + + +{#if open} + + + + + +{/if} diff --git a/dashboard/frontend/src/routes/Dashboard.svelte b/dashboard/frontend/src/routes/Dashboard.svelte index ab712af..50d8bc4 100644 --- a/dashboard/frontend/src/routes/Dashboard.svelte +++ b/dashboard/frontend/src/routes/Dashboard.svelte @@ -1,23 +1,31 @@
-
-

Overview

-

System status at a glance

+ +
+
+

Overview

+

System status at a glance

+
+
+ + +
{#if loading} @@ -51,9 +141,47 @@ {/each}
{:else} + +
+ {#each pinned as item} + + {/each} + +
+ + + {#if editPinned} +
+

Pin your favorite services

+
+ {#each allNavItems as item} + + {/each} +
+
+ {/if} +
-

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 @@
- -
- -
+ +
+ +
-

Docker Containers

- {containers.filter(c => c.status === "running").length}/{containers.length} running -
-
- {#each containers.slice(0, 5) as c} -
-
- - {c.name} -
- {c.image} -
- {/each} - {#if containers.length > 5} -

+{containers.length - 5} more

- {/if} +

Service Health

+ {services.filter(s => s.status === "up").length}/{services.length} online
+ {#if serviceLoading} +
+ {#each Array(6) as _} +
+ {/each} +
+ {:else} + + {/if}
- +
-

Git Repositories

- {repos.length} repos +

CI Runs

+ nas-tools
-
- {#each repos.slice(0, 5) as r} -
-
- - {r.name} + {#if ciLoading} +
+ {#each Array(4) as _} +
+ {/each} +
+ {:else if ciRuns.length === 0} +
No CI runs found
+ {:else} +
+ {#each ciRuns as run} +
+ +
+
{run.display_title || "—"}
+
+ #{run.run_number} + {run.event} + {run.head_sha?.slice(0, 8) || ""} +
+
+ {run.status}
- {r.language || "—"} -
- {/each} - {#if repos.length > 5} -

+{repos.length - 5} more

- {/if} -
+ {/each} +
+ {/if}