feat: add admin operations war room

This commit is contained in:
boos
2026-06-06 21:50:23 +08:00
parent f7916a42e5
commit 3b3292b934
2 changed files with 269 additions and 0 deletions
+163
View File
@@ -1600,6 +1600,152 @@ body {
margin: -6px 0 20px; 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 { .dashboard-command-item {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -1773,6 +1919,14 @@ body {
} }
@media (max-width: 1200px) { @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 { .dashboard-command-strip {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -1839,6 +1993,15 @@ body {
} }
@media (max-width: 860px) { @media (max-width: 860px) {
.dashboard-pulse-grid {
grid-template-columns: 1fr;
}
.dashboard-risk-row {
grid-template-columns: 1fr;
align-items: start;
}
.dashboard-command-strip { .dashboard-command-strip {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+106
View File
@@ -547,6 +547,72 @@ export default function Dashboard({
const latency = site.averageLatencyMs ?? 0; const latency = site.averageLatencyMs ?? 0;
return availability < 99 || latency > 1800 || site.failedCount > 0; return availability < 99 || latency > 1800 || site.failedCount > 0;
}).length; }).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 = [ const dashboardCommandLinks = [
{ {
to: "/routes", to: "/routes",
@@ -681,6 +747,46 @@ export default function Dashboard({
</div> </div>
</section> </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="运营快捷入口"> <nav className="dashboard-command-strip" aria-label="运营快捷入口">
{dashboardCommandLinks.map((item) => ( {dashboardCommandLinks.map((item) => (
<Link <Link