feat: add gateway health status indicator on bot dashboard

- Add /api/gateway-health endpoint to check gateway service status
- Add GatewayStatus component with 10s auto-polling
- Show green checkmark when healthy, red cross with error details when down
- Click gateway badge to open OpenClaw web chat page
- Fix variable name collision in alert-monitor.tsx (setInterval shadowing)
- Add i18n translations for gateway status (zh/en)
This commit is contained in:
xmanrui
2026-02-24 14:54:41 +08:00
parent 5ea1d38e43
commit 9298838cf5
5 changed files with 116 additions and 1 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
// 后台告警检查组件 - 服务启动时自动开始
export function AlertMonitor() {
const [enabled, setEnabled] = useState(false);
const [interval, setInterval] = useState(10);
const [checkInterval, setCheckInterval] = useState(10);
const [lastResults, setLastResults] = useState<string[]>([]);
useEffect(() => {
+39
View File
@@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
export async function GET() {
try {
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
const config = JSON.parse(raw);
const port = config.gateway?.port || 18789;
const token = config.gateway?.auth?.token || "";
const url = `http://localhost:${port}/api/health`;
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = `Bearer ${token}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const resp = await fetch(url, { headers, signal: controller.signal });
clearTimeout(timeout);
if (!resp.ok) {
return NextResponse.json({ ok: false, error: `HTTP ${resp.status}` });
}
const data = await resp.json().catch(() => null);
return NextResponse.json({ ok: true, data, webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}` });
} catch (err: any) {
const msg = err.cause?.code === "ECONNREFUSED"
? "Gateway 未运行"
: err.name === "AbortError"
? "请求超时"
: err.message;
return NextResponse.json({ ok: false, error: msg });
}
}
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useI18n } from "@/lib/i18n";
interface HealthResult {
ok: boolean;
error?: string;
data?: any;
webUrl?: string;
}
export function GatewayStatus() {
const { t } = useI18n();
const [health, setHealth] = useState<HealthResult | null>(null);
const [showError, setShowError] = useState(false);
const check = useCallback(() => {
fetch("/api/gateway-health")
.then((r) => r.json())
.then((d) => setHealth(d))
.catch(() => setHealth({ ok: false, error: t("gateway.fetchError") }));
}, [t]);
useEffect(() => {
check();
const timer = setInterval(check, 10000);
return () => clearInterval(timer);
}, [check]);
if (!health) return null;
return (
<div className="relative inline-flex items-center gap-2">
<a
href={health.ok && health.webUrl ? health.webUrl : undefined}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-cyan-500/20 text-cyan-300 border-cyan-500/30 hover:bg-cyan-500/30 transition-colors cursor-pointer"
>
🌐 Gateway
<span className="opacity-50 text-[10px]"></span>
</a>
{health.ok ? (
<span className="text-green-400 text-sm cursor-help" title={t("gateway.healthy")}></span>
) : (
<span
className="text-red-400 text-sm cursor-pointer"
title={health.error || t("gateway.unhealthy")}
onClick={() => setShowError((v) => !v)}
></span>
)}
{showError && !health.ok && health.error && (
<div className="absolute top-full left-0 mt-1 z-50 px-3 py-2 rounded-lg bg-red-500/15 border border-red-500/30 text-red-300 text-xs max-w-64 whitespace-pre-wrap shadow-lg">
{health.error}
</div>
)}
</div>
);
}
+6
View File
@@ -2,6 +2,7 @@
import { useEffect, useState, useCallback, useRef } from "react";
import { useI18n } from "@/lib/i18n";
import { GatewayStatus } from "./gateway-status";
interface Platform {
name: string;
@@ -611,6 +612,11 @@ export default function Home() {
</div>
</div>
{/* Gateway 状态 */}
<div className="mb-4">
<GatewayStatus />
</div>
{/* 卡片墙 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.agents.map((agent) => (
+10
View File
@@ -184,6 +184,11 @@ const translations: Record<Locale, Record<string, string>> = {
"skills.noDesc": "无描述",
"skills.source.builtin": "内置",
"skills.source.custom": "自定义",
// gateway status
"gateway.healthy": "Gateway 运行正常",
"gateway.unhealthy": "Gateway 异常",
"gateway.fetchError": "无法检查 Gateway 状态",
},
en: {
// layout
@@ -364,6 +369,11 @@ const translations: Record<Locale, Record<string, string>> = {
"skills.noDesc": "No description",
"skills.source.builtin": "Built-in",
"skills.source.custom": "Custom",
// gateway status
"gateway.healthy": "Gateway is running",
"gateway.unhealthy": "Gateway is down",
"gateway.fetchError": "Cannot check Gateway status",
},
};