refactor(panel): dashboard

This commit is contained in:
Lemon-miaow committed 2026-06-27 20:59:58 +08:00
1 parent b81b33453a
commit 9d1378793e
4 files changed
+498 -47

No files matched your search

+7 -2
View File
@@ -34,8 +34,13 @@ export function VoxelFleet({ servers }: Props) {
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
mount.appendChild(renderer.domElement); renderer.domElement.style.position = "absolute";
renderer.domElement.style.top = "0";
renderer.domElement.style.left = "0";
renderer.domElement.style.width = "100%";
renderer.domElement.style.height = "100%";
renderer.domElement.style.display = "block"; renderer.domElement.style.display = "block";
mount.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.55)); scene.add(new THREE.AmbientLight(0xffffff, 0.55));
const key = new THREE.DirectionalLight(0xffffff, 1.1); const key = new THREE.DirectionalLight(0xffffff, 1.1);
@@ -157,7 +162,7 @@ export function VoxelFleet({ servers }: Props) {
}; };
}, []); }, []);
return <div ref={mountRef} className="h-full w-full" />; return <div ref={mountRef} className="h-full w-full relative" />;
} }
/** sig is a cheap fingerprint of the fleet's renderable shape (order-independent /** sig is a cheap fingerprint of the fleet's renderable shape (order-independent
+32 -1
View File
@@ -10,5 +10,36 @@
"fleet": "Fleet", "fleet": "Fleet",
"manage": "Manage", "manage": "Manage",
"loading_scene": "Loading scene…", "loading_scene": "Loading scene…",
"preparing_scene": "Preparing scene…" "preparing_scene": "Preparing scene…",
"fleet_load": "Fleet Load & Health",
"active_load": "Player Load Rate",
"status_distribution": "Node Status Distribution",
"account_status": "Account Status",
"account_linked_title": "Account Linked",
"account_linked_desc": "Your identity is bound to a Minecraft UUID. You have full server ownership and wake permissions.",
"account_unlinked_title": "Account Unlinked",
"account_unlinked_desc": "You must link a Minecraft character to claim or operate servers. Go to Account to bind now.",
"go_link": "Bind Now",
"system_specs": "Specs & Metrics",
"spec_images_total": "Whitelisted Images",
"spec_images_built": "Built Locally",
"spec_images_external": "External Whitelisted",
"spec_servers_total": "Total Servers",
"spec_servers_owned": "Owned Servers",
"spec_servers_claimable": "Claimable Servers",
"spec_running_ratio": "Running Rate",
"spec_stopped_ratio": "Sleeping Rate",
"spec_failed_ratio": "Failed Rate",
"no_servers_monitor": "No active instances in the cluster",
"images_count": "Whitelisted backend images available",
"legend_title": "3D Node Monitor Legend",
"legend_running": "Running",
"legend_starting": "Starting",
"legend_stopping": "Stopping",
"legend_stopped": "Sleeping",
"legend_failed": "Failed",
"legend_unknown": "Unknown",
"welcome_title": "Welcome to Felis Lolicon",
"welcome_desc": "You have logged into the User Dashboard. Go to 'My Servers' to view and claim nodes allocated to you, or manage their sleep, wake, and console logs.",
"go_to_servers_btn": "Manage My Servers"
} }
+32 -1
View File
@@ -10,5 +10,36 @@
"fleet": "服务器概览", "fleet": "服务器概览",
"manage": "管理", "manage": "管理",
"loading_scene": "正在加载 3D 场景…", "loading_scene": "正在加载 3D 场景…",
"preparing_scene": "正在准备场景…" "preparing_scene": "正在准备场景…",
"fleet_load": "负载与健康度",
"active_load": "玩家在线负载",
"status_distribution": "节点状态分布",
"account_status": "账号绑定状态",
"account_linked_title": "已关联游戏身份",
"account_linked_desc": "您的 Web 身份已成功绑定至 Minecraft 角色。拥有完整的所有权认领及启动权限。",
"account_unlinked_title": "未关联游戏角色",
"account_unlinked_desc": "您需要关联 Minecraft 角色才能认领或操作服务器。请立即前往个人中心绑定。",
"go_link": "前往绑定",
"system_specs": "系统实例与镜像指标",
"spec_images_total": "支持镜像总数",
"spec_images_built": "集群自建镜像",
"spec_images_external": "外部登记镜像",
"spec_servers_total": "可见服务器总数",
"spec_servers_owned": "我拥有的服务器",
"spec_servers_claimable": "公网可认领服",
"spec_running_ratio": "节点运行率",
"spec_stopped_ratio": "节点休眠率",
"spec_failed_ratio": "节点故障率",
"no_servers_monitor": "当前集群暂无服务器实例",
"images_count": "个支持的白名单镜像环境",
"legend_title": "3D 节点监控图例",
"legend_running": "运行中",
"legend_starting": "启动中",
"legend_stopping": "停止中",
"legend_stopped": "已休眠",
"legend_failed": "异常失败",
"legend_unknown": "未知状态",
"welcome_title": "欢迎使用 Felis 编排平台",
"welcome_desc": "您已成功登录用户控制台。您可以在「我的服务器」页面查看并认领分配给您的专属节点,或者对其进行休眠、唤醒和控制台日志监控。",
"go_to_servers_btn": "操作我的服务器"
} }
+427 -43
View File
@@ -1,13 +1,25 @@
import { lazy, Suspense, useMemo } from "react"; import { lazy, Suspense, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Activity, Server, Play, Hand } from "lucide-react"; import {
Activity,
Server,
Play,
CheckCircle,
AlertTriangle,
ArrowRight,
Cpu,
ShieldCheck,
Database,
Box,
Hand,
} from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loading, ErrorState, EmptyState } from "@/components/States"; import { Loading, ErrorState } from "@/components/States";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { useAsync, useConfig } from "@/lib/hooks"; import { useAsync, useConfig } from "@/lib/hooks";
import type { Phase, ServerInfo } from "@/lib/types"; import type { Phase, ServerInfo, WhitelistImage } from "@/lib/types";
// three.js is heavy and only the Dashboard renders it — split it into its own // three.js is heavy and only the Dashboard renders it — split it into its own
// async chunk so the rest of the panel doesn't pay for it on first load. // async chunk so the rest of the panel doesn't pay for it on first load.
@@ -41,18 +53,32 @@ function Stat({
export function Dashboard() { export function Dashboard() {
const cfg = useConfig(); const cfg = useConfig();
const { data, error, loading, reload } = useAsync(() => api.myServers(), []); const { data: servers, error: serversErr, loading: serversLoading, reload: reloadServers } = useAsync(() => api.myServers(), []);
const { data: linkData, error: linkErr, loading: linkLoading } = useAsync(() => api.linkStatus(), []);
const { data: identity, error: meErr, loading: meLoading } = useAsync(() => api.me(), []);
// 仅对管理员加载镜像白名单列表,防止普通用户请求 403 报错
const { data: imagesData } = useAsync(() => {
if (identity?.is_admin) {
return api.listImages();
}
return Promise.resolve([]);
}, [identity]);
const { t } = useTranslation("dashboard"); const { t } = useTranslation("dashboard");
const counts = useMemo(() => { const counts = useMemo(() => {
const servers = data ?? []; const list = servers ?? [];
const by = (p: Phase) => servers.filter((s) => s.phase === p).length; const by = (p: Phase) => list.filter((s) => s.phase === p).length;
return { return {
total: servers.length, total: list.length,
running: by("Running"), running: by("Running"),
players: servers.reduce((n, s) => n + (s.players ?? 0), 0), players: list.reduce((n, s) => n + (s.players ?? 0), 0),
}; };
}, [data]); }, [servers]);
const loading = serversLoading || linkLoading || meLoading;
const error = serversErr || linkErr || meErr;
return ( return (
<div className="mx-auto max-w-6xl space-y-6"> <div className="mx-auto max-w-6xl space-y-6">
@@ -61,12 +87,20 @@ export function Dashboard() {
<p className="text-sm text-muted-foreground">{t("subtitle")}</p> <p className="text-sm text-muted-foreground">{t("subtitle")}</p>
</div> </div>
{loading && !data ? ( {loading && !servers ? (
<Loading /> <Loading />
) : error ? ( ) : error ? (
<ErrorState error={error} onRetry={reload} /> <ErrorState error={error} onRetry={reloadServers} />
) : !cfg ? (
<Loading />
) : ( ) : (
<FleetView servers={data ?? []} counts={counts} hasConfig={!!cfg} /> <FleetView
servers={servers ?? []}
counts={counts}
linkStatus={linkData || undefined}
images={imagesData || []}
isAdmin={!!identity?.is_admin}
/>
)} )}
</div> </div>
); );
@@ -75,55 +109,405 @@ export function Dashboard() {
function FleetView({ function FleetView({
servers, servers,
counts, counts,
hasConfig, linkStatus,
images,
isAdmin,
}: { }: {
servers: ServerInfo[]; servers: ServerInfo[];
counts: { total: number; running: number; players: number }; counts: { total: number; running: number; players: number };
hasConfig: boolean; linkStatus?: { linked: boolean };
images: WhitelistImage[];
isAdmin: boolean;
}) { }) {
const { t } = useTranslation("dashboard"); const { t } = useTranslation("dashboard");
if (servers.length === 0) {
// 1. 统计各状态 of 服务器数量
const statusStats = useMemo(() => {
let running = 0;
let starting = 0;
let stopping = 0;
let stopped = 0;
let failed = 0;
let unknown = 0;
let maxPlayers = 0;
servers.forEach((s) => {
if (s.phase === "Running") running++;
else if (s.phase === "Starting") starting++;
else if (s.phase === "Stopping") stopping++;
else if (s.phase === "Stopped") stopped++;
else if (s.phase === "Failed") failed++;
else unknown++;
maxPlayers += s.maxPlayers ?? 0;
});
return {
running,
starting,
stopping,
stopped,
failed,
unknown,
maxPlayers,
};
}, [servers]);
const loadPercentage = statusStats.maxPlayers > 0
? Math.round((counts.players / statusStats.maxPlayers) * 100)
: 0;
// 2. 自建与外部镜像真实统计
const builtImagesCount = useMemo(() =>
images.filter((img) => img.source === "built" || !img.source).length,
[images]
);
const externalImagesCount = useMemo(() =>
images.filter((img) => img.source === "external").length,
[images]
);
// 3. 我拥有及可认领服务器真实统计
const ownedServersCount = useMemo(() =>
servers.filter((s) => s.owned).length,
[servers]
);
const claimableServersCount = useMemo(() =>
servers.filter((s) => s.claimable).length,
[servers]
);
// 4. 各种真实状态比率统计
const runningRatio = useMemo(() =>
servers.length > 0 ? Math.round((statusStats.running / servers.length) * 100) : 0,
[servers, statusStats.running]
);
const stoppedRatio = useMemo(() =>
servers.length > 0 ? Math.round((statusStats.stopped / servers.length) * 100) : 0,
[servers, statusStats.stopped]
);
const failedRatio = useMemo(() =>
servers.length > 0 ? Math.round((statusStats.failed / servers.length) * 100) : 0,
[servers, statusStats.failed]
);
// 辅助渲染百分比进度条段
const renderBarSegment = (count: number, colorClass: string) => {
if (servers.length === 0 || count === 0) return null;
const widthPct = (count / servers.length) * 100;
return ( return (
<EmptyState <div
title={t("no_servers_title")} className={`${colorClass} h-full transition-all`}
hint={t("no_servers_hint")} style={{ width: `${widthPct}%` }}
> />
<Link to="/servers">
<Button>{t("go_to_my_servers")}</Button>
</Link>
</EmptyState>
); );
} };
return ( return (
<> <div className="space-y-6">
{/* 顶部 Stat */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Stat icon={Server} label={t("stat_servers")} value={counts.total} /> <Stat icon={Server} label={t("stat_servers")} value={counts.total} />
<Stat icon={Play} label={t("stat_running")} value={counts.running} /> <Stat icon={Play} label={t("stat_running")} value={counts.running} />
<Stat icon={Activity} label={t("stat_players_online")} value={counts.players} /> <Stat icon={Activity} label={t("stat_players_online")} value={counts.players} />
</div> </div>
<Card className="overflow-hidden"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-12 items-stretch">
<CardHeader className="flex-row items-center justify-between space-y-0"> {/* 左侧:账号状态、健康度及负载监控 (lg:col-span-8 h-full 捕获拉伸高度) */}
<CardTitle className="text-base">{t("fleet")}</CardTitle> <div className="lg:col-span-8 flex flex-col gap-6 h-full">
<Link to="/servers"> {/* 1. 游戏角色绑定 Banner */}
<Button variant="ghost" size="sm"> <Card className="overflow-hidden">
{t("manage")} <Hand className="h-3.5 w-3.5" /> <CardContent className="p-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
</Button> {linkStatus?.linked ? (
</Link> <>
</CardHeader> <div className="space-y-1 max-w-xl">
<CardContent className="p-0"> <div className="flex items-center gap-2 text-green-600 dark:text-green-400 font-semibold text-sm">
{hasConfig ? ( <CheckCircle className="h-5 w-5 shrink-0" />
<div className="h-[340px] w-full bg-gradient-to-b from-transparent to-primary/5"> <span>{t("account_linked_title")}</span>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{t("account_linked_desc")}
</p>
</div>
<Link to="/account" className="shrink-0 w-full sm:w-auto">
<Button variant="outline" size="sm" className="w-full text-xs">
管理游戏角色
</Button>
</Link>
</>
) : (
<>
<div className="space-y-1 max-w-xl">
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-500 font-semibold text-sm">
<AlertTriangle className="h-5 w-5 shrink-0 animate-bounce" />
<span>{t("account_unlinked_title")}</span>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{t("account_unlinked_desc")}
</p>
</div>
<Link to="/account" className="shrink-0 w-full sm:w-auto">
<Button variant="default" size="sm" className="w-full text-xs">
{t("go_link")} <ArrowRight className="h-3 w-3 ml-1" />
</Button>
</Link>
</>
)}
</CardContent>
</Card>
{/* 2. 管理员展示集群负载健康与镜像参数看板,普通用户展示极简引导 */}
{isAdmin ? (
<>
{/* 集群负载与健康卡片 */}
<Card>
<CardHeader className="pb-3 pt-5">
<CardTitle className="text-base">{t("fleet_load")}</CardTitle>
</CardHeader>
<CardContent className="space-y-5 pb-5">
{/* 玩家负载进度条 */}
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-muted-foreground flex items-center gap-1.5">
<Cpu className="h-3.5 w-3.5 text-primary" /> {t("active_load")}
</span>
<span className="font-medium text-foreground">
{counts.players} / {statusStats.maxPlayers} {t("stat_players_online")} ({loadPercentage}%)
</span>
</div>
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
<div
className="bg-primary h-full rounded-full transition-all"
style={{ width: `${loadPercentage}%` }}
/>
</div>
</div>
{/* 服务器状态分布健康条 */}
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">{t("status_distribution")}</span>
<span className="font-medium text-foreground">{servers.length} 实例</span>
</div>
{servers.length > 0 ? (
<div className="h-3 w-full bg-secondary rounded-full overflow-hidden flex">
{renderBarSegment(statusStats.running, "bg-[#22c55e]")}
{renderBarSegment(statusStats.starting, "bg-[#eab308]")}
{renderBarSegment(statusStats.stopping, "bg-[#f97316]")}
{renderBarSegment(statusStats.failed, "bg-[#ef4444]")}
{renderBarSegment(statusStats.stopped, "bg-[#64748b]")}
{renderBarSegment(statusStats.unknown, "bg-[#94a3b8]")}
</div>
) : (
<div className="text-xs text-muted-foreground/60 py-1 text-center bg-muted/20 rounded">
{t("no_servers_monitor")}
</div>
)}
</div>
{/* 镜像及规格说明 */}
<div className="flex items-center gap-3 p-3 rounded-lg border bg-accent/5 text-xs text-muted-foreground">
<ShieldCheck className="h-4.5 w-4.5 text-primary shrink-0" />
<div>
<span className="font-semibold text-foreground mr-1">{images.length}</span>
{t("images_count")}。所有后端镜像运行前均通过 Trivy CVE 漏洞扫描,强制在隔离的安全容器中托管(spec §16)。
</div>
</div>
</CardContent>
</Card>
{/* 平台环境与配置参数卡片 (flex-1 自动拉伸) */}
<Card className="flex-1 flex flex-col justify-between">
<CardHeader className="pb-3 pt-5 px-6">
<CardTitle className="text-base font-semibold text-foreground">
{t("system_specs")}
</CardTitle>
</CardHeader>
<CardContent className="px-6 pb-6 pt-1 flex-1 flex flex-col justify-between">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-8 gap-y-6">
{/* 镜像总数 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<ShieldCheck className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_images_total")}</span>
</div>
<div className="text-xl font-bold font-mono text-foreground leading-none mt-1.5">{images.length}</div>
</div>
{/* 集群内自建镜像 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Box className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_images_built")}</span>
</div>
<div className="text-xl font-bold font-mono text-foreground leading-none mt-1.5">{builtImagesCount}</div>
</div>
{/* 外部登记导入镜像 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Database className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_images_external")}</span>
</div>
<div className="text-xl font-bold font-mono text-foreground leading-none mt-1.5">{externalImagesCount}</div>
</div>
{/* 可见服务器总数 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Server className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_servers_total")}</span>
</div>
<div className="text-xl font-bold font-mono text-foreground leading-none mt-1.5">{counts.total}</div>
</div>
{/* 我拥有管理的服务器 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<CheckCircle className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_servers_owned")}</span>
</div>
<div className="text-xl font-bold font-mono text-foreground leading-none mt-1.5">{ownedServersCount}</div>
</div>
{/* 公网可认领服务器 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Hand className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_servers_claimable")}</span>
</div>
<div className="text-xl font-bold font-mono text-foreground leading-none mt-1.5">{claimableServersCount}</div>
</div>
{/* 运行中实例占比 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Activity className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_running_ratio")}</span>
</div>
<div className="text-xl font-bold font-mono text-[#22c55e] leading-none mt-1.5">{runningRatio}%</div>
</div>
{/* 节点休眠率 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Activity className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_stopped_ratio")}</span>
</div>
<div className="text-xl font-bold font-mono text-slate-500 leading-none mt-1.5">{stoppedRatio}%</div>
</div>
{/* 运行故障率 */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-muted-foreground">
<AlertTriangle className="h-3.5 w-3.5 text-primary" />
<span className="text-[11px] font-medium">{t("spec_failed_ratio")}</span>
</div>
<div className="text-xl font-bold font-mono text-rose-500 leading-none mt-1.5">{failedRatio}%</div>
</div>
</div>
</CardContent>
</Card>
</>
) : (
<Card className="overflow-hidden flex flex-col justify-between flex-1">
<CardHeader className="pb-3 pt-5 px-6">
<CardTitle className="text-base font-semibold text-foreground">
{t("welcome_title")}
</CardTitle>
</CardHeader>
<CardContent className="px-6 pb-6 pt-1 flex-1 flex flex-col justify-between gap-4">
<p className="text-xs text-muted-foreground leading-relaxed">
{t("welcome_desc")}
</p>
{/* 极简平台新手引导指引(真实且有用) */}
<div className="p-3.5 rounded-lg border bg-accent/5 text-[11px] text-muted-foreground space-y-1.5">
<div className="font-semibold text-foreground text-xs">快速入门指引:</div>
<ul className="list-disc list-inside space-y-1">
<li>认领无主服后,系统会自动将您的 Minecraft UUID 添加至准入白名单。</li>
<li>服务器如果长时间处于无人在线空闲状态,平台会自动执行休眠存档以节约资源。</li>
<li>游戏入口统一由 Velocity 网关承载,只需连接您的专用子域名即可。</li>
</ul>
</div>
<div>
<Link to="/servers">
<Button variant="default" size="sm" className="text-xs gap-1.5 w-full sm:w-auto">
{t("go_to_servers_btn")} <ArrowRight className="w-3.5 h-3.5" />
</Button>
</Link>
</div>
</CardContent>
</Card>
)}
</div>
{/* 右侧:3D星图拓扑监控 (lg:col-span-4 h-full) */}
<Card className="lg:col-span-4 flex flex-col overflow-hidden h-full">
<CardHeader className="space-y-1 pb-4">
<CardTitle className="text-base">{t("fleet")}</CardTitle>
<p className="text-xs text-muted-foreground">三维体素网络拓扑图</p>
</CardHeader>
<CardContent className="flex-1 flex flex-col p-0 justify-between">
<div className="flex-1 min-h-[220px] w-full bg-gradient-to-b from-transparent to-primary/5 border-b relative">
<Suspense fallback={<Loading label={t("loading_scene")} />}> <Suspense fallback={<Loading label={t("loading_scene")} />}>
<VoxelFleet servers={servers} /> <VoxelFleet servers={servers} />
</Suspense> </Suspense>
</div> </div>
) : ( {/* 状态图例说明 + 指标统计 */}
<Loading label={t("preparing_scene")} /> <div className="p-4 space-y-3 text-xs bg-muted/10">
)} <div className="font-semibold text-muted-foreground text-[10px] uppercase tracking-wider">
</CardContent> {t("legend_title")}
</Card> </div>
</> <div className="grid grid-cols-2 gap-x-4 gap-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#22c55e]" />
<span className="text-muted-foreground text-[11px]">{t("legend_running")}</span>
</div>
<span className="font-mono text-[11px] text-foreground font-semibold">{statusStats.running}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#eab308] animate-pulse" />
<span className="text-muted-foreground text-[11px]">{t("legend_starting")}</span>
</div>
<span className="font-mono text-[11px] text-foreground font-semibold">{statusStats.starting}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#f97316] animate-pulse" />
<span className="text-muted-foreground text-[11px]">{t("legend_stopping")}</span>
</div>
<span className="font-mono text-[11px] text-foreground font-semibold">{statusStats.stopping}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#64748b]" />
<span className="text-muted-foreground text-[11px]">{t("legend_stopped")}</span>
</div>
<span className="font-mono text-[11px] text-foreground font-semibold">{statusStats.stopped}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#ef4444]" />
<span className="text-muted-foreground text-[11px]">{t("legend_failed")}</span>
</div>
<span className="font-mono text-[11px] text-foreground font-semibold">{statusStats.failed}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#94a3b8]" />
<span className="text-muted-foreground text-[11px]">{t("legend_unknown")}</span>
</div>
<span className="font-mono text-[11px] text-foreground font-semibold">{statusStats.unknown}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
); );
} }