Compare commits

...

6 Commits

Author SHA1 Message Date
hamster1963
fe18404f4e v2.3.0 2025-01-21 10:40:10 +08:00
hamster1963
f9f57e4d19 perf: fully optimize 2025-01-21 10:39:52 +08:00
hamster1963
646354e515 refactor: biome config 2025-01-21 10:31:54 +08:00
hamster1963
a8f4c8564f perf: remove unused imports 2025-01-21 10:08:02 +08:00
hamster1963
b4c3bccace chore: deps 2025-01-21 10:04:45 +08:00
hamster1963
b76ab55cb2 refactor: core lib 2025-01-21 10:02:23 +08:00
47 changed files with 171 additions and 347 deletions

View File

@ -1,10 +1,10 @@
"use client"
import { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api"
import type { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api"
import NetworkChartLoading from "@/components/loading/NetworkChartLoading"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import {
ChartConfig,
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
@ -120,9 +120,12 @@ export const NetworkChart = React.memo(function NetworkChart({
() =>
chartDataKey.map((key) => (
<button
type="button"
key={key}
data-active={activeChart === key}
className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
className={
"relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6"
}
onClick={() => handleButtonClick(key)}
>
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
@ -216,7 +219,7 @@ export const NetworkChart = React.memo(function NetworkChart({
const smoothed = { ...point } as ResultItem
if (activeChart === defaultChart) {
chartDataKey.forEach((key) => {
for (const key of chartDataKey) {
const values = window
.map((w) => w[key])
.filter((v) => v !== undefined && v !== null) as number[]
@ -233,7 +236,7 @@ export const NetworkChart = React.memo(function NetworkChart({
smoothed[key] = ewmaHistory[key]
}
}
})
}
} else {
const values = window
.map((w) => w.avg_delay)
@ -243,12 +246,12 @@ export const NetworkChart = React.memo(function NetworkChart({
const processed = processValues(values)
if (processed !== null) {
// 应用EWMA平滑
if (ewmaHistory["current"] === undefined) {
ewmaHistory["current"] = processed
if (ewmaHistory.current === undefined) {
ewmaHistory.current = processed
} else {
ewmaHistory["current"] = alpha * processed + (1 - alpha) * ewmaHistory["current"]
ewmaHistory.current = alpha * processed + (1 - alpha) * ewmaHistory.current
}
smoothed.avg_delay = ewmaHistory["current"]
smoothed.avg_delay = ewmaHistory.current
}
}
}
@ -320,7 +323,7 @@ export const NetworkChart = React.memo(function NetworkChart({
const transformData = (data: NezhaAPIMonitor[]) => {
const monitorData: ServerMonitorChart = {}
data.forEach((item) => {
for (const item of data) {
const monitorName = item.monitor_name
if (!monitorData[monitorName]) {
@ -333,7 +336,7 @@ const transformData = (data: NezhaAPIMonitor[]) => {
avg_delay: item.avg_delay[i],
})
}
})
}
return monitorData
}
@ -342,16 +345,18 @@ const formatData = (rawData: NezhaAPIMonitor[]) => {
const result: { [time: number]: ResultItem } = {}
const allTimes = new Set<number>()
rawData.forEach((item) => {
item.created_at.forEach((time) => allTimes.add(time))
})
for (const item of rawData) {
for (const time of item.created_at) {
allTimes.add(time)
}
}
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
rawData.forEach((item) => {
for (const item of rawData) {
const { monitor_name, created_at, avg_delay } = item
allTimeArray.forEach((time) => {
for (const time of allTimeArray) {
if (!result[time]) {
result[time] = { created_at: time }
}
@ -359,8 +364,8 @@ const formatData = (rawData: NezhaAPIMonitor[]) => {
const timeIndex = created_at.indexOf(time)
// @ts-expect-error - avg_delay is an array
result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null
})
})
}
}
return Object.values(result).sort((a, b) => a.created_at - b.created_at)
}

View File

@ -2,14 +2,14 @@
import {
MAX_HISTORY_LENGTH,
ServerDataWithTimestamp,
type ServerDataWithTimestamp,
useServerData,
} from "@/app/lib/server-data-context"
import { NezhaAPISafe } from "@/app/types/nezha-api"
} from "@/app/context/server-data-context"
import type { NezhaAPISafe } from "@/app/types/nezha-api"
import { ServerDetailChartLoading } from "@/components/loading/ServerDetailLoading"
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
import { Card, CardContent } from "@/components/ui/card"
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
import { formatBytes, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
import { useTranslations } from "next-intl"
import { useEffect, useRef, useState } from "react"
@ -684,14 +684,14 @@ function NetworkChart({
<div className="flex flex-col w-20">
<p className="text-xs text-muted-foreground">{t("Upload")}</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" />
<p className="text-xs font-medium">{up.toFixed(2)} M/s</p>
</div>
</div>
<div className="flex flex-col w-20">
<p className=" text-xs text-muted-foreground">{t("Download")}</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" />
<p className="text-xs font-medium">{down.toFixed(2)} M/s</p>
</div>
</div>
@ -826,14 +826,14 @@ function ConnectChart({
<div className="flex flex-col w-12">
<p className="text-xs text-muted-foreground">TCP</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" />
<p className="text-xs font-medium">{tcp}</p>
</div>
</div>
<div className="flex flex-col w-12">
<p className=" text-xs text-muted-foreground">UDP</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" />
<p className="text-xs font-medium">{udp}</p>
</div>
</div>

View File

@ -1,6 +1,6 @@
"use client"
import { useServerData } from "@/app/lib/server-data-context"
import { useServerData } from "@/app/context/server-data-context"
import { BackIcon } from "@/components/Icon"
import ServerFlag from "@/components/ServerFlag"
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
@ -40,7 +40,7 @@ export default function ServerDetailClient({
if (hasHistory) {
router.back()
} else {
router.push(`/`)
router.push("/")
}
}
@ -120,8 +120,8 @@ export default function ServerDetailClient({
<div className="text-xs">
{" "}
{uptime / 86400 >= 1
? (uptime / 86400).toFixed(0) + " " + t("Days")
: (uptime / 3600).toFixed(0) + " " + t("Hours")}{" "}
? `${(uptime / 86400).toFixed(0)} ${t("Days")}`
: `${(uptime / 3600).toFixed(0)} ${t("Hours")}`}
</div>
</section>
</CardContent>

View File

@ -1,6 +1,6 @@
"use client"
import { IPInfo } from "@/app/api/server-ip/route"
import type { IPInfo } from "@/app/api/server-ip/route"
import { Loader } from "@/components/loading/Loader"
import { Card, CardContent } from "@/components/ui/card"
import { nezhaFetcher } from "@/lib/utils"

View File

@ -1,11 +1,11 @@
"use client"
import { TooltipProvider } from "@/app/(main)/ClientComponents/detail/TooltipContext"
import GlobalInfo from "@/app/(main)/ClientComponents/main/GlobalInfo"
import { InteractiveMap } from "@/app/(main)/ClientComponents/main/InteractiveMap"
import { useServerData } from "@/app/lib/server-data-context"
import { useServerData } from "@/app/context/server-data-context"
import { TooltipProvider } from "@/app/context/tooltip-context"
import GlobalLoading from "@/components/loading/GlobalLoading"
import { geoJsonString } from "@/lib/geo-json-string"
import { geoJsonString } from "@/lib/geo/geo-json-string"
export default function ServerGlobal() {
const { data: nezhaServerList, error } = useServerData()
@ -24,7 +24,7 @@ export default function ServerGlobal() {
const countryList: string[] = []
const serverCounts: { [key: string]: number } = {}
nezhaServerList.result.forEach((server) => {
for (const server of nezhaServerList.result) {
if (server.host.CountryCode) {
const countryCode = server.host.CountryCode.toUpperCase()
if (!countryList.includes(countryCode)) {
@ -32,7 +32,7 @@ export default function ServerGlobal() {
}
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
}
})
}
const width = 900
const height = 500

View File

@ -1,8 +1,8 @@
"use client"
import { useTooltip } from "@/app/(main)/ClientComponents/detail/TooltipContext"
import MapTooltip from "@/app/(main)/ClientComponents/main/MapTooltip"
import { countryCoordinates } from "@/lib/geo-limit"
import { useTooltip } from "@/app/context/tooltip-context"
import { countryCoordinates } from "@/lib/geo/geo-limit"
import { geoEquirectangular, geoPath } from "d3-geo"
interface InteractiveMapProps {
@ -40,6 +40,7 @@ export function InteractiveMap({
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
>
<title>Interactive Map</title>
<defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
@ -55,14 +56,14 @@ export function InteractiveMap({
fill="transparent"
onMouseEnter={() => setTooltipData(null)}
/>
{filteredFeatures.map((feature, index) => {
{filteredFeatures.map((feature) => {
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
return (
<path
key={index}
key={feature.properties.iso_a2_eh}
d={path(feature) || ""}
className={
isHighlighted

View File

@ -1,6 +1,6 @@
"use client"
import { useTooltip } from "@/app/(main)/ClientComponents/detail/TooltipContext"
import { useTooltip } from "@/app/context/tooltip-context"
import { useTranslations } from "next-intl"
import { memo } from "react"
@ -42,13 +42,13 @@ const MapTooltip = memo(function MapTooltip() {
overflowY: "auto",
}}
>
{sortedServers.map((server, index) => (
<div key={index} className="flex items-center gap-1.5 py-0.5">
{sortedServers.map((server) => (
<div key={server.name} className="flex items-center gap-1.5 py-0.5">
<span
className={`w-1.5 h-1.5 shrink-0 rounded-full ${
server.status ? "bg-green-500" : "bg-red-500"
}`}
></span>
/>
<span className="text-xs">{server.name}</span>
</div>
))}

View File

@ -1,14 +1,14 @@
"use client"
import { useServerData } from "@/app/lib/server-data-context"
import { useFilter } from "@/app/context/network-filter-context"
import { useServerData } from "@/app/context/server-data-context"
import { useStatus } from "@/app/context/status-context"
import ServerCard from "@/components/ServerCard"
import ServerCardInline from "@/components/ServerCardInline"
import Switch from "@/components/Switch"
import GlobalLoading from "@/components/loading/GlobalLoading"
import { Loader } from "@/components/loading/Loader"
import getEnv from "@/lib/env-entry"
import { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"
import { cn } from "@/lib/utils"
import { MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
import { useTranslations } from "next-intl"
@ -124,16 +124,17 @@ export default function ServerListClient() {
}
const tagCountMap: Record<string, number> = {}
filteredServersByStatus.forEach((server) => {
for (const server of filteredServersByStatus) {
if (server.tag) {
tagCountMap[server.tag] = (tagCountMap[server.tag] || 0) + 1
}
})
}
return (
<>
<section className="flex items-center gap-2 w-full overflow-hidden">
<button
type="button"
onClick={() => {
setShowMap(!showMap)
}}
@ -147,6 +148,7 @@ export default function ServerListClient() {
<MapIcon className="size-[13px]" />
</button>
<button
type="button"
onClick={() => {
setInline(inline === "0" ? "1" : "0")
localStorage.setItem("inline", inline === "0" ? "1" : "0")

View File

@ -1,11 +1,11 @@
"use client"
import { useServerData } from "@/app/lib/server-data-context"
import { useFilter } from "@/app/context/network-filter-context"
import { useServerData } from "@/app/context/server-data-context"
import { useStatus } from "@/app/context/status-context"
import { Loader } from "@/components/loading/Loader"
import { Card, CardContent } from "@/components/ui/card"
import getEnv from "@/lib/env-entry"
import { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"
import { cn, formatBytes } from "@/lib/utils"
import blogMan from "@/public/blog-man.webp"
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
@ -46,7 +46,7 @@ export default function ServerOverviewClient() {
<p className="text-sm font-medium md:text-base">{t("p_816-881_Totalservers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500" />
</span>
{data?.result ? (
<div className="text-lg font-semibold">{data?.result.length}</div>
@ -76,8 +76,8 @@ export default function ServerOverviewClient() {
<p className="text-sm font-medium md:text-base">{t("p_1610-1676_Onlineservers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500" />
</span>
{data?.result ? (
<div className="text-lg font-semibold">{data?.live_servers}</div>
@ -107,8 +107,8 @@ export default function ServerOverviewClient() {
<p className="text-sm font-medium md:text-base">{t("p_2532-2599_Offlineservers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500" />
</span>
{data?.result ? (
<div className="text-lg font-semibold">{data?.offline_servers}</div>

View File

@ -13,6 +13,7 @@ export default function Footer() {
href="https://github.com/hamster1963/nezha-dash"
target="_blank"
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
rel="noreferrer"
>
{t("a_303-585_GitHub")}
</a>
@ -20,6 +21,7 @@ export default function Footer() {
href={`https://github.com/hamster1963/nezha-dash/releases/tag/v${version}`}
target="_blank"
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
rel="noreferrer"
>
v{version}
</a>

View File

@ -8,7 +8,7 @@ import getEnv from "@/lib/env-entry"
import { DateTime } from "luxon"
import { useTranslations } from "next-intl"
import { useRouter } from "next/navigation"
import React, { useEffect, useRef, useState } from "react"
import { useEffect, useState } from "react"
function Header() {
const t = useTranslations("Header")
@ -24,7 +24,7 @@ function Header() {
<section
onClick={() => {
sessionStorage.removeItem("selectedTag")
router.push(`/`)
router.push("/")
}}
className="flex cursor-pointer items-center text-base font-medium hover:opacity-50 transition-opacity duration-300"
>
@ -80,10 +80,10 @@ function Links() {
return (
<div className="flex items-center gap-2">
{links.map((link, index) => {
{links.map((link) => {
return (
<a
key={index}
key={link.link}
href={link.link}
target="_blank"
rel="noopener noreferrer"
@ -124,7 +124,7 @@ function Overview() {
{mouted ? (
<p className="text-sm font-medium">{timeString}</p>
) : (
<Skeleton className="h-[20px] w-[50px] rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[20px] w-[50px] rounded-[5px] bg-muted-foreground/10 animate-none" />
)}
</div>
</section>

View File

@ -1,10 +1,10 @@
import Footer from "@/app/(main)/footer"
import Header from "@/app/(main)/header"
import { ServerDataProvider } from "@/app/lib/server-data-context"
import { ServerDataProvider } from "@/app/context/server-data-context"
import { auth } from "@/auth"
import { SignIn } from "@/components/SignIn"
import getEnv from "@/lib/env-entry"
import React from "react"
import type React from "react"
type DashboardProps = {
children: React.ReactNode

View File

@ -2,7 +2,7 @@ import { auth } from "@/auth"
import getEnv from "@/lib/env-entry"
import { GetServerDetail } from "@/lib/serverFetch"
import { redirect } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
export const dynamic = "force-dynamic"
@ -27,8 +27,8 @@ export async function GET(req: NextRequest) {
}
try {
const serverIdNum = parseInt(server_id, 10)
if (isNaN(serverIdNum)) {
const serverIdNum = Number.parseInt(server_id, 10)
if (Number.isNaN(serverIdNum)) {
return NextResponse.json({ error: "server_id must be a valid number" }, { status: 400 })
}

View File

@ -2,7 +2,7 @@ import { auth } from "@/auth"
import getEnv from "@/lib/env-entry"
import { GetServerMonitor } from "@/lib/serverFetch"
import { redirect } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
export const dynamic = "force-dynamic"
@ -27,8 +27,8 @@ export async function GET(req: NextRequest) {
}
try {
const serverIdNum = parseInt(server_id, 10)
if (isNaN(serverIdNum)) {
const serverIdNum = Number.parseInt(server_id, 10)
if (Number.isNaN(serverIdNum)) {
return NextResponse.json({ error: "server_id must be a number" }, { status: 400 })
}

View File

@ -1,11 +1,11 @@
import fs from "fs"
import path from "path"
import fs from "node:fs"
import path from "node:path"
import { auth } from "@/auth"
import getEnv from "@/lib/env-entry"
import { GetServerIP } from "@/lib/serverFetch"
import { AsnResponse, CityResponse, Reader } from "maxmind"
import { type AsnResponse, type CityResponse, Reader } from "maxmind"
import { redirect } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
export const dynamic = "force-dynamic"
@ -41,9 +41,9 @@ export async function GET(req: NextRequest) {
try {
const ip = await GetServerIP({ server_id: Number(server_id) })
const cityDbPath = path.join(process.cwd(), "lib", "GeoLite2-City.mmdb")
const cityDbPath = path.join(process.cwd(), "lib", "maxmind-db", "GeoLite2-City.mmdb")
const asnDbPath = path.join(process.cwd(), "lib", "GeoLite2-ASN.mmdb")
const asnDbPath = path.join(process.cwd(), "lib", "maxmind-db", "GeoLite2-ASN.mmdb")
const cityDbBuffer = fs.readFileSync(cityDbPath)
const asnDbBuffer = fs.readFileSync(asnDbPath)

View File

@ -1,6 +1,6 @@
"use client"
import React, { ReactNode, createContext, useContext, useState } from "react"
import { type ReactNode, createContext, useContext, useState } from "react"
interface FilterContextType {
filter: boolean

View File

@ -1,9 +1,9 @@
"use client"
import { ServerApi } from "@/app/types/nezha-api"
import type { ServerApi } from "@/app/types/nezha-api"
import getEnv from "@/lib/env-entry"
import { nezhaFetcher } from "@/lib/utils"
import { ReactNode, createContext, useContext, useEffect, useState } from "react"
import { type ReactNode, createContext, useContext, useEffect, useState } from "react"
import useSWR from "swr"
export interface ServerDataWithTimestamp {

View File

@ -1,6 +1,6 @@
"use client"
import React, { ReactNode, createContext, useContext, useState } from "react"
import { type ReactNode, createContext, useContext, useState } from "react"
type Status = "all" | "online" | "offline"

View File

@ -1,6 +1,6 @@
"use client"
import { ReactNode, createContext, useContext, useState } from "react"
import { type ReactNode, createContext, useContext, useState } from "react"
export interface TooltipData {
centroid: [number, number]

View File

@ -1,18 +1,18 @@
import { FilterProvider } from "@/app/context/network-filter-context"
import { StatusProvider } from "@/app/context/status-context"
// @auto-i18n-check. Please do not delete the line.
import { ThemeColorManager } from "@/components/ThemeColorManager"
import getEnv from "@/lib/env-entry"
import { FilterProvider } from "@/lib/network-filter-context"
import { StatusProvider } from "@/lib/status-context"
import { cn } from "@/lib/utils"
import "@/styles/globals.css"
import type { Metadata } from "next"
import { Viewport } from "next"
import type { Viewport } from "next"
import { NextIntlClientProvider } from "next-intl"
import { getLocale, getMessages } from "next-intl/server"
import { PublicEnvScript } from "next-runtime-env"
import { ThemeProvider } from "next-themes"
import { Inter as FontSans } from "next/font/google"
import React from "react"
import type React from "react"
const fontSans = FontSans({
subsets: ["latin"],
@ -33,8 +33,8 @@ export const metadata: Metadata = {
statusBarStyle: "default",
},
robots: {
index: disableIndex ? false : true,
follow: disableIndex ? false : true,
index: !disableIndex,
follow: !disableIndex,
},
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": { "ignoreUnknown": false, "ignore": [".next", "public"] },
"files": { "ignoreUnknown": false, "ignore": [".next", "public", "styles/globals.css"] },
"formatter": {
"enabled": true,
"useEditorconfig": true,
@ -16,7 +16,13 @@
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"recommended": true,
"a11y": {
"useKeyWithClickEvents": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"complexity": { "noUselessTypeConstraint": "error" },
"correctness": {
"noUnusedVariables": "error",
@ -52,15 +58,7 @@
"linter": {
"rules": {
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidBuiltinInstantiation": "off",
"noInvalidConstructorSuper": "off",
"noNewSymbol": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
"noUnusedImports": "error"
},
"style": {
"noArguments": "error",
@ -73,7 +71,6 @@
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"useGetterReturn": "off"

BIN
bun.lockb

Binary file not shown.

View File

@ -2,7 +2,7 @@ import Image from "next/image"
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 496 512" fill="white" {...props}>
<svg role="img" aria-label="github-icon" viewBox="0 0 496 512" fill="white" {...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg>
)

View File

@ -12,7 +12,6 @@ import { setUserLocale } from "@/i18n/locale"
import { cn } from "@/lib/utils"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { useLocale } from "next-intl"
import * as React from "react"
export function LanguageSwitcher() {
const locale = useLocale()

View File

@ -1,4 +1,4 @@
import { NezhaAPISafe } from "@/app/types/nezha-api"
import type { NezhaAPISafe } from "@/app/types/nezha-api"
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { Badge } from "@/components/ui/badge"
@ -43,7 +43,7 @@ export default function ServerCard({
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
<div
className={cn(
"flex items-center justify-center",
@ -151,7 +151,7 @@ export default function ServerCard({
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
<div
className={cn(
"flex items-center justify-center",

View File

@ -1,4 +1,4 @@
import { NezhaAPISafe } from "@/app/types/nezha-api"
import type { NezhaAPISafe } from "@/app/types/nezha-api"
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { Card } from "@/components/ui/card"
@ -36,7 +36,7 @@ export default function ServerCardInline({
className={cn("grid items-center gap-2 lg:w-36")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
<div
className={cn(
"flex items-center justify-center",
@ -133,7 +133,7 @@ export default function ServerCardInline({
className={cn("grid items-center gap-2 lg:w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
<div
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}
>

View File

@ -1,5 +1,4 @@
import { Progress } from "@/components/ui/progress"
import React from "react"
type ServerUsageBarProps = {
value: number

View File

@ -64,6 +64,7 @@ export function SignIn() {
/>
</label>
<button
type="submit"
className="px-1.5 py-0.5 w-fit flex items-center gap-1 text-sm font-semibold border-stone-300 dark:border-stone-800 rounded-[8px] border bg-card hover:brightness-95 transition-all text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none"
disabled={loading}
>

View File

@ -3,7 +3,7 @@
import getEnv from "@/lib/env-entry"
import { cn } from "@/lib/utils"
import { useTranslations } from "next-intl"
import React, { createRef, useEffect, useRef, useState } from "react"
import { createRef, useEffect, useRef, useState } from "react"
export default function Switch({
allTag,

View File

@ -2,7 +2,7 @@
import { cn } from "@/lib/utils"
import { useTranslations } from "next-intl"
import React, { useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
export default function TabSwitch({
tabs,

View File

@ -5,7 +5,7 @@ export const Loader = ({ visible }: { visible: boolean }) => {
<div className="hamster-loading-wrapper" data-visible={visible}>
<div className="hamster-spinner">
{bars.map((_, i) => (
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
<div className="hamster-loading-bar" key={`hamster-bar-${i + 1}`} />
))}
</div>
</div>

View File

@ -7,16 +7,16 @@ export default function NetworkChartLoading() {
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
<CardTitle className="flex items-center gap-0.5 text-xl">
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
<div className="aspect-auto h-[20px] w-24 bg-muted" />
</CardTitle>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted" />
</div>
<div className="hidden pr-4 pt-4 sm:block">
<Loader visible={true} />
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<div className="aspect-auto h-[250px] w-full"></div>
<div className="aspect-auto h-[250px] w-full" />
</CardContent>
</Card>
)

View File

@ -6,12 +6,12 @@ export function ServerDetailChartLoading() {
return (
<div>
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" />
</section>
</div>
)
@ -24,14 +24,14 @@ export function ServerDetailLoading() {
<>
<div
onClick={() => {
router.push(`/`)
router.push("/")
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
>
<BackIcon />
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none" />
</div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none" />
</>
)
}

View File

@ -38,6 +38,7 @@ export default function AnimatedCircularProgressBar({
}
>
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
<title>Circular Progress Bar</title>
{currentPercent <= 90 && currentPercent >= 0 && (
<circle
cx="50"

View File

@ -1,6 +1,5 @@
import Image from "next/image"
import Link from "next/link"
import React from "react"
export const AnimatedTooltip = ({
items,

View File

@ -1,6 +1,6 @@
import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import type * as React from "react"
const badgeVariants = cva(
"inline-flex items-center text-nowarp rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors pointer-events-none focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,3 @@
import React from "react"
import type { SVGProps } from "react"
export function GetFontLogoClass(platform: string): string {
@ -50,16 +49,16 @@ export function GetFontLogoClass(platform: string): string {
) {
return platform
}
if (platform == "darwin") {
if (platform === "darwin") {
return "apple"
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux"
}
if (platform == "amazon") {
if (platform === "amazon") {
return "redhat"
}
if (platform == "arch") {
if (platform === "arch") {
return "archlinux"
}
if (platform.toLowerCase().includes("opensuse")) {
@ -113,16 +112,16 @@ export function GetOsName(platform: string): string {
) {
return platform.charAt(0).toUpperCase() + platform.slice(1)
}
if (platform == "darwin") {
if (platform === "darwin") {
return "macOS"
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux"
}
if (platform == "amazon") {
if (platform === "amazon") {
return "Redhat"
}
if (platform == "arch") {
if (platform === "arch") {
return "Archlinux"
}
if (platform.toLowerCase().includes("opensuse")) {
@ -134,10 +133,11 @@ export function GetOsName(platform: string): string {
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<title>Mage Microsoft Windows</title>
<path
fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path>
/>
</svg>
)
}

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 56 MiB

After

Width:  |  Height:  |  Size: 56 MiB

View File

@ -1,7 +1,7 @@
"use server"
import { NezhaAPI, ServerApi } from "@/app/types/nezha-api"
import { MakeOptional } from "@/app/types/utils"
import type { NezhaAPI, ServerApi } from "@/app/types/nezha-api"
import type { MakeOptional } from "@/app/types/utils"
import getEnv from "@/lib/env-entry"
import { unstable_noStore as noStore } from "next/cache"
@ -71,9 +71,9 @@ export async function GetNezhaData() {
}
// Remove unwanted properties
delete element.ipv4
delete element.ipv6
delete element.valid_ip
element.ipv4 = undefined
element.ipv6 = undefined
element.valid_ip = undefined
return element
},
@ -213,9 +213,9 @@ export async function GetServerDetail({ server_id }: { server_id: number }) {
const timestamp = Date.now() / 1000
const detailData = detailDataList.map((element) => {
element.online_status = timestamp - element.last_active <= 180
delete element.ipv4
delete element.ipv6
delete element.valid_ip
element.ipv4 = undefined
element.ipv6 = undefined
element.valid_ip = undefined
return element
})[0]

View File

@ -1,4 +1,4 @@
import { NezhaAPISafe } from "@/app/types/nezha-api"
import type { NezhaAPISafe } from "@/app/types/nezha-api"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
@ -42,7 +42,7 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
}
}
export function formatBytes(bytes: number, decimals: number = 2) {
export function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes"
const k = 1024
@ -51,7 +51,7 @@ export function formatBytes(bytes: number, decimals: number = 2) {
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
}
export function getDaysBetweenDates(date1: string, date2: string): number {
@ -102,11 +102,14 @@ export function formatRelativeTime(timestamp: number): string {
if (hours > 24) {
const days = Math.floor(hours / 24)
return `${days}d`
} else if (hours > 0) {
}
if (hours > 0) {
return `${hours}h`
} else if (minutes > 0) {
}
if (minutes > 0) {
return `${minutes}m`
} else if (seconds >= 0) {
}
if (seconds >= 0) {
return `${seconds}s`
}
return "0s"

View File

@ -1,6 +1,6 @@
{
"name": "nezha-dash",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"scripts": {
"dev": "next dev -p 3040",
@ -31,20 +31,20 @@
"@turf/turf": "^7.2.0",
"@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"caniuse-lite": "^1.0.30001692",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"caniuse-lite": "^1.0.30001695",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.14",
"d3-geo": "^3.1.1",
"d3-selection": "^3.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"flag-icons": "^7.2.3",
"flag-icons": "^7.3.2",
"i18n-iso-countries": "^7.13.0",
"lucide-react": "^0.454.0",
"luxon": "^3.5.0",
"maxmind": "^4.3.23",
"next": "^15.1.4",
"next": "^15.1.5",
"next-auth": "^5.0.0-beta.25",
"next-intl": "^3.26.3",
"next-runtime-env": "^3.2.2",
@ -52,29 +52,29 @@
"react": "^19.0.0",
"react-device-detect": "^2.2.3",
"react-dom": "^19.0.0",
"react-intersection-observer": "^9.14.1",
"react-intersection-observer": "^9.15.0",
"react-wrap-balancer": "^1.1.1",
"recharts": "^2.15.0",
"sharp": "^0.33.5",
"swr": "^2.3.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"typescript-eslint": "^8.19.1"
"typescript-eslint": "^8.21.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@next/bundle-analyzer": "^15.1.4",
"@next/bundle-analyzer": "^15.1.5",
"@tailwindcss/postcss": "^4.0.0-beta.9",
"@types/node": "^22.10.5",
"@types/react": "^19.0.5",
"@types/node": "^22.10.7",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"eslint-config-next": "^15.1.4",
"eslint-config-next": "^15.1.5",
"eslint-plugin-turbo": "^2.3.3",
"eslint-plugin-unused-imports": "^4.1.4",
"postcss": "^8.4.49",
"postcss": "^8.5.1",
"tailwindcss": "^4.0.0-beta.9",
"typescript": "^5.7.3",
"vercel": "^39.2.6"
"vercel": "^39.3.0"
},
"overrides": {
"react-is": "^19.0.0-rc-69d4b800-20241021"

View File

@ -3,8 +3,8 @@
@variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));