diff --git a/PLAN.md b/PLAN.md
index 1a8089a..25bc4d1 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -176,11 +176,12 @@ nas-tools/
### Phase 14 — Hybrid AI Gateway (LiteLLM + cc-connect) 🚧
49. ~~Add `litellm/` service scaffold (`docker-compose.yml`, `config/litellm.yaml`, `.env.example`) with localhost-only host binding and env-driven provider keys~~ — Done
50. ~~Add dashboard backend LiteLLM operational probe (`/api/litellm/health`) and wire into protected routes~~ — Done
-51. Add `cc-connect/` templates (`README.md`, `config.example.toml`, optional compose) for mobile bridge rollout
+51. ~~Add `cc-connect/` templates (`README.md`, `config.example.toml`, optional compose) for mobile bridge rollout~~ — Done
52. ~~Deploy LiteLLM image to NAS using server2 pull + stream load workflow to avoid slow GHCR pulls on NAS~~ — Done
53. ~~Network fix: attach LiteLLM to `nas-dashboard_internal` and set dashboard-dev `LITELLM_URL=http://litellm:4005`~~ — Done
54. ~~Update backend health probe to support auth-required mode and configured health auth key~~ — Done
55. ~~Add LiteLLM dashboard UI page with sidebar link and health status card~~ — Done
+56. ~~Add cc-connect dashboard UI page, sidebar link, and backend health/status endpoint~~ — Done
### Phase 13 — Off-site Backup
46. Add cloud backup (OneDrive or Google Drive) for critical data
diff --git a/dashboard/backend/config.py b/dashboard/backend/config.py
index 46814f5..1d7475e 100644
--- a/dashboard/backend/config.py
+++ b/dashboard/backend/config.py
@@ -48,6 +48,9 @@ CHAT_SUMMARY_DB = os.environ.get("CHAT_SUMMARY_DB", "/app/data/chat-summarizer/c
LITELLM_URL = os.environ.get("LITELLM_URL", "http://127.0.0.1:4005")
LITELLM_HEALTH_API_KEY = os.environ.get("LITELLM_HEALTH_API_KEY", "")
+# CC Connect
+CC_CONNECT_URL = os.environ.get("CC_CONNECT_URL", "")
+
# OpenClaw
OPENCLAW_GATEWAY_TOKEN = os.environ.get("OPENCLAW_GATEWAY_TOKEN", "")
diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py
index 924a929..eb59587 100644
--- a/dashboard/backend/main.py
+++ b/dashboard/backend/main.py
@@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
-from routers import docker_router, gitea, files, terminal, system, auth, chat_summary, security, passkey, totp, litellm
+from routers import docker_router, gitea, files, terminal, system, auth, chat_summary, security, passkey, totp, litellm, cc_connect
import auth as auth_module
from rbac import require_page, require_write
@@ -181,6 +181,8 @@ app.include_router(system.router, prefix="/api/system",
dependencies=[Depends(_inject_user), Depends(require_page("dashboard"))])
app.include_router(litellm.router, prefix="/api/litellm",
dependencies=[Depends(_inject_user), Depends(require_page("dashboard"))])
+app.include_router(cc_connect.router, prefix="/api/cc-connect",
+ dependencies=[Depends(_inject_user), Depends(require_page("dashboard"))])
app.include_router(chat_summary.router, prefix="/api/chat-summary",
dependencies=[Depends(_inject_user), Depends(require_page("chat-digest"))])
app.include_router(security.router, prefix="/api/security",
diff --git a/dashboard/backend/routers/cc_connect.py b/dashboard/backend/routers/cc_connect.py
new file mode 100644
index 0000000..8d59573
--- /dev/null
+++ b/dashboard/backend/routers/cc_connect.py
@@ -0,0 +1,107 @@
+import time
+
+import docker
+import httpx
+from fastapi import APIRouter
+
+from config import CC_CONNECT_URL, DOCKER_HOST
+
+router = APIRouter()
+client = docker.DockerClient(base_url=DOCKER_HOST, timeout=5)
+
+
+@router.get("/health")
+async def cc_connect_health():
+ url = (CC_CONNECT_URL or "").strip()
+
+ container = None
+ try:
+ matches = client.containers.list(all=True, filters={"name": "cc-connect"})
+ if matches:
+ container = matches[0]
+ except Exception as exc:
+ return {
+ "ok": False,
+ "status": "down",
+ "source": "docker",
+ "container_name": None,
+ "container_status": None,
+ "endpoint": None,
+ "http_status": None,
+ "latency_ms": None,
+ "error": f"docker check failed: {exc}",
+ }
+
+ container_name = container.name if container else None
+ container_status = container.status if container else "not_found"
+ container_up = container_status == "running"
+
+ if not url:
+ return {
+ "ok": container_up,
+ "status": "up" if container_up else "down",
+ "source": "docker",
+ "container_name": container_name,
+ "container_status": container_status,
+ "endpoint": None,
+ "http_status": None,
+ "latency_ms": None,
+ "error": None if container_up else "cc-connect container is not running",
+ }
+
+ timeout = httpx.Timeout(2.0)
+ started = time.perf_counter()
+ try:
+ async with httpx.AsyncClient(timeout=timeout) as http_client:
+ response = await http_client.get(url)
+ latency_ms = round((time.perf_counter() - started) * 1000, 1)
+
+ if response.is_success and container_up:
+ return {
+ "ok": True,
+ "status": "up",
+ "source": "docker+http",
+ "container_name": container_name,
+ "container_status": container_status,
+ "endpoint": url,
+ "http_status": response.status_code,
+ "latency_ms": latency_ms,
+ "error": None,
+ }
+
+ if response.is_success and not container_up:
+ return {
+ "ok": False,
+ "status": "down",
+ "source": "docker+http",
+ "container_name": container_name,
+ "container_status": container_status,
+ "endpoint": url,
+ "http_status": response.status_code,
+ "latency_ms": latency_ms,
+ "error": "HTTP probe succeeded but cc-connect container is not running",
+ }
+
+ return {
+ "ok": False,
+ "status": "down",
+ "source": "docker+http",
+ "container_name": container_name,
+ "container_status": container_status,
+ "endpoint": url,
+ "http_status": response.status_code,
+ "latency_ms": latency_ms,
+ "error": f"HTTP probe returned {response.status_code}",
+ }
+ except Exception as exc:
+ return {
+ "ok": False,
+ "status": "down",
+ "source": "docker+http",
+ "container_name": container_name,
+ "container_status": container_status,
+ "endpoint": url,
+ "http_status": None,
+ "latency_ms": None,
+ "error": str(exc),
+ }
diff --git a/dashboard/frontend/src/App.svelte b/dashboard/frontend/src/App.svelte
index 16a0870..cdc57b6 100644
--- a/dashboard/frontend/src/App.svelte
+++ b/dashboard/frontend/src/App.svelte
@@ -10,6 +10,7 @@
import ChatSummary from "./routes/ChatSummary.svelte";
import Security from "./routes/Security.svelte";
import LiteLLM from "./routes/LiteLLM.svelte";
+ import CcConnect from "./routes/CcConnect.svelte";
import Login from "./routes/Login.svelte";
import { onMount } from "svelte";
import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser, getPreferences, savePreferences } from "./lib/api.js";
@@ -156,6 +157,8 @@
Mobile bridge operational status
+Failed to fetch cc-connect health
+{requestError}
+OK
+{String(Boolean(health.ok))}
+Source
+{valueOrDash(health.source)}
+Container
+{valueOrDash(health.container_name)}
+Container Status
+{valueOrDash(health.container_status)}
+Endpoint
+{valueOrDash(health.endpoint)}
+HTTP Status
+{valueOrDash(health.http_status)}
+Latency
+{health.latency_ms === null || health.latency_ms === undefined ? "—" : `${health.latency_ms} ms`}
+Error
+{valueOrDash(health.error)}
+No health data available.
+ {/if} +