refactor: move ops triage cards to ops page
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user