功能修改
This commit is contained in:
+240
-144
@@ -1,4 +1,17 @@
|
||||
import { Activity, AlertTriangle, CakeSlice, Candy, Info, MoonStar, Sparkles } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Bot,
|
||||
CakeSlice,
|
||||
Candy,
|
||||
CircleGauge,
|
||||
Info,
|
||||
MoonStar,
|
||||
RotateCw,
|
||||
ServerCog,
|
||||
Sparkles,
|
||||
Workflow
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
@@ -12,26 +25,38 @@ import type {
|
||||
ServerMetricsResponse
|
||||
} from "../api/types";
|
||||
import { UsageMeter } from "../components/OperationControls";
|
||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||
import { jobBuckets, moduleFreshnessLabel, summarizeEndpointOperations, type OperationsModuleState } from "../contracts/operationsConsole";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import type { GameTypeDistributionEntry, PlatformOverviewSignal } from "../contracts/workspace";
|
||||
import { serverIsOnline } from "../contracts/workspace";
|
||||
import { cx } from "../utils/classes";
|
||||
|
||||
type ModuleState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
||||
|
||||
interface OverviewData {
|
||||
instances: ServerInstanceResponse[];
|
||||
endpoints: RunEndpointResponse[];
|
||||
jobs: JobResponse[];
|
||||
}
|
||||
|
||||
export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
const [core, setCore] = useState<ModuleState<OverviewData>>({ status: "loading" });
|
||||
const [metrics, setMetrics] = useState<ModuleState<ServerMetricsResponse[]>>({ status: "loading" });
|
||||
const [usage, setUsage] = useState<ModuleState<PlatformResourceUsageResponse>>({ status: "loading" });
|
||||
const [providers, setProviders] = useState<ModuleState<AiProviderResponse[]>>({ status: "loading" });
|
||||
const [signals, setSignals] = useState<ModuleState<AuditEventResponse[]>>({ status: "loading" });
|
||||
export interface HomePageInitialState {
|
||||
core?: OperationsModuleState<OverviewData>;
|
||||
metrics?: OperationsModuleState<ServerMetricsResponse[]>;
|
||||
usage?: OperationsModuleState<PlatformResourceUsageResponse>;
|
||||
providers?: OperationsModuleState<AiProviderResponse[]>;
|
||||
signals?: OperationsModuleState<AuditEventResponse[]>;
|
||||
}
|
||||
|
||||
interface HomePageProps extends PageComponentProps {
|
||||
initialState?: HomePageInitialState;
|
||||
}
|
||||
|
||||
export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
const [core, setCore] = useState<OperationsModuleState<OverviewData>>(initialState?.core ?? { status: "loading" });
|
||||
const [metrics, setMetrics] = useState<OperationsModuleState<ServerMetricsResponse[]>>(initialState?.metrics ?? { status: "loading" });
|
||||
const [usage, setUsage] = useState<OperationsModuleState<PlatformResourceUsageResponse>>(initialState?.usage ?? { status: "loading" });
|
||||
const [providers, setProviders] = useState<OperationsModuleState<AiProviderResponse[]>>(initialState?.providers ?? { status: "loading" });
|
||||
const [signals, setSignals] = useState<OperationsModuleState<AuditEventResponse[]>>(initialState?.signals ?? { status: "loading" });
|
||||
|
||||
const refreshCore = useCallback(async () => {
|
||||
setCore({ status: "loading" });
|
||||
@@ -41,9 +66,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
platformApiClient.listRunEndpoints(),
|
||||
platformApiClient.listJobs()
|
||||
]);
|
||||
setCore({ status: "ready", data: { instances: instances.items, endpoints: endpoints.items, jobs: jobs.items } });
|
||||
setCore({ status: "ready", data: { instances: instances.items, endpoints: endpoints.items, jobs: jobs.items }, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setCore({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
|
||||
setCore({ status: "error", reason: errorMessage(error, "服务器、节点或任务加载失败"), diagnosticId: "overview-core" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -51,9 +76,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
setMetrics({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listServerMetrics();
|
||||
setMetrics({ status: "ready", data: response.items });
|
||||
} catch {
|
||||
setMetrics({ status: "ready", data: [] });
|
||||
setMetrics({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setMetrics({ status: "error", reason: errorMessage(error, "服务器指标加载失败"), diagnosticId: "overview-server-metrics" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -61,9 +86,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
setUsage({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.getPlatformResourceUsage();
|
||||
setUsage({ status: "ready", data: response });
|
||||
} catch {
|
||||
setUsage({ status: "error", reason: "平台资源指标接口尚未提供" });
|
||||
setUsage({ status: "ready", data: response, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setUsage({ status: "error", reason: errorMessage(error, "平台资源指标加载失败"), diagnosticId: "overview-platform-usage" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -71,9 +96,9 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
setProviders({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listAiProviders();
|
||||
setProviders({ status: "ready", data: response.items });
|
||||
setProviders({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setProviders({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
|
||||
setProviders({ status: "error", reason: errorMessage(error, "AI 提供商状态加载失败"), diagnosticId: "overview-ai-providers" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -81,19 +106,26 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
setSignals({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listAuditEvents();
|
||||
setSignals({ status: "ready", data: response.items });
|
||||
} catch {
|
||||
setSignals({ status: "ready", data: [] });
|
||||
setSignals({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setSignals({ status: "error", reason: errorMessage(error, "审计事件加载失败"), diagnosticId: "overview-audit-events" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshAll = useCallback(() => {
|
||||
void refreshCore();
|
||||
void refreshMetrics();
|
||||
void refreshUsage();
|
||||
void refreshProviders();
|
||||
void refreshSignals();
|
||||
}, [refreshCore, refreshMetrics, refreshUsage, refreshProviders, refreshSignals]);
|
||||
}, [refreshCore, refreshMetrics, refreshProviders, refreshSignals, refreshUsage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
return;
|
||||
}
|
||||
refreshAll();
|
||||
}, [initialState, refreshAll]);
|
||||
|
||||
const distribution = useMemo<GameTypeDistributionEntry[]>(() => {
|
||||
if (core.status !== "ready") {
|
||||
@@ -106,73 +138,19 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
return [...counts.entries()].map(([serverType, count]) => ({ serverType, label: serverType, count }));
|
||||
}, [core]);
|
||||
|
||||
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => {
|
||||
const collected: PlatformOverviewSignal[] = [];
|
||||
if (core.status === "ready") {
|
||||
for (const instance of core.data.instances.filter((item) => item.state === "failed")) {
|
||||
collected.push({
|
||||
id: `fault-${instance.id}`,
|
||||
kind: "fault",
|
||||
summary: `服务器 ${instance.name} 处于异常状态`,
|
||||
detail: `实例 ${instance.id} 状态为 failed,需要排查`,
|
||||
targetPage: "servers",
|
||||
targetId: instance.id,
|
||||
tone: "error",
|
||||
at: instance.updatedAt
|
||||
});
|
||||
}
|
||||
for (const job of core.data.jobs.filter((item) => item.state === "failed").slice(0, 5)) {
|
||||
collected.push({
|
||||
id: `job-${job.id}`,
|
||||
kind: "job",
|
||||
summary: `任务 ${job.capability} 失败`,
|
||||
detail: `任务 ${job.id}${job.serverInstanceId ? `(服务器 ${job.serverInstanceId})` : ""} 执行失败`,
|
||||
targetPage: "servers",
|
||||
targetId: job.serverInstanceId,
|
||||
tone: "error",
|
||||
at: job.updatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
if (providers.status === "ready") {
|
||||
for (const provider of providers.data.filter((item) => item.status === "error")) {
|
||||
collected.push({
|
||||
id: `ai-${provider.id}`,
|
||||
kind: "aiProvider",
|
||||
summary: `AI 提供商 ${provider.name} 连接异常`,
|
||||
detail: `提供商 ${provider.id} 状态为 error,LLM 辅助可能不可用`,
|
||||
targetPage: "aiProviders",
|
||||
targetId: provider.id,
|
||||
tone: "warning",
|
||||
at: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
if (signals.status === "ready") {
|
||||
for (const event of signals.data.slice(0, 5)) {
|
||||
collected.push({
|
||||
id: `audit-${event.id}`,
|
||||
kind: "log",
|
||||
summary: event.summary || `${event.action} ${event.resourceKind}`,
|
||||
detail: `${event.actorId} 对 ${event.resourceKind}/${event.resourceId} 执行 ${event.action}(${event.result})`,
|
||||
targetPage: event.resourceKind === "server-instance" ? "servers" : "maintenance",
|
||||
targetId: event.resourceId,
|
||||
tone: event.result === "failure" ? "warning" : "info",
|
||||
at: event.createdAt
|
||||
});
|
||||
}
|
||||
}
|
||||
return collected.slice(0, 8);
|
||||
}, [core, providers, signals]);
|
||||
|
||||
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => buildOverviewSignals(core, providers, signals), [core, providers, signals]);
|
||||
const jobs = core.status === "ready" ? jobBuckets(core.data.jobs) : null;
|
||||
const endpointSummary = core.status === "ready" ? summarizeEndpointOperations(core.data.endpoints) : null;
|
||||
const onlineCount = core.status === "ready" ? core.data.instances.filter((item) => serverIsOnline(item.state)).length : 0;
|
||||
const offlineCount = core.status === "ready" ? core.data.instances.length - onlineCount : 0;
|
||||
const activeProviders = providers.status === "ready" ? providers.data.filter((item) => item.status === "active").length : 0;
|
||||
const errorProviders = providers.status === "ready" ? providers.data.filter((item) => item.status === "error").length : 0;
|
||||
const moduleFailureCount = [core, metrics, usage, providers, signals].filter((module) => module.status === "error").length;
|
||||
const canManageServers = session.capabilities.includes("servers.manage");
|
||||
|
||||
const metricAverages = useMemo(() => {
|
||||
if (usage.status === "ready") {
|
||||
return { cpu: usage.data.cpuPercent, memory: usage.data.memoryPercent, disk: usage.data.diskPercent, source: "平台指标" };
|
||||
return { cpu: usage.data.cpuPercent, memory: usage.data.memoryPercent, disk: usage.data.diskPercent, source: "平台指标", refreshedAt: usage.refreshedAt };
|
||||
}
|
||||
if (metrics.status === "ready" && metrics.data.length > 0) {
|
||||
const average = (values: Array<number | undefined>) => {
|
||||
@@ -183,38 +161,45 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
cpu: average(metrics.data.map((item) => item.cpuPercent)),
|
||||
memory: average(metrics.data.map((item) => item.memoryPercent)),
|
||||
disk: average(metrics.data.map((item) => item.diskPercent)),
|
||||
source: "按服务器均值"
|
||||
source: "服务器指标均值",
|
||||
refreshedAt: metrics.refreshedAt
|
||||
};
|
||||
}
|
||||
return { cpu: undefined, memory: undefined, disk: undefined, source: "暂无数据" };
|
||||
return { cpu: undefined, memory: undefined, disk: undefined, source: metrics.status === "error" || usage.status === "error" ? "指标不可用" : "等待指标", refreshedAt: undefined };
|
||||
}, [usage, metrics]);
|
||||
|
||||
return (
|
||||
<div className="console-page">
|
||||
<div className="console-page operations-overview-page">
|
||||
<header className="page-header">
|
||||
<div>
|
||||
<p className="page-kicker">平台管理员</p>
|
||||
<p className="page-kicker">运营指挥</p>
|
||||
<h1 className="page-title">平台概览</h1>
|
||||
</div>
|
||||
<span className={cx("page-status", core.status === "ready" && "page-status-ready")}>
|
||||
{core.status === "ready" ? "数据已加载" : core.status === "loading" ? "加载中" : "部分模块加载失败"}
|
||||
</span>
|
||||
<div className="page-header-actions">
|
||||
<span className={cx("page-status", moduleFailureCount === 0 && core.status === "ready" && "page-status-ready")}>
|
||||
{moduleFailureCount > 0 ? `${moduleFailureCount} 个模块不可用` : core.status === "ready" ? "运营数据已同步" : "正在同步"}
|
||||
</span>
|
||||
<button type="button" className="icon-command" onClick={refreshAll}>
|
||||
<RotateCw size={14} />
|
||||
全部刷新
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{core.status === "loading" && <LoadingState label="正在加载服务器与任务概况…" />}
|
||||
{core.status === "error" && <ErrorState title="平台概况加载失败" reason={core.reason} diagnosticId="overview-core" onRetry={() => void refreshCore()} />}
|
||||
{core.status === "loading" && <LoadingState label="正在加载服务器、节点与任务概况…" />}
|
||||
{core.status === "error" && <ErrorState title="平台核心概况不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} />}
|
||||
{core.status === "ready" && core.data.instances.length === 0 && (
|
||||
<EmptyState
|
||||
icon={<CakeSlice size={26} />}
|
||||
title="还没有服务器实例"
|
||||
description="平台尚未创建任何服务器。前往服务器管理创建第一个实例,或检查运行节点是否在线。"
|
||||
description={canManageServers ? "平台尚未创建服务器。先确认运行节点在线,再创建第一个实例。" : "当前账号可查看概览,但没有创建服务器的权限。"}
|
||||
actionLabel="前往服务器管理"
|
||||
onAction={() => onNavigate("servers")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{core.status === "ready" && core.data.instances.length > 0 && (
|
||||
<section className="console-grid" aria-label="platform health">
|
||||
{core.status === "ready" && (
|
||||
<section className="console-grid operations-summary-grid" aria-label="平台运营摘要">
|
||||
<article className="overview-card metric-tone-success">
|
||||
<span className="metric-label">在线服务器</span>
|
||||
<strong className="metric-value">{onlineCount}</strong>
|
||||
@@ -225,53 +210,128 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
<strong className="metric-value">{offlineCount}</strong>
|
||||
<p>{core.data.instances.filter((item) => item.state === "failed").length} 个异常</p>
|
||||
</article>
|
||||
<article className="overview-card metric-tone-neutral">
|
||||
<article className={cx("overview-card", endpointSummary && endpointSummary.degraded + endpointSummary.offline > 0 ? "metric-tone-warning" : "metric-tone-neutral")}>
|
||||
<span className="metric-label">运行节点</span>
|
||||
<strong className="metric-value">{core.data.endpoints.filter((item) => item.status === "online").length}</strong>
|
||||
<p>共 {core.data.endpoints.length} 个节点</p>
|
||||
<strong className="metric-value">{endpointSummary?.online ?? 0} 在线</strong>
|
||||
<p>{endpointSummary ? `${endpointSummary.degraded} 降级,${endpointSummary.offline} 离线` : "节点状态不可用"}</p>
|
||||
</article>
|
||||
<article className={cx("overview-card", errorProviders > 0 ? "metric-tone-warning" : "metric-tone-success")}>
|
||||
<span className="metric-label">LLM 连接</span>
|
||||
<strong className="metric-value">
|
||||
{providers.status === "ready" ? `${activeProviders} 可用` : providers.status === "loading" ? "…" : "未知"}
|
||||
</strong>
|
||||
<p>{errorProviders > 0 ? `${errorProviders} 个提供商异常` : "提供商状态正常"}</p>
|
||||
<article className={cx("overview-card", providers.status === "error" || errorProviders > 0 ? "metric-tone-warning" : "metric-tone-success")}>
|
||||
<span className="metric-label">AI 提供商</span>
|
||||
<strong className="metric-value">{providers.status === "ready" ? `${activeProviders} 可用` : providers.status === "loading" ? "加载中" : "不可用"}</strong>
|
||||
<p>{providers.status === "error" ? "提供商状态接口失败" : errorProviders > 0 ? `${errorProviders} 个连接异常` : "连接状态正常"}</p>
|
||||
</article>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="operations-command-grid" aria-label="平台运营模块">
|
||||
<article className="console-panel operations-module" aria-label="任务脉冲">
|
||||
<div className="panel-header">
|
||||
<h2><Workflow size={16} /> 任务脉冲</h2>
|
||||
<span className="page-status">{moduleFreshnessLabel(core.status === "ready" ? core.refreshedAt : core.refreshedAt)}</span>
|
||||
</div>
|
||||
{core.status === "loading" && <LoadingState label="正在汇总任务…" compact />}
|
||||
{core.status === "error" && <ErrorState title="任务状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
||||
{core.status === "ready" && jobs && (
|
||||
<>
|
||||
<dl className="operations-pulse-strip">
|
||||
<div><dt>执行中</dt><dd>{jobs.active.length}</dd></div>
|
||||
<div><dt>等待重试</dt><dd>{jobs.retrying.length}</dd></div>
|
||||
<div><dt>近期失败</dt><dd>{jobs.failed.length}</dd></div>
|
||||
</dl>
|
||||
{jobs.active.length === 0 && jobs.failed.length === 0 ? (
|
||||
<p className="operations-module-empty">当前没有执行中或失败任务。</p>
|
||||
) : (
|
||||
<div className="operations-job-list">
|
||||
{[...jobs.active.slice(0, 3), ...jobs.failed.slice(0, 2)].map((job) => (
|
||||
<button
|
||||
key={job.id}
|
||||
type="button"
|
||||
className={cx("operations-job-row", job.state === "failed" && "operations-job-row-failed")}
|
||||
onClick={() => onNavigate(job.serverInstanceId ? "serverDetail" : "maintenance", job.serverInstanceId ? { serverId: job.serverInstanceId } : undefined)}
|
||||
>
|
||||
<span><strong>{job.capability}</strong><small>{job.serverInstanceId ? `服务器 ${job.serverInstanceId}` : "平台任务"}</small></span>
|
||||
<span>{job.state === "retrying" ? `重试 ${job.attempt}/${job.retryPolicy.maxAttempts}` : jobStateLabel(job.state)}</span>
|
||||
<span>{Math.round(job.progress.percent)}%</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<article className="console-panel operations-module" aria-label="运行节点状态">
|
||||
<div className="panel-header">
|
||||
<h2><ServerCog size={16} /> 运行节点</h2>
|
||||
<button type="button" className="icon-command" onClick={() => onNavigate("maintenance")}><CircleGauge size={14} /> 排障</button>
|
||||
</div>
|
||||
{core.status === "loading" && <LoadingState label="正在汇总节点…" compact />}
|
||||
{core.status === "error" && <ErrorState title="节点状态不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />}
|
||||
{core.status === "ready" && endpointSummary && (
|
||||
<>
|
||||
<dl className="operations-pulse-strip">
|
||||
<div><dt>在线</dt><dd>{endpointSummary.online}</dd></div>
|
||||
<div><dt>活跃任务</dt><dd>{endpointSummary.activeJobs}</dd></div>
|
||||
<div><dt>排队</dt><dd>{endpointSummary.queuedJobs}</dd></div>
|
||||
</dl>
|
||||
{core.data.endpoints.length === 0 ? (
|
||||
<p className="operations-module-empty">Platform 未返回运行节点。</p>
|
||||
) : (
|
||||
<div className="operations-endpoint-list">
|
||||
{core.data.endpoints.slice(0, 4).map((endpoint) => (
|
||||
<div key={endpoint.id} className="operations-endpoint-row">
|
||||
<span><strong>{endpoint.displayName}</strong><small>{endpoint.version}</small></span>
|
||||
<span className={cx("status-pill", `status-${endpoint.status}`)}>{endpointStatusLabel(endpoint.status)}</span>
|
||||
<span>{endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs} 运行</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<ProductionGovernancePanel compact title="生产容量与告警" />
|
||||
|
||||
<section className="overview-two-col">
|
||||
<article className="console-panel" aria-label="resource usage">
|
||||
<div className="panel-header">
|
||||
<h2>
|
||||
<Activity size={16} style={{ verticalAlign: "-2px" }} /> 资源负载
|
||||
</h2>
|
||||
<h2><Activity size={16} /> 资源负载</h2>
|
||||
<span className="page-status">{metricAverages.source}</span>
|
||||
</div>
|
||||
<div className="server-card-meters">
|
||||
<UsageMeter label="CPU" percent={metricAverages.cpu} />
|
||||
<UsageMeter label="内存" percent={metricAverages.memory} />
|
||||
<UsageMeter label="磁盘" percent={metricAverages.disk} />
|
||||
</div>
|
||||
{usage.status === "loading" && metrics.status === "loading" ? (
|
||||
<LoadingState label="正在读取资源指标…" compact />
|
||||
) : usage.status === "error" && metrics.status === "error" ? (
|
||||
<ErrorState title="资源指标不可用" reason={`${usage.reason};${metrics.reason}`} diagnosticId="overview-resource-usage" onRetry={() => { void refreshUsage(); void refreshMetrics(); }} compact />
|
||||
) : (
|
||||
<>
|
||||
<div className="server-card-meters">
|
||||
<UsageMeter label="CPU" percent={metricAverages.cpu} />
|
||||
<UsageMeter label="内存" percent={metricAverages.memory} />
|
||||
<UsageMeter label="磁盘" percent={metricAverages.disk} />
|
||||
</div>
|
||||
<span className="operations-module-freshness">{moduleFreshnessLabel(metricAverages.refreshedAt)}</span>
|
||||
{(usage.status === "error" || metrics.status === "error") && (
|
||||
<button type="button" className="operations-inline-warning" onClick={() => { void refreshUsage(); void refreshMetrics(); }}>
|
||||
<AlertTriangle size={14} /> 部分指标源不可用,点击重试
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<article className="console-panel" aria-label="game type distribution">
|
||||
<div className="panel-header">
|
||||
<h2>
|
||||
<Candy size={16} style={{ verticalAlign: "-2px" }} /> 游戏类型分布
|
||||
</h2>
|
||||
</div>
|
||||
<div className="panel-header"><h2><Candy size={16} /> 游戏类型分布</h2></div>
|
||||
{core.status === "loading" ? (
|
||||
<LoadingState label="统计中…" compact />
|
||||
) : core.status === "error" ? (
|
||||
<ErrorState title="分布数据不可用" reason={core.reason} diagnosticId={core.diagnosticId} onRetry={() => void refreshCore()} compact />
|
||||
) : distribution.length === 0 ? (
|
||||
<EmptyState title="暂无分布数据" description="创建服务器后这里会显示各游戏类型的实例数量。" />
|
||||
) : (
|
||||
<div className="action-list">
|
||||
{distribution.map((entry) => (
|
||||
<span key={entry.serverType}>
|
||||
<strong>{entry.label}</strong>:{entry.count} 个实例
|
||||
</span>
|
||||
))}
|
||||
{distribution.map((entry) => <span key={entry.serverType}><strong>{entry.label}</strong>:{entry.count} 个实例</span>)}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
@@ -279,17 +339,16 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
|
||||
<section className="console-panel" aria-label="recent signals">
|
||||
<div className="panel-header">
|
||||
<h2>
|
||||
<MoonStar size={16} style={{ verticalAlign: "-2px" }} /> 最近运营信号
|
||||
</h2>
|
||||
<button type="button" className="icon-command" onClick={() => void refreshSignals()}>
|
||||
<Sparkles size={14} />
|
||||
刷新
|
||||
</button>
|
||||
<h2><MoonStar size={16} /> 最近运营信号</h2>
|
||||
<div className="panel-header-actions">
|
||||
<span className="page-status">{moduleFreshnessLabel(signals.status === "ready" ? signals.refreshedAt : signals.refreshedAt)}</span>
|
||||
<button type="button" className="icon-command" onClick={() => void refreshSignals()}><Sparkles size={14} /> 刷新审计</button>
|
||||
</div>
|
||||
</div>
|
||||
{signals.status === "loading" && core.status === "loading" ? (
|
||||
<LoadingState label="正在收集信号…" compact />
|
||||
) : overviewSignals.length === 0 ? (
|
||||
{signals.status === "loading" && core.status === "loading" && <LoadingState label="正在收集信号…" compact />}
|
||||
{signals.status === "error" && <ErrorState title="审计信号不可用" reason={signals.reason} diagnosticId={signals.diagnosticId} onRetry={() => void refreshSignals()} compact />}
|
||||
{providers.status === "error" && <ErrorState title="AI 提供商信号不可用" reason={providers.reason} diagnosticId={providers.diagnosticId} onRetry={() => void refreshProviders()} compact />}
|
||||
{overviewSignals.length === 0 && signals.status === "ready" && core.status === "ready" && providers.status === "ready" ? (
|
||||
<EmptyState title="暂无异常信号" description="最近没有故障、失败任务或需要关注的审计事件。" />
|
||||
) : (
|
||||
<div className="signal-list">
|
||||
@@ -298,18 +357,10 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
key={signal.id}
|
||||
type="button"
|
||||
className={cx("signal-item", `signal-tone-${signal.tone}`)}
|
||||
onClick={() =>
|
||||
onNavigate(
|
||||
signal.targetPage === "servers" && signal.targetId ? "serverDetail" : signal.targetPage,
|
||||
signal.targetPage === "servers" && signal.targetId ? { serverId: signal.targetId } : undefined
|
||||
)
|
||||
}
|
||||
onClick={() => onNavigate(signal.targetPage === "servers" && signal.targetId ? "serverDetail" : signal.targetPage, signal.targetPage === "servers" && signal.targetId ? { serverId: signal.targetId } : undefined)}
|
||||
>
|
||||
{signal.tone === "error" ? <AlertTriangle size={18} /> : signal.tone === "warning" ? <AlertTriangle size={18} /> : <Info size={18} />}
|
||||
<span>
|
||||
<strong>{signal.summary}</strong>
|
||||
<p>{signal.detail}</p>
|
||||
</span>
|
||||
{signal.tone === "error" || signal.tone === "warning" ? <AlertTriangle size={18} /> : signal.kind === "aiProvider" ? <Bot size={18} /> : <Info size={18} />}
|
||||
<span><strong>{signal.summary}</strong><p>{signal.detail}</p></span>
|
||||
<span className="provider-id">{signal.at ? new Date(signal.at).toLocaleString() : ""}</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -319,3 +370,48 @@ export function HomePage({ onNavigate }: PageComponentProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildOverviewSignals(
|
||||
core: OperationsModuleState<OverviewData>,
|
||||
providers: OperationsModuleState<AiProviderResponse[]>,
|
||||
signals: OperationsModuleState<AuditEventResponse[]>
|
||||
): PlatformOverviewSignal[] {
|
||||
const collected: PlatformOverviewSignal[] = [];
|
||||
if (core.status === "ready") {
|
||||
for (const instance of core.data.instances.filter((item) => item.state === "failed")) {
|
||||
collected.push({ id: `fault-${instance.id}`, kind: "fault", summary: `服务器 ${instance.name} 处于异常状态`, detail: `实例 ${instance.id} 需要排查`, targetPage: "servers", targetId: instance.id, tone: "error", at: instance.updatedAt });
|
||||
}
|
||||
for (const job of jobBuckets(core.data.jobs).failed.slice(0, 5)) {
|
||||
collected.push({ id: `job-${job.id}`, kind: "job", summary: `任务 ${job.capability} 失败`, detail: job.serverInstanceId ? `服务器 ${job.serverInstanceId} 的任务执行失败` : "平台任务执行失败", targetPage: job.serverInstanceId ? "servers" : "maintenance", targetId: job.serverInstanceId, tone: "error", at: job.updatedAt });
|
||||
}
|
||||
}
|
||||
if (providers.status === "ready") {
|
||||
for (const provider of providers.data.filter((item) => item.status === "error")) {
|
||||
collected.push({ id: `ai-${provider.id}`, kind: "aiProvider", summary: `AI 提供商 ${provider.name} 连接异常`, detail: "LLM 辅助暂不可用,请在提供商管理中检查已保存配置。", targetPage: "aiProviders", targetId: provider.id, tone: "warning", at: "" });
|
||||
}
|
||||
}
|
||||
if (signals.status === "ready") {
|
||||
for (const event of signals.data.slice(0, 5)) {
|
||||
collected.push({ id: `audit-${event.id}`, kind: "log", summary: event.summary || `${event.action} ${event.resourceKind}`, detail: `${event.resourceKind}/${event.resourceId}:${event.result}`, targetPage: event.resourceKind === "server-instance" ? "servers" : "maintenance", targetId: event.resourceId, tone: event.result === "failure" ? "warning" : "info", at: event.createdAt });
|
||||
}
|
||||
}
|
||||
return collected.sort((left, right) => Date.parse(right.at || "") - Date.parse(left.at || "")).slice(0, 8);
|
||||
}
|
||||
|
||||
function refreshedNow(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown, fallback: string): string {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
|
||||
function jobStateLabel(state: JobResponse["state"]): string {
|
||||
const labels: Record<JobResponse["state"], string> = { queued: "排队", accepted: "已领取", running: "执行中", retrying: "等待重试", succeeded: "完成", failed: "失败", cancelled: "已取消" };
|
||||
return labels[state];
|
||||
}
|
||||
|
||||
function endpointStatusLabel(status: RunEndpointResponse["status"]): string {
|
||||
const labels: Record<RunEndpointResponse["status"], string> = { online: "在线", offline: "离线", degraded: "降级", disabled: "停用" };
|
||||
return labels[status];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user