Compare commits

...

3 Commits

Author SHA1 Message Date
hamster1963
bc0886e8c0 v2.2.0 2025-01-12 22:58:42 +08:00
hamster1963
957c679a90 chore: deps 2025-01-12 22:57:31 +08:00
hamster1963
0dd8bf7bb7 feat: show last active 2025-01-12 22:56:37 +08:00
8 changed files with 93 additions and 44 deletions

View File

@ -6,7 +6,7 @@ import ServerFlag from "@/components/ServerFlag"
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading" import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { cn, formatBytes } from "@/lib/utils" import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"
import countries from "i18n-iso-countries" import countries from "i18n-iso-countries"
import enLocale from "i18n-iso-countries/langs/en.json" import enLocale from "i18n-iso-countries/langs/en.json"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
@ -45,9 +45,9 @@ export default function ServerDetailClient({
} }
const { data: serverList, error, isLoading } = useServerData() const { data: serverList, error, isLoading } = useServerData()
const data = serverList?.result?.find((item) => item.id === server_id) const serverData = serverList?.result?.find((item) => item.id === server_id)
if (!data && !isLoading) { if (!serverData && !isLoading) {
notFound() notFound()
} }
@ -62,7 +62,28 @@ export default function ServerDetailClient({
) )
} }
if (!data) return <ServerDetailLoading /> if (!serverData) return <ServerDetailLoading />
const {
name,
online,
uptime,
version,
arch,
mem_total,
disk_total,
country_code,
platform,
platform_version,
cpu_info,
gpu_info,
load_1,
load_5,
load_15,
net_out_transfer,
net_in_transfer,
last_active_time_string,
} = formatNezhaInfo(serverData)
return ( return (
<div> <div>
@ -71,7 +92,7 @@ export default function ServerDetailClient({
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl hover:opacity-50 transition-opacity duration-300" className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl hover:opacity-50 transition-opacity duration-300"
> >
<BackIcon /> <BackIcon />
{data?.name} {name}
</div> </div>
<section className="flex flex-wrap gap-2 mt-3"> <section className="flex flex-wrap gap-2 mt-3">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
@ -82,12 +103,12 @@ export default function ServerDetailClient({
className={cn( className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", "text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
{ {
" bg-green-800": data?.online_status, " bg-green-800": online,
" bg-red-600": !data?.online_status, " bg-red-600": !online,
}, },
)} )}
> >
{data?.online_status ? t("Online") : t("Offline")} {online ? t("Online") : t("Offline")}
</Badge> </Badge>
</section> </section>
</CardContent> </CardContent>
@ -98,29 +119,29 @@ export default function ServerDetailClient({
<p className="text-xs text-muted-foreground">{t("Uptime")}</p> <p className="text-xs text-muted-foreground">{t("Uptime")}</p>
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{data?.status.Uptime / 86400 >= 1 {uptime / 86400 >= 1
? (data?.status.Uptime / 86400).toFixed(0) + " " + t("Days") ? (uptime / 86400).toFixed(0) + " " + t("Days")
: (data?.status.Uptime / 3600).toFixed(0) + " " + t("Hours")}{" "} : (uptime / 3600).toFixed(0) + " " + t("Hours")}{" "}
</div> </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
{data?.host.Version && ( {version && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Version")}</p> <p className="text-xs text-muted-foreground">{t("Version")}</p>
<div className="text-xs">{data?.host.Version} </div> <div className="text-xs">{version} </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data?.host.Arch && ( {arch && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Arch")}</p> <p className="text-xs text-muted-foreground">{t("Arch")}</p>
<div className="text-xs">{data?.host.Arch} </div> <div className="text-xs">{arch} </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -130,7 +151,7 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-xs text-muted-foreground">{t("Mem")}</p>
<div className="text-xs">{formatBytes(data?.host.MemTotal)}</div> <div className="text-xs">{formatBytes(mem_total)}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -138,23 +159,18 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Disk")}</p> <p className="text-xs text-muted-foreground">{t("Disk")}</p>
<div className="text-xs">{formatBytes(data?.host.DiskTotal)}</div> <div className="text-xs">{formatBytes(disk_total)}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
{data?.host.CountryCode && ( {country_code && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Region")}</p> <p className="text-xs text-muted-foreground">{t("Region")}</p>
<section className="flex items-start gap-1"> <section className="flex items-start gap-1">
<div className="text-xs text-start"> <div className="text-xs text-start">{countries.getName(country_code, "en")}</div>
{countries.getName(data?.host.CountryCode, "en")} <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />
</div>
<ServerFlag
className="text-[11px] -mt-[1px]"
country_code={data?.host.CountryCode}
/>
</section> </section>
</section> </section>
</CardContent> </CardContent>
@ -162,7 +178,7 @@ export default function ServerDetailClient({
)} )}
</section> </section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="flex flex-wrap gap-2 mt-1">
{data?.host.Platform && ( {platform && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
@ -170,29 +186,29 @@ export default function ServerDetailClient({
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{data?.host.Platform} - {data?.host.PlatformVersion}{" "} {platform} - {platform_version}{" "}
</div> </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data?.host.CPU && ( {cpu_info && cpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("CPU")}</p> <p className="text-xs text-muted-foreground">{t("CPU")}</p>
<div className="text-xs"> {data?.host.CPU.join(", ")}</div> <div className="text-xs"> {cpu_info.join(", ")}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data?.host.GPU && ( {gpu_info && gpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"GPU"}</p> <p className="text-xs text-muted-foreground">{"GPU"}</p>
<div className="text-xs"> {data?.host.GPU.join(", ")}</div> <div className="text-xs"> {gpu_info.join(", ")}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -204,8 +220,7 @@ export default function ServerDetailClient({
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Load")}</p> <p className="text-xs text-muted-foreground">{t("Load")}</p>
<div className="text-xs"> <div className="text-xs">
{data.status.Load1.toFixed(2) || "0.00"} / {data.status.Load5.toFixed(2) || "0.00"}{" "} {load_1 || "0.00"} / {load_5 || "0.00"} / {load_15 || "0.00"}
/ {data.status.Load15.toFixed(2) || "0.00"}
</div> </div>
</section> </section>
</CardContent> </CardContent>
@ -214,8 +229,8 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-xs text-muted-foreground">{t("Upload")}</p>
{data.status.NetOutTransfer ? ( {net_out_transfer ? (
<div className="text-xs"> {formatBytes(data.status.NetOutTransfer)} </div> <div className="text-xs"> {formatBytes(net_out_transfer)} </div>
) : ( ) : (
<div className="text-xs">Unknown</div> <div className="text-xs">Unknown</div>
)} )}
@ -226,8 +241,8 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Download")}</p> <p className="text-xs text-muted-foreground">{t("Download")}</p>
{data.status.NetInTransfer ? ( {net_in_transfer ? (
<div className="text-xs"> {formatBytes(data.status.NetInTransfer)} </div> <div className="text-xs"> {formatBytes(net_in_transfer)} </div>
) : ( ) : (
<div className="text-xs">Unknown</div> <div className="text-xs">Unknown</div>
)} )}
@ -235,6 +250,18 @@ export default function ServerDetailClient({
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
<section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("LastActive")}</p>
<div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"}
</div>
</section>
</CardContent>
</Card>
</section>
</div> </div>
) )
} }

BIN
bun.lockb

Binary file not shown.

View File

@ -13,14 +13,32 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
process: serverInfo.status.ProcessCount || 0, process: serverInfo.status.ProcessCount || 0,
up: serverInfo.status.NetOutSpeed / 1024 / 1024 || 0, up: serverInfo.status.NetOutSpeed / 1024 / 1024 || 0,
down: serverInfo.status.NetInSpeed / 1024 / 1024 || 0, down: serverInfo.status.NetInSpeed / 1024 / 1024 || 0,
last_active_time_string: serverInfo.last_active
? new Date(serverInfo.last_active * 1000).toLocaleString()
: "",
online: serverInfo.online_status, online: serverInfo.online_status,
uptime: serverInfo.status.Uptime || 0,
version: serverInfo.host.Version || null,
tcp: serverInfo.status.TcpConnCount || 0, tcp: serverInfo.status.TcpConnCount || 0,
udp: serverInfo.status.UdpConnCount || 0, udp: serverInfo.status.UdpConnCount || 0,
arch: serverInfo.host.Arch || "",
mem_total: serverInfo.host.MemTotal || 0,
swap_total: serverInfo.host.SwapTotal || 0,
disk_total: serverInfo.host.DiskTotal || 0,
platform: serverInfo.host.Platform || "",
platform_version: serverInfo.host.PlatformVersion || "",
mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100 || 0, mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100 || 0,
swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100 || 0, swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100 || 0,
disk: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0, disk: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0,
stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0, stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0,
net_out_transfer: serverInfo.status.NetOutTransfer || 0,
net_in_transfer: serverInfo.status.NetInTransfer || 0,
country_code: serverInfo.host.CountryCode, country_code: serverInfo.host.CountryCode,
cpu_info: serverInfo.host.CPU || [],
gpu_info: serverInfo.host.GPU || [],
load_1: serverInfo.status.Load1?.toFixed(2) || 0.0,
load_5: serverInfo.status.Load5?.toFixed(2) || 0.0,
load_15: serverInfo.status.Load15?.toFixed(2) || 0.0,
} }
} }

View File

@ -85,7 +85,8 @@
"CPU": "CPU", "CPU": "CPU",
"Upload": "Upload", "Upload": "Upload",
"Download": "Download", "Download": "Download",
"Load": "Load" "Load": "Load",
"LastActive": "Last Active"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "Please check your environment variables and review the server console", "chart_fetch_error_message": "Please check your environment variables and review the server console",

View File

@ -85,7 +85,8 @@
"CPU": "CPU", "CPU": "CPU",
"Load": "負荷", "Load": "負荷",
"Upload": "Upload", "Upload": "Upload",
"Download": "Download" "Download": "Download",
"LastActive": "Last Active"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "環境変数を確認し、サーバーコンソールを確認してください", "chart_fetch_error_message": "環境変数を確認し、サーバーコンソールを確認してください",

View File

@ -85,7 +85,8 @@
"CPU": "CPU", "CPU": "CPU",
"Upload": "上傳", "Upload": "上傳",
"Download": "下載", "Download": "下載",
"Load": "負載" "Load": "負載",
"LastActive": "最後上報時間"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "獲取伺服器詳情失敗,請檢查您的環境變數並檢查伺服器控制台", "chart_fetch_error_message": "獲取伺服器詳情失敗,請檢查您的環境變數並檢查伺服器控制台",

View File

@ -85,7 +85,8 @@
"CPU": "CPU", "CPU": "CPU",
"Upload": "上传", "Upload": "上传",
"Download": "下载", "Download": "下载",
"Load": "负载" "Load": "负载",
"LastActive": "最后上报时间"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台", "chart_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台",

View File

@ -1,6 +1,6 @@
{ {
"name": "nezha-dash", "name": "nezha-dash",
"version": "2.1.3", "version": "2.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3040", "dev": "next dev -p 3040",
@ -66,8 +66,8 @@
"@next/bundle-analyzer": "^15.1.4", "@next/bundle-analyzer": "^15.1.4",
"@tailwindcss/postcss": "^4.0.0-beta.9", "@tailwindcss/postcss": "^4.0.0-beta.9",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/react": "^19.0.4", "@types/react": "^19.0.5",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.3",
"eslint-config-next": "^15.1.4", "eslint-config-next": "^15.1.4",
"eslint-plugin-turbo": "^2.3.3", "eslint-plugin-turbo": "^2.3.3",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",