refactor: move ops triage cards to ops page

This commit is contained in:
boos
2026-06-29 15:47:35 +08:00
parent 0515b90b6c
commit 535fa988ef
4 changed files with 185 additions and 170 deletions
-146
View File
@@ -1,7 +1,6 @@
import { type AnchorHTMLAttributes, type KeyboardEvent as ReactKeyboardEvent, type ReactNode, Suspense, lazy, useEffect, useState, useCallback } from "react";
import { Link, useInRouterContext } from "react-router-dom";
import { api } from "../api.js";
import DiagnosticActionGroup from "../components/DiagnosticActionGroup.js";
import { useToast } from "../components/Toast.js";
import { useIsMobile } from "../components/useIsMobile.js";
import { useOptionalI18n } from "../i18n.js";
@@ -657,16 +656,6 @@ export default function Dashboard({
: overallHealthTone === "warning"
? t("需关注")
: t("异常");
const spendTone =
todaySpend <= todayReward
? "success"
: todaySpend <= Math.max(1, todayReward * 2)
? "warning"
: "danger";
const throughputLabel =
requestsPerMinute > 0
? `${Math.round(requestsPerMinute).toLocaleString()} RPM`
: t("暂无流量");
const avgActiveSiteLatency = activeSites.length > 0
? Math.round(
activeSites.reduce(
@@ -703,69 +692,6 @@ export default function Dashboard({
tone: requestsPerMinute > 0 ? "primary" : "muted",
},
];
const operationsPulse = [
{
label: t("整体状态"),
value: overallHealthLabel,
note: `${proxySuccessRate.toFixed(1)}% ${t("成功率")}`,
tone: overallHealthTone,
},
{
label: t("站点风险"),
value: `${siteRiskCount}`,
note: `${activeSites.length}/${rawSiteAvailability.length} ${t("活跃站点")}`,
tone: siteRiskCount > 0 ? "warning" : "success",
},
{
label: t("今日成本"),
value: `$${todaySpend.toFixed(2)}`,
note: `${t("奖励")} $${todayReward.toFixed(2)}`,
tone: spendTone,
},
{
label: t("当前吞吐"),
value: throughputLabel,
note: `${formatCompactTokenMetric(tokensPerMinute)} TPM`,
tone: requestsPerMinute > 0 ? "primary" : "muted",
},
];
const dashboardCommandLinks = [
{
to: "/ops",
label: t("运维监控"),
value: overallHealthLabel,
note: t("值班入口"),
tone: overallHealthTone,
},
{
to: "/routes",
label: t("路由健康"),
value: routeHealthItems.length > 0 ? String(routeRiskCount) : t("暂无"),
note: routeHealthItems.length > 0 ? (routeRiskCount > 0 ? t("需处理路由") : t("配置正常")) : t("等待路由"),
tone: routeHealthItems.length > 0 ? (routeHealthCounts.danger > 0 ? "danger" : routeRiskCount > 0 ? "warning" : "success") : "muted",
},
{
to: "/accounts",
label: t("连接管理"),
value: `${Math.round(activeAccounts)}/${Math.round(totalAccounts)}`,
note: t("账号健康"),
tone: activeAccountRate >= 80 ? "success" : "warning",
},
{
to: "/sites",
label: t("站点管理"),
value: String(siteRiskCount),
note: siteRiskCount > 0 ? t("需关注站点") : t("风险站点"),
tone: siteRiskCount > 0 ? "warning" : "success",
},
{
to: "/ip-management",
label: t("IP 管理"),
value: siteRiskCount > 0 ? t("检查") : t("正常"),
note: t("代理与出口"),
tone: siteRiskCount > 0 ? "warning" : "success",
},
];
const getLatencyColor = (ms: number) =>
ms <= 500
? "var(--color-success)"
@@ -887,78 +813,6 @@ export default function Dashboard({
</div>
</section>
<section className="dashboard-war-room" aria-label={t("运营战情室")}>
<div className="dashboard-pulse-grid">
{operationsPulse.map((item) => (
<div className="dashboard-pulse-card" data-tone={item.tone} key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
<em>{item.note}</em>
</div>
))}
</div>
<div className="dashboard-risk-panel">
<div className="dashboard-risk-head">
<div>
<span>{t("风险雷达")}</span>
<strong>{riskySites.length > 0 ? t("优先检查这些站点") : t("暂无明显站点风险")}</strong>
</div>
<DashboardLink to="/sites" className="btn btn-ghost">{t("站点管理")}</DashboardLink>
</div>
{riskySites.length > 0 ? (
<div className="dashboard-risk-list">
{riskySites.map((site) => (
<article
key={site.siteId}
className="dashboard-risk-row"
>
<div className="dashboard-risk-row-main">
<strong>{site.siteName}</strong>
<span>{formatAvailabilityPercent(site.availabilityPercent)}</span>
<em>{t(formatDashboardLatency(site.averageLatencyMs))}</em>
</div>
<DiagnosticActionGroup
className="dashboard-risk-actions"
actions={[
{
label: t("运维监控"),
href: buildSiteLast24hOpsRoute(site.siteId),
hint: t("查看该站点 24h 请求"),
tone: (site.availabilityPercent ?? 100) < 95 ? "danger" : "primary",
},
{
label: t("站点管理"),
href: "/sites",
hint: t("检查代理和模型配置"),
},
]}
/>
</article>
))}
</div>
) : (
<div className="dashboard-risk-empty">
{t("站点可用性、平均延迟和失败数都处于可接受范围。")}
</div>
)}
</div>
</section>
<nav className="dashboard-command-strip" aria-label={t("运营快捷入口")}>
{dashboardCommandLinks.map((item) => (
<DashboardLink
key={item.to}
to={item.to}
className="dashboard-command-item"
data-tone={item.tone}
>
<span>{item.label}</span>
<strong>{item.value}</strong>
<em>{item.note}</em>
</DashboardLink>
))}
</nav>
<div className="dashboard-stat-grid">
<div className="stat-card animate-slide-up stagger-1">
<div className="stat-card-header">
+147
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { api } from '../api.js';
import DiagnosticActionGroup from '../components/DiagnosticActionGroup.js';
import { useOptionalI18n } from '../i18n.js';
import { formatCompactTokenMetric } from '../numberFormat.js';
import { formatDateTimeMinuteLocal } from './helpers/checkinLogTime.js';
@@ -187,6 +188,8 @@ export default function OpsMonitor() {
const successRate = proxy24hTotal > 0 ? (proxy24hSuccess / proxy24hTotal) * 100 : 100;
const requestsPerMinute = safeNumber(dashboard?.performance?.requestsPerMinute);
const tokensPerMinute = safeNumber(dashboard?.performance?.tokensPerMinute);
const todaySpend = safeNumber(dashboard?.todaySpend);
const todayReward = safeNumber(dashboard?.todayReward);
const activeAccounts = safeNumber(dashboard?.activeAccounts);
const totalAccounts = safeNumber(dashboard?.totalAccounts);
const activeAccountRate = totalAccounts > 0 ? (activeAccounts / totalAccounts) * 100 : 0;
@@ -235,6 +238,8 @@ export default function OpsMonitor() {
successRate,
requestsPerMinute,
tokensPerMinute,
todaySpend,
todayReward,
activeAccounts,
totalAccounts,
activeAccountRate,
@@ -252,6 +257,84 @@ export default function OpsMonitor() {
};
}, [state, t]);
const operationsPulse = useMemo(() => {
const spendTone: Tone = metrics.todaySpend <= metrics.todayReward
? 'success'
: metrics.todaySpend <= Math.max(1, metrics.todayReward * 2)
? 'warning'
: 'danger';
return [
{
label: t('整体状态'),
value: metrics.overallLabel,
note: `${formatPercent(metrics.successRate)} ${t('成功率')}`,
tone: metrics.overallTone,
},
{
label: t('站点风险'),
value: `${metrics.riskySites.length}`,
note: `${metrics.activeSites.length}/${Array.isArray(state.insights?.siteAvailability) ? state.insights.siteAvailability.length : 0} ${t('活跃站点')}`,
tone: metrics.riskySites.length > 0 ? 'warning' : 'success',
},
{
label: t('今日成本'),
value: `$${metrics.todaySpend.toFixed(2)}`,
note: `${t('奖励')} $${metrics.todayReward.toFixed(2)}`,
tone: spendTone,
},
{
label: t('当前吞吐'),
value: metrics.requestsPerMinute > 0 ? `${Math.round(metrics.requestsPerMinute).toLocaleString()} RPM` : t('暂无流量'),
note: `${formatCompactTokenMetric(metrics.tokensPerMinute)} TPM`,
tone: metrics.requestsPerMinute > 0 ? 'primary' : 'muted',
},
];
}, [metrics, state.insights, t]);
const commandLinks = useMemo(() => {
const routeRiskCount = metrics.riskyRoutes.length;
const hasDangerRoute = metrics.riskyRoutes.some((item) => item.health.tone === 'danger');
return [
{
to: '/ops',
label: t('运维监控'),
value: metrics.overallLabel,
note: t('值班入口'),
tone: metrics.overallTone,
},
{
to: '/routes',
label: t('路由健康'),
value: metrics.routeItems.length > 0 ? String(routeRiskCount) : t('暂无'),
note: metrics.routeItems.length > 0 ? (routeRiskCount > 0 ? t('需处理路由') : t('配置正常')) : t('等待路由'),
tone: metrics.routeItems.length > 0 ? (hasDangerRoute ? 'danger' : routeRiskCount > 0 ? 'warning' : 'success') : 'muted',
},
{
to: '/accounts',
label: t('连接管理'),
value: `${Math.round(metrics.activeAccounts)}/${Math.round(metrics.totalAccounts)}`,
note: t('账号健康'),
tone: metrics.activeAccountRate >= 80 ? 'success' : 'warning',
},
{
to: '/sites',
label: t('站点管理'),
value: String(metrics.riskySites.length),
note: metrics.riskySites.length > 0 ? t('需关注站点') : t('风险站点'),
tone: metrics.riskySites.length > 0 ? 'warning' : 'success',
},
{
to: '/ip-management',
label: t('IP 管理'),
value: metrics.riskySites.length > 0 ? t('检查') : t('正常'),
note: t('代理与出口'),
tone: metrics.riskySites.length > 0 ? 'warning' : 'success',
},
];
}, [metrics, t]);
const queue = useMemo(() => {
const items: Array<{
key: string;
@@ -412,6 +495,70 @@ export default function OpsMonitor() {
</article>
</section>
<section className="dashboard-war-room ops-monitor-war-room" aria-label={t('运营战情室')}>
<div className="dashboard-pulse-grid">
{operationsPulse.map((item) => (
<div className="dashboard-pulse-card" data-tone={item.tone} key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
<em>{item.note}</em>
</div>
))}
</div>
<div className="dashboard-risk-panel">
<div className="dashboard-risk-head">
<div>
<span>{t('风险雷达')}</span>
<strong>{metrics.riskySites.length > 0 ? t('优先检查这些站点') : t('暂无明显站点风险')}</strong>
</div>
<Link to="/sites" className="btn btn-ghost">{t('站点管理')}</Link>
</div>
{metrics.riskySites.length > 0 ? (
<div className="dashboard-risk-list">
{metrics.riskySites.map((site: any) => (
<article key={site.siteId} className="dashboard-risk-row">
<div className="dashboard-risk-row-main">
<strong>{site.siteName || `${t('站点')} ${site.siteId}`}</strong>
<span>{typeof site.availabilityPercent === 'number' ? formatPercent(site.availabilityPercent) : t('暂无可用率')}</span>
<em>{formatLatency(site.averageLatencyMs, t)}</em>
</div>
<DiagnosticActionGroup
className="dashboard-risk-actions"
actions={[
{
label: t('运维监控'),
href: buildSiteLogsHref(site.siteId, undefined, logContextSearch),
hint: t('查看该站点 24h 请求'),
tone: (site.availabilityPercent ?? 100) < 95 ? 'danger' : 'primary',
},
{
label: t('站点管理'),
href: '/sites',
hint: t('检查代理和模型配置'),
},
]}
/>
</article>
))}
</div>
) : (
<div className="dashboard-risk-empty">
{t('站点可用性、平均延迟和失败数都处于可接受范围。')}
</div>
)}
</div>
</section>
<nav className="dashboard-command-strip ops-monitor-command-strip" aria-label={t('运营快捷入口')}>
{commandLinks.map((item) => (
<Link key={item.to} to={item.to} className="dashboard-command-item" data-tone={item.tone}>
<span>{item.label}</span>
<strong>{item.value}</strong>
<em>{item.note}</em>
</Link>
))}
</nav>
<section className="ops-monitor-layout">
<div className="ops-monitor-main-stack">
<section className="card ops-monitor-panel ops-monitor-log-center" aria-label={t('日志中心')}>
@@ -146,25 +146,11 @@ describe('Dashboard site observability panel', () => {
expect(trendPanel.props.role).toBe('tabpanel');
expect(trendPanel.props['aria-labelledby']).toBe('dashboard-range-tab-90');
const riskPanel = root!.root.find((node) => (
expect(root!.root.findAll((node) => (
typeof node.props.className === 'string'
&& node.props.className.includes('dashboard-risk-panel')
));
const riskText = collectText(riskPanel);
expect(riskText).toContain('风险雷达');
expect(riskText).toContain('Demo Site');
expect(riskText).toContain('75%');
expect(riskText).toContain('320ms');
expect(riskText).toContain('运维监控');
expect(riskText).toContain('查看该站点 24h 请求');
expect(riskText).toContain('站点管理');
expect(riskText).toContain('检查代理和模型配置');
const riskLogLink = riskPanel.find((node) => (
node.type === 'a'
&& collectText(node).trim() === '运维监控'
));
expect(String(riskLogLink.props.href || riskLogLink.props.to || '')).toContain('/ops?siteId=1');
))).toHaveLength(0);
expect(collectText(root!.root)).not.toContain('风险雷达');
const panel = root!.root.find((node) => (
typeof node.props.className === 'string'
+35 -7
View File
@@ -152,17 +152,21 @@ describe('Dashboard and ops log placement', () => {
await flushMicrotasks();
const dashboardText = collectText(root.root);
expect(dashboardText).toContain('运维监控');
expect(dashboardText).toContain('值班入口');
expect(dashboardText).not.toContain('值班入口');
expect(dashboardText).not.toContain('风险雷达');
expect(dashboardText).not.toContain('排障队列');
expect(dashboardText).not.toContain('运行健康处理队列');
expect(dashboardText).not.toContain('日志中心');
expect(dashboardText).not.toContain('程序日志');
const dashboardLinks = root.root.findAll((node) => node.type === 'a');
expect(dashboardLinks.some((link) => link.props.href === '/ops' || link.props.to === '/ops')).toBe(true);
expect(dashboardLinks.some((link) => link.props.href === '/logs?status=failed' || link.props.to === '/logs?status=failed')).toBe(false);
expect(dashboardLinks.some((link) => link.props.href === '/events' || link.props.to === '/events')).toBe(false);
expect(root.root.findAll((node) => (
typeof node.props.className === 'string'
&& node.props.className.includes('dashboard-war-room')
))).toHaveLength(0);
expect(root.root.findAll((node) => (
typeof node.props.className === 'string'
&& node.props.className.includes('dashboard-command-strip')
))).toHaveLength(0);
} finally {
root?.unmount();
}
@@ -179,6 +183,31 @@ describe('Dashboard and ops log placement', () => {
});
await flushMicrotasks();
const warRoom = root.root.find((node) => (
node.type === 'section'
&& node.props['aria-label'] === '运营战情室'
));
const warRoomText = collectText(warRoom);
expect(warRoomText).toContain('整体状态');
expect(warRoomText).toContain('异常');
expect(warRoomText).toContain('风险雷达');
expect(warRoomText).toContain('优先检查这些站点');
expect(warRoomText).toContain('Slow Site');
expect(warRoomText).toContain('运维监控');
expect(warRoomText).toContain('查看该站点 24h 请求');
expect(warRoomText).toContain('站点管理');
expect(warRoomText).toContain('检查代理和模型配置');
const commandStrip = root.root.find((node) => (
node.type === 'nav'
&& node.props['aria-label'] === '运营快捷入口'
));
const commandStripText = collectText(commandStrip);
expect(commandStripText).toContain('值班入口');
expect(commandStripText).toContain('路由健康');
expect(commandStripText).toContain('连接管理');
expect(commandStripText).toContain('IP 管理');
const logCenter = root.root.find((node) => (
node.type === 'section'
&& node.props['aria-label'] === '日志中心'
@@ -241,7 +270,6 @@ describe('Dashboard and ops log placement', () => {
const dashboardText = collectText(root.root);
expect(dashboardText).toContain('健康');
expect(dashboardText).toContain('100.0% 成功率');
expect(dashboardText).toContain('平台运行平稳,可以继续观察吞吐和成本变化。');
expect(dashboardText).not.toContain('平台存在明显异常');
} finally {