feat: add admin operations war room
This commit is contained in:
@@ -1600,6 +1600,152 @@ body {
|
||||
margin: -6px 0 20px;
|
||||
}
|
||||
|
||||
.dashboard-war-room {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(340px, 0.62fr);
|
||||
gap: 12px;
|
||||
margin: -8px 0 18px;
|
||||
}
|
||||
|
||||
.dashboard-pulse-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dashboard-pulse-card,
|
||||
.dashboard-risk-panel {
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--color-bg-card) 88%, transparent), color-mix(in srgb, var(--color-bg-card) 72%, var(--color-bg)));
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.dashboard-pulse-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
min-height: 118px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-pulse-card::before {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 4px;
|
||||
content: "";
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.dashboard-pulse-card[data-tone="success"]::before {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.dashboard-pulse-card[data-tone="warning"]::before {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
|
||||
.dashboard-pulse-card[data-tone="danger"]::before {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
.dashboard-pulse-card[data-tone="muted"]::before {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.dashboard-pulse-card span,
|
||||
.dashboard-pulse-card em,
|
||||
.dashboard-risk-head span,
|
||||
.dashboard-risk-row span,
|
||||
.dashboard-risk-row em,
|
||||
.dashboard-risk-empty {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.dashboard-pulse-card strong {
|
||||
overflow: hidden;
|
||||
color: var(--color-text-primary);
|
||||
font-size: clamp(22px, 2.3vw, 34px);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dashboard-risk-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.dashboard-risk-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboard-risk-head div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-risk-head strong {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dashboard-risk-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dashboard-risk-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 13px;
|
||||
background: color-mix(in srgb, var(--color-bg) 62%, var(--color-bg-card));
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
transition: transform 0.14s ease, border-color 0.14s ease, background-color 0.14s ease;
|
||||
}
|
||||
|
||||
.dashboard-risk-row:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--color-warning) 36%, var(--console-line));
|
||||
background: color-mix(in srgb, var(--color-warning) 7%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.dashboard-risk-row strong {
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-risk-empty {
|
||||
min-height: 72px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px dashed var(--console-line);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dashboard-command-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -1773,6 +1919,14 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-war-room {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-pulse-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dashboard-command-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -1839,6 +1993,15 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.dashboard-pulse-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-risk-row {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dashboard-command-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -547,6 +547,72 @@ export default function Dashboard({
|
||||
const latency = site.averageLatencyMs ?? 0;
|
||||
return availability < 99 || latency > 1800 || site.failedCount > 0;
|
||||
}).length;
|
||||
const riskySites = activeSites
|
||||
.filter((site) => {
|
||||
const availability = site.availabilityPercent ?? 100;
|
||||
const latency = site.averageLatencyMs ?? 0;
|
||||
return availability < 99 || latency > 1800 || site.failedCount > 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aScore =
|
||||
(100 - (a.availabilityPercent ?? 100)) * 100 +
|
||||
(a.failedCount || 0) * 8 +
|
||||
Math.max(0, (a.averageLatencyMs || 0) - 1200) / 10;
|
||||
const bScore =
|
||||
(100 - (b.availabilityPercent ?? 100)) * 100 +
|
||||
(b.failedCount || 0) * 8 +
|
||||
Math.max(0, (b.averageLatencyMs || 0) - 1200) / 10;
|
||||
return bScore - aScore;
|
||||
})
|
||||
.slice(0, 3);
|
||||
const overallHealthTone =
|
||||
proxySuccessRate >= 99 && siteRiskCount === 0
|
||||
? "success"
|
||||
: proxySuccessRate >= 95 && siteRiskCount <= 2
|
||||
? "warning"
|
||||
: "danger";
|
||||
const overallHealthLabel =
|
||||
overallHealthTone === "success"
|
||||
? "稳定"
|
||||
: overallHealthTone === "warning"
|
||||
? "需关注"
|
||||
: "异常";
|
||||
const spendTone =
|
||||
todaySpend <= todayReward
|
||||
? "success"
|
||||
: todaySpend <= Math.max(1, todayReward * 2)
|
||||
? "warning"
|
||||
: "danger";
|
||||
const throughputLabel =
|
||||
requestsPerMinute > 0
|
||||
? `${Math.round(requestsPerMinute).toLocaleString()} RPM`
|
||||
: "暂无流量";
|
||||
const operationsPulse = [
|
||||
{
|
||||
label: "整体状态",
|
||||
value: overallHealthLabel,
|
||||
note: `${proxySuccessRate.toFixed(1)}% 成功率`,
|
||||
tone: overallHealthTone,
|
||||
},
|
||||
{
|
||||
label: "站点风险",
|
||||
value: `${siteRiskCount}`,
|
||||
note: `${activeSites.length}/${rawSiteAvailability.length} 活跃站点`,
|
||||
tone: siteRiskCount > 0 ? "warning" : "success",
|
||||
},
|
||||
{
|
||||
label: "今日成本",
|
||||
value: `$${todaySpend.toFixed(2)}`,
|
||||
note: `奖励 $${todayReward.toFixed(2)}`,
|
||||
tone: spendTone,
|
||||
},
|
||||
{
|
||||
label: "当前吞吐",
|
||||
value: throughputLabel,
|
||||
note: `${formatCompactTokenMetric(tokensPerMinute)} TPM`,
|
||||
tone: requestsPerMinute > 0 ? "primary" : "muted",
|
||||
},
|
||||
];
|
||||
const dashboardCommandLinks = [
|
||||
{
|
||||
to: "/routes",
|
||||
@@ -681,6 +747,46 @@ export default function Dashboard({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-war-room" aria-label="运营战情室">
|
||||
<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>风险雷达</span>
|
||||
<strong>{riskySites.length > 0 ? "优先检查这些站点" : "暂无明显站点风险"}</strong>
|
||||
</div>
|
||||
<Link to="/sites" className="btn btn-ghost">站点管理</Link>
|
||||
</div>
|
||||
{riskySites.length > 0 ? (
|
||||
<div className="dashboard-risk-list">
|
||||
{riskySites.map((site) => (
|
||||
<Link
|
||||
key={site.siteId}
|
||||
to={buildSiteLast24hLogsRoute(site.siteId)}
|
||||
className="dashboard-risk-row"
|
||||
>
|
||||
<strong>{site.siteName}</strong>
|
||||
<span>{formatAvailabilityPercent(site.availabilityPercent)}</span>
|
||||
<em>{site.averageLatencyMs != null ? `${site.averageLatencyMs}ms` : "暂无延迟"}</em>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="dashboard-risk-empty">
|
||||
站点可用性、平均延迟和失败数都处于可接受范围。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav className="dashboard-command-strip" aria-label="运营快捷入口">
|
||||
{dashboardCommandLinks.map((item) => (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user