"use client"; import { useEffect, useState } from "react"; import { Activity, AlertTriangle, RefreshCcw, ShieldCheck, Database, Cpu, HardDrive, RadioTower } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { opsApi } from "@/lib/ops-api"; import type { ObservationCollectorStatusPayload, SourceHealthPayload, SystemStatusPayload, HealthPayload, } from "@/types/ops"; function sourceStatusTone(status?: string) { if (status === "fresh") return "text-emerald-500"; if (status === "expected_wait") return "text-blue-500"; if (status === "delayed") return "text-amber-500"; if (status === "stale" || status === "missing") return "text-red-500"; return "text-slate-500"; } function sourceStatusLabel(status?: string) { if (status === "fresh") return "正常"; if (status === "expected_wait") return "等待更新"; if (status === "delayed") return "延迟"; if (status === "stale") return "断线"; if (status === "missing") return "缺失"; return "未知"; } function collectorStatusTone(status?: string) { if (status === "ok") return "text-emerald-500"; if (status === "due") return "text-blue-500"; if (status === "cooldown") return "text-amber-500"; if (status === "failed" || status === "never_run") return "text-red-500"; return "text-slate-500"; } function collectorStatusLabel(status?: string) { if (status === "ok") return "正常"; if (status === "due") return "到期"; if (status === "cooldown") return "冷却"; if (status === "failed") return "失败"; if (status === "never_run") return "未采集"; return "未知"; } function formatAge(ageMin?: number | null) { if (ageMin == null) return "—"; if (ageMin < 60) return `${Math.round(ageMin)}m`; return `${(ageMin / 60).toFixed(1)}h`; } function formatSeconds(seconds?: number | null) { if (seconds == null) return "—"; if (seconds <= 0) return "已到期"; if (seconds < 60) return `${Math.round(seconds)}s`; if (seconds < 3600) return `${Math.round(seconds / 60)}m`; return `${(seconds / 3600).toFixed(1)}h`; } function formatLatency(ms?: number | null) { if (ms == null) return "—"; if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(1)}s`; } function formatTimestamp(value?: string | null) { if (!value) return "—"; return value.replace("T", " ").replace("Z", "").slice(0, 19); } function sourceReasonLabel(reason?: string | null) { const key = String(reason || "").trim().toLowerCase(); if (key === "observation_time_missing") return "观测时间缺失"; if (key === "expected_source_not_present_in_cached_detail") return "缓存详情未包含预期来源"; if (key === "past_expected_cadence") return "超过预期更新节奏"; if (key === "within_expected_cadence") return "仍在预期更新窗口"; if (key === "missing_observation") return "缺少观测值"; if (key === "no_cached_detail") return "缺少城市详情缓存"; return key || "—"; } function formatOpsValue(value: unknown) { if (typeof value === "boolean") { return { label: value ? "TRUE" : "FALSE", active: value }; } if (typeof value === "string" || typeof value === "number") { return { label: String(value), active: Boolean(value) }; } if (Array.isArray(value)) { return { label: `${value.length} 项`, active: value.length > 0 }; } if (value && typeof value === "object") { const entries = Object.values(value as Record); const enabled = entries.filter(Boolean).length; return { label: `${enabled}/${entries.length} 已配置`, active: enabled > 0 }; } return { label: "—", active: false }; } export function SystemPageClient() { const [loading, setLoading] = useState(true); const [health, setHealth] = useState(null); const [status, setStatus] = useState(null); const [sourceHealth, setSourceHealth] = useState(null); const [collectorStatus, setCollectorStatus] = useState(null); const [error, setError] = useState(""); const load = async () => { setLoading(true); setError(""); try { const [h, s, sh, cs] = await Promise.all([ opsApi.health(), opsApi.systemStatus() as Promise, opsApi.sourceHealth(80) as Promise, opsApi.observationCollectorStatus(200) as Promise, ]); setHealth(h); setStatus(s); setSourceHealth(sh); setCollectorStatus(cs); } catch (e) { setError(String(e).slice(0, 200)); } finally { setLoading(false); } }; useEffect(() => { void load(); }, []); if (loading) { return
加载中...
; } if (error) { return
加载失败: {error}
; } const dbOk = status?.db?.ok ?? health?.db?.ok; const cacheAnalysis = status?.cache?.analysis; const collectorIssues = (collectorStatus?.entries || []) .filter((entry) => { const state = String(entry.status || ""); return ["failed", "cooldown", "never_run", "due"].includes(state) || (entry.failure_count ?? 0) > 0; }) .slice(0, 12); const sourceIssues = (sourceHealth?.cities || []) .flatMap((city) => (city.sources || []) .filter((source) => ["delayed", "stale", "missing", "unknown"].includes(String(source.status || ""))) .map((source) => ({ city: city.city, source })), ) .slice(0, 10); return (

系统状态

{/* Health badges */}
Health
{health?.status ?? "—"}
Database
{dbOk ? "OK" : "FAIL"}
存储模式
{status?.state_storage_mode ?? "—"}
概率引擎
{status?.probability?.engine_mode ?? "—"}
{/* Features & Integrations */}
功能开关
{status?.features ? Object.entries(status.features).map(([k, v]) => { const formatted = formatOpsValue(v); return (
{k} {formatted.label}
); }) : 无数据}
集成状态
{status?.integrations ? Object.entries(status.integrations).map(([k, v]) => { const formatted = formatOpsValue(v); return (
{k} {formatted.label}
); }) : 无数据}
{/* Cache Analysis */} {cacheAnalysis ? ( 缓存分析
总请求
{cacheAnalysis.total_requests ?? 0}
命中
{cacheAnalysis.cache_hits ?? 0}
未命中
{cacheAnalysis.cache_misses ?? 0}
强制刷新
{cacheAnalysis.force_refresh_requests ?? 0}
命中率
{cacheAnalysis.hit_rate != null ? `${(cacheAnalysis.hit_rate * 100).toFixed(0)}%` : "—"}
这里统计当前后端进程的分析缓存请求;部署重启后会重新累计。
) : null} 观测采集器
{["ok", "due", "cooldown", "failed", "never_run"].map((key) => (
{collectorStatusLabel(key)}
{collectorStatus?.status_counts?.[key] ?? 0}
))}
{(collectorStatus?.sources || []).length ? (
{(collectorStatus?.sources || []).map((source) => (
{source.source} {collectorStatusLabel(source.worst_status)}
城市 {source.city_count ?? 0} 间隔 {source.min_interval_sec ?? source.interval_sec ?? "—"}s 失败 {source.failure_count ?? 0} 冷却 {source.cooldown_count ?? 0} 延迟 {formatLatency(source.avg_latency_ms)} 成功 {formatTimestamp(source.last_success_at)}
))}
) : (
暂无后台采集状态;collector 首次写入后会显示每个 source/city 的最近采集时间、失败次数、延迟和冷却状态。
)} {collectorIssues.length ? (
{collectorIssues.map((entry) => ( ))}
Source 城市 状态 最近成功 失败次数 延迟 下次 错误
{entry.source} {entry.city} {collectorStatusLabel(entry.status)} {formatTimestamp(entry.last_success_at)} {entry.failure_count ?? 0} {formatLatency(entry.last_latency_ms)} {formatSeconds(entry.due_in_sec)} {entry.last_error || "—"}
) : (
当前后台采集器没有失败、冷却或到期积压。
)}
城市数据源健康
{["fresh", "expected_wait", "delayed", "stale", "missing"].map((key) => (
{sourceStatusLabel(key)}
{sourceHealth?.status_counts?.[key] ?? 0}
))}
{sourceIssues.length ? (
{sourceIssues.map(({ city, source }, index) => ( ))}
城市 来源 状态 延迟 最近观测 原因
{city}
{source.source_label || source.source_code}
{source.role}
{sourceStatusLabel(source.status)} {formatAge(source.age_min)} {source.observed_at || "—"} {sourceReasonLabel(source.reason)}
) : (
当前缓存内未发现 MGM、KNMI、IMS 或机场站断线/延迟异常。
)} {(sourceHealth?.cities || []).some((city) => !city.cache_exists) ? (
有城市缺少 full/panel 缓存,可能是后台冷启动或该城市尚未预热。
) : null}
{/* DB Path */} {status?.db?.db_path ? ( 数据库路径 {status.db.db_path} ) : null}
); }