feat: add admin operations war room
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user