diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a5cfcf0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +FROM node:21-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +RUN apk --no-cache add ca-certificates wget +RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub +RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk +RUN apk add --no-cache --force-overwrite glibc-2.28-r0.apk + +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lockb* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ + elif [ -f bun.lockb ]; then npm install -g bun && bun install; \ + else echo "Lockfile not found." && exit 1; \ + fi + + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG PROD_ENV="" +# Appends to .env.production +RUN printf "$PROD_ENV" >> .env.production + +RUN yarn build + + +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.env.production ./.env.production + +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + + + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index ed1f032..aa26433 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,11 @@ ### 一键部署到 Vercel + [部署简易教程](https://buycoffee.top/blog/tech/nezha)

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fhamster1963%2Fnezha-dash&env=NezhaBaseUrl,NezhaAuth&project-name=nezha-dash&repository-name=nezha-dash) - - - ![screen-shot-one](/.github/shotOne.png) ![screen-shot-two](/.github/shotTwo.png) diff --git a/app/(main)/ClientComponents/ServerListClient.tsx b/app/(main)/ClientComponents/ServerListClient.tsx index 2f0765b..d899f4e 100644 --- a/app/(main)/ClientComponents/ServerListClient.tsx +++ b/app/(main)/ClientComponents/ServerListClient.tsx @@ -4,33 +4,31 @@ import { ServerApi } from "@/app/types/nezha-api"; import ServerCard from "@/components/ServerCard"; import { nezhaFetcher } from "@/lib/utils"; import useSWR from "swr"; -import { DateTime } from "luxon"; export default function ServerListClient() { - const { data } = useSWR('/api/server', nezhaFetcher, { + const { data } = useSWR("/api/server", nezhaFetcher, { refreshInterval: 3000, }); if (!data) return null; const sortedResult = data.result.sort((a: any, b: any) => a.id - b.id); + const timestamp = Date.now() / 1000; return ( -
- {sortedResult.map( - (server: any) => ( - 300 ? "offline" : "online"} - uptime={server.status.Uptime / 86400} - mem={(server.status.MemUsed / server.host.MemTotal) * 100} - stg={(server.status.DiskUsed / server.host.DiskTotal) * 100} - /> - ), - )} +
+ {sortedResult.map((server: any) => ( + 300 ? "offline" : "online"} + uptime={server.status.Uptime / 86400} + mem={(server.status.MemUsed / server.host.MemTotal) * 100} + stg={(server.status.DiskUsed / server.host.DiskTotal) * 100} + /> + ))}
); } diff --git a/app/(main)/ClientComponents/ServerOverviewClient.tsx b/app/(main)/ClientComponents/ServerOverviewClient.tsx index 8ec1b76..d64c1d5 100644 --- a/app/(main)/ClientComponents/ServerOverviewClient.tsx +++ b/app/(main)/ClientComponents/ServerOverviewClient.tsx @@ -9,70 +9,94 @@ import { Loader } from "@/components/loading/Loader"; import { ServerApi } from "@/app/types/nezha-api"; export default function ServerOverviewClient() { - const { data } = useSWR('/api/server', nezhaFetcher, { - refreshInterval: 30000 - }); + const { data } = useSWR("/api/server", nezhaFetcher, { + refreshInterval: 30000, + }); - return ( -
- - -
-

Total servers

-
- - - - {data ?

{data?.result.length}

:
} -
-
-
-
- - -
-

Online servers

-
- - - - - {data ?

{data?.live_servers}

:
} -
-
-
-
- - -
-

Offline servers

-
- - - - - {data ?

{data?.offline_servers}

:
} -
- - -
-
-
- - -
-

Total bandwidth

- {data ?

{formatBytes(data?.total_bandwidth)}

:
} -
- {'Hamster1963'} -
-
-
- ) -} \ No newline at end of file + return ( +
+ + +
+

Total servers

+
+ + + + {data ? ( +

{data?.result.length}

+ ) : ( +
+ +
+ )} +
+
+
+
+ + +
+

Online servers

+
+ + + + + {data ? ( +

{data?.live_servers}

+ ) : ( +
+ +
+ )} +
+
+
+
+ + +
+

Offline servers

+
+ + + + + {data ? ( +

{data?.offline_servers}

+ ) : ( +
+ +
+ )} +
+
+
+
+ + +
+

Total bandwidth

+ {data ? ( +

+ {formatBytes(data?.total_bandwidth)} +

+ ) : ( +
+ +
+ )} +
+ {"Hamster1963"} +
+
+
+ ); +} diff --git a/app/(main)/footer.tsx b/app/(main)/footer.tsx index a14f070..2792eb3 100644 --- a/app/(main)/footer.tsx +++ b/app/(main)/footer.tsx @@ -1,22 +1,22 @@ export default function Footer() { - return ( - - ) + return ( + + ); } diff --git a/app/(main)/header.tsx b/app/(main)/header.tsx index f062240..65702ba 100644 --- a/app/(main)/header.tsx +++ b/app/(main)/header.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { Separator } from "@/components/ui/separator"; import { DateTime } from "luxon"; @@ -36,22 +36,45 @@ function Header() { ); } +// https://github.com/streamich/react-use/blob/master/src/useInterval.ts +const useInterval = (callback: Function, delay?: number | null) => { + const savedCallback = useRef(() => { }); + + useEffect(() => { + savedCallback.current = callback; + }); + + useEffect(() => { + if (delay !== null) { + const interval = setInterval(() => savedCallback.current(), delay || 0); + return () => clearInterval(interval); + } + + return undefined; + }, [delay]); +}; + function Overview() { const [mouted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); - const time = DateTime.TIME_SIMPLE; - time.hour12 = true; + const timeOption = DateTime.TIME_SIMPLE; + timeOption.hour12 = true; + const [timeString, setTimeString] = useState(DateTime.now().setLocale("en-US").toLocaleString(timeOption)); + + useInterval(() => { + setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption)); + }, 1000); return ( -
+

👋 Overview

where the time is

{mouted && (

- {DateTime.now().setLocale("en-US").toLocaleString(time)} + {timeString}

)}
diff --git a/app/(main)/nav.tsx b/app/(main)/nav.tsx deleted file mode 100644 index 3f8b907..0000000 --- a/app/(main)/nav.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import { clsx } from "clsx"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import React from "react"; - -import { cn } from "@/lib/utils"; - -export const siteUrlList = [ - { - name: "Home", - header: "👋 Overview", - url: "/", - }, - { - name: "Service", - header: "🎛️ Service", - url: "/service", - }, -]; -export default function Nav() { - const nowPath = usePathname(); - return ( -
-
- {siteUrlList.map((site, index) => ( -
- {index !== 0 && ( -

- / -

- )} - - {site.name} - -
- ))} -
-
- ); -} diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 26e7e08..e7dcb53 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -1,14 +1,21 @@ import ServerList from "@/components/ServerList"; import ServerOverview from "@/components/ServerOverview"; - - +import { GetNezhaData } from "@/lib/prefetch"; +import { SWRConfig } from "swr"; export default function Home() { - return ( -
- - -
+ +
+ + +
+
); } diff --git a/app/api/server/route.ts b/app/api/server/route.ts index b08f38a..08789e0 100644 --- a/app/api/server/route.ts +++ b/app/api/server/route.ts @@ -1,56 +1,58 @@ - import { NezhaAPI, ServerApi } from "@/app/types/nezha-api"; import { MakeOptional } from "@/app/types/utils"; import { NextResponse } from "next/server"; -import { DateTime } from "luxon"; export async function GET(_: Request) { - if (!process.env.NezhaBaseUrl) { - return NextResponse.json({ error: 'NezhaBaseUrl is not set' }, { status: 400 }) - } + if (!process.env.NezhaBaseUrl) { + return NextResponse.json( + { error: "NezhaBaseUrl is not set" }, + { status: 400 }, + ); + } - // Remove trailing slash - var nezhaBaseUrl = process.env.NezhaBaseUrl; + // Remove trailing slash + var nezhaBaseUrl = process.env.NezhaBaseUrl; - if (process.env.NezhaBaseUrl[process.env.NezhaBaseUrl.length - 1] === '/') { - nezhaBaseUrl = process.env.NezhaBaseUrl.slice(0, -1); - } + if (process.env.NezhaBaseUrl[process.env.NezhaBaseUrl.length - 1] === "/") { + nezhaBaseUrl = process.env.NezhaBaseUrl.slice(0, -1); + } - try { - const response = await fetch(nezhaBaseUrl+ '/api/v1/server/details',{ - headers: { - 'Authorization': process.env.NezhaAuth as string - }, - next:{ - revalidate:1 - } - }); - const nezhaData = (await response.json()).result as NezhaAPI[]; - const data: ServerApi = { - live_servers: 0, - offline_servers: 0, - total_bandwidth: 0, - result: [] + try { + const response = await fetch(nezhaBaseUrl + "/api/v1/server/details", { + headers: { + Authorization: process.env.NezhaAuth as string, + }, + next: { + revalidate: 0, + }, + }); + const nezhaData = (await response.json()).result as NezhaAPI[]; + const data: ServerApi = { + live_servers: 0, + offline_servers: 0, + total_bandwidth: 0, + result: [], + }; + const timestamp = Date.now() / 1000; + data.result = nezhaData.map( + (element: MakeOptional) => { + if (timestamp - element.last_active > 300) { + data.offline_servers += 1; + } else { + data.live_servers += 1; } + data.total_bandwidth += element.status.NetOutTransfer; - data.result = nezhaData.map((element: MakeOptional) => { - if (DateTime.now().toUnixInteger() - element.last_active > 300) { - data.offline_servers += 1; - } else { - data.live_servers += 1; - } - data.total_bandwidth += element.status.NetOutTransfer; + delete element.ipv4; + delete element.ipv6; + delete element.valid_ip; - delete element.ipv4; - delete element.ipv6; - delete element.valid_ip; + return element; + }, + ); - return element; - }); - - return NextResponse.json(data, { status: 200 }) - } catch (error) { - return NextResponse.json({ error: error }, { status: 200 }) - } - -} \ No newline at end of file + return NextResponse.json(data, { status: 200 }); + } catch (error) { + return NextResponse.json({ error: error }, { status: 200 }); + } +} diff --git a/app/types/utils.ts b/app/types/utils.ts index 488df36..69b6f87 100644 --- a/app/types/utils.ts +++ b/app/types/utils.ts @@ -1 +1,2 @@ -export type MakeOptional = Omit & Partial>; +export type MakeOptional = Omit & + Partial>; diff --git a/bun.lockb b/bun.lockb index 23316e6..113abeb 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/ServerCard.tsx b/components/ServerCard.tsx index 9397acf..6120137 100644 --- a/components/ServerCard.tsx +++ b/components/ServerCard.tsx @@ -40,9 +40,11 @@ export default function ServerCard({ -
+
-

+

{name}

@@ -90,7 +92,9 @@ export default function ServerCard({ -
+

{name}

diff --git a/components/ServerList.tsx b/components/ServerList.tsx index db72679..14c28d2 100644 --- a/components/ServerList.tsx +++ b/components/ServerList.tsx @@ -2,10 +2,6 @@ import React from "react"; import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient"; - - export default async function ServerList() { - return ( - - ); + return ; } diff --git a/components/ServerOverview.tsx b/components/ServerOverview.tsx index d5523fa..2e61c79 100644 --- a/components/ServerOverview.tsx +++ b/components/ServerOverview.tsx @@ -1,12 +1,5 @@ import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient"; - - - export default async function ServerOverview() { - return ( - - ) - - -} \ No newline at end of file + return ; +} diff --git a/components/SubscribeCard.tsx b/components/SubscribeCard.tsx deleted file mode 100644 index b52d9fa..0000000 --- a/components/SubscribeCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react"; - -import { Loader } from "@/components/loading/Loader"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Progress } from "@/components/ui/progress"; - -type SubscribeCardProps = { - provider: string; - behavior: string; - value: number; - nextBillDay?: number; - total: number; - unit: string; - colorClassName: string; - isLoading: boolean; -}; - -function SubscribeCard({ - provider, - behavior, - value, - total, - unit, - nextBillDay, - colorClassName, - isLoading, -}: SubscribeCardProps) { - return ( - - - - {provider} - - {nextBillDay !== -1 && ( -
-

- {nextBillDay} -

-

|

-
- )} -
- -
-

{behavior}

- -
- -
-

{value}

-

- {unit} -

-
- -
-
- ); -} - -export default SubscribeCard; diff --git a/lib/prefetch.tsx b/lib/prefetch.tsx new file mode 100644 index 0000000..911edbb --- /dev/null +++ b/lib/prefetch.tsx @@ -0,0 +1,54 @@ +import { NezhaAPI, ServerApi } from "@/app/types/nezha-api"; +import { MakeOptional } from "@/app/types/utils"; +import { error } from "console"; + +export async function GetNezhaData() { + if (!process.env.NezhaBaseUrl) { + error("NezhaBaseUrl is not set"); + return; + } + // Remove trailing slash + var nezhaBaseUrl = process.env.NezhaBaseUrl; + + if (process.env.NezhaBaseUrl[process.env.NezhaBaseUrl.length - 1] === "/") { + nezhaBaseUrl = process.env.NezhaBaseUrl.slice(0, -1); + } + try { + const response = await fetch(nezhaBaseUrl + "/api/v1/server/details", { + headers: { + Authorization: process.env.NezhaAuth as string, + }, + next: { + revalidate: 1, + }, + }); + const nezhaData = (await response.json()).result as NezhaAPI[]; + const data: ServerApi = { + live_servers: 0, + offline_servers: 0, + total_bandwidth: 0, + result: [], + }; + const timestamp = Date.now() / 1000; + data.result = nezhaData.map( + (element: MakeOptional) => { + if (timestamp - element.last_active > 300) { + data.offline_servers += 1; + } else { + data.live_servers += 1; + } + data.total_bandwidth += element.status.NetOutTransfer; + + delete element.ipv4; + delete element.ipv6; + delete element.valid_ip; + + return element; + }, + ); + + return data; + } catch (error) { + return error; + } +} \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts index 4025a4e..6dcf264 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -6,15 +6,25 @@ export function cn(...inputs: ClassValue[]) { } export function formatBytes(bytes: number, decimals: number = 2) { - if (!+bytes) return '0 Bytes' + if (!+bytes) return "0 Bytes"; - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = [ + "Bytes", + "KiB", + "MiB", + "GiB", + "TiB", + "PiB", + "EiB", + "ZiB", + "YiB", + ]; - const i = Math.floor(Math.log(bytes) / Math.log(k)) + const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; } export function getDaysBetweenDates(date1: string, date2: string): number { @@ -43,16 +53,15 @@ export const fetcher = (url: string) => }); export const nezhaFetcher = (url: string) => - fetch(url) - .then((res) => { - if (!res.ok) { - throw new Error(res.statusText); - } - return res.json(); - }) - .then((data) => data) - .catch((err) => { - console.error(err); - throw err; - }); - \ No newline at end of file + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText); + } + return res.json(); + }) + .then((data) => data) + .catch((err) => { + console.error(err); + throw err; + }); diff --git a/next.config.mjs b/next.config.mjs index 9b4c182..7e5fcf7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -18,6 +18,9 @@ const withPWA = withPWAInit({ }); /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + output: "standalone", + reactStrictMode: true, +}; export default bundleAnalyzer(withPWA(nextConfig)); diff --git a/package.json b/package.json index 5deab95..9bcef28 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "private": true, "scripts": { "dev": "next dev -p 3020", - "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "node .next/standalone/server.js", + "lint": "next lint", + "build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/" }, "dependencies": { "@ducanh2912/next-pwa": "^10.2.6", @@ -33,7 +33,7 @@ "react-wrap-balancer": "^1.1.0", "sharp": "^0.33.3", "sonner": "^1.4.41", - "swr": "^2.2.6-beta.3", + "swr": "^2.2.6-beta.4", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4"