Merge branch 'cloudflare' into cloudflare-dev

This commit is contained in:
hamster1963 2024-10-24 21:30:42 +08:00
commit 51e66ef2f1
32 changed files with 2205 additions and 286 deletions

View File

@ -18,11 +18,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
@ -50,30 +49,19 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
changelog: release:
name: Generate Changelog
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-and-push
outputs:
release_body: ${{ steps.git-cliff.outputs.content }}
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4 - name: Set node
id: git-cliff uses: actions/setup-node@v4
with: with:
config: git-cliff-config/cliff.toml registry-url: https://registry.npmjs.org/
args: -vv --latest --strip 'footer' node-version: lts/*
- run: npx changelogithub
env: env:
OUTPUT: CHANGES.md GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
body: ${{ steps.git-cliff.outputs.content }}
token: ${{ secrets.GITHUB_TOKEN }}
env:
GITHUB_REPOSITORY: ${{ github.repository }}

View File

@ -1,4 +1,4 @@
FROM oven/bun:1 AS base FROM --platform=$BUILDPLATFORM oven/bun:1 AS base
# Stage 1: Install dependencies # Stage 1: Install dependencies
FROM base AS deps FROM base AS deps

View File

@ -5,10 +5,10 @@
</div> </div>
| 一键部署到 Vercel-推荐 | Docker部署 | Cloudflare部署 | 如何更新? | | 一键部署到 Vercel-推荐 | Docker部署 | Cloudflare部署 | 如何更新? |
| ----------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------- | | ----------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------- |
| [部署简易教程](https://buycoffee.top/blog/tech/nezha) | [Docker 部署教程](https://buycoffee.top/blog/tech/nezha-docker) | [Cloudflare 部署教程](https://buycoffee.top/blog/tech/nezha-cloudflare) | [更新教程](https://buycoffee.top/blog/tech/nezha-upgrade) | | [部署简易教程](https://buycoffee.top/blog/tech/nezha) | [Docker 部署教程](https://buycoffee.top/blog/tech/nezha-docker) | [Cloudflare 部署教程](https://buycoffee.top/blog/tech/nezha-cloudflare) | [更新教程](https://buycoffee.top/blog/tech/nezha-upgrade) |
| [Vercel-demo](https://nezha-vercel.buycoffee.top) | [Docker-demo](https://nezha-docker.buycoffee.tech) | [Cloudflare-demo](https://nezha-cloudflare.buycoffee.tech) | | [Vercel-demo](https://nezha-vercel.buycoffee.top) | [Docker-demo](https://nezha-docker.buycoffee.tech) | [Cloudflare-demo](https://nezha-cloudflare.buycoffee.tech) [密码: nezhadash] |
#### 环境变量 #### 环境变量
@ -16,7 +16,7 @@
| ------------------------------ | ------------------------ | ------------------------------------------------------------- | | ------------------------------ | ------------------------ | ------------------------------------------------------------- |
| NezhaBaseUrl | nezha 面板地址 | http://120.x.x.x:8008 | | NezhaBaseUrl | nezha 面板地址 | http://120.x.x.x:8008 |
| NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi | | NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi |
| SitePassword | 页面密码 | 123456 | | SitePassword | 页面密码 | **默认**:无密码 |
| DefaultLocale | 面板默认显示语言 | **默认**en [简中:zh 繁中:zh-t 英语:en 日语:ja] | | DefaultLocale | 面板默认显示语言 | **默认**en [简中:zh 繁中:zh-t 英语:en 日语:ja] |
| ForceShowAllServers | 是否强制显示所有服务器 | **默认**false | | ForceShowAllServers | 是否强制显示所有服务器 | **默认**false |
| NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**2000 | | NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**2000 |

View File

@ -51,7 +51,7 @@ export function NetworkChartClient({
{ {
refreshInterval: refreshInterval:
Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000, Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000,
isPaused: () => !show, isVisible: () => show,
}, },
); );
@ -181,13 +181,7 @@ export const NetworkChart = React.memo(function NetworkChart({
<Card> <Card>
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row"> <CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row">
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4"> <div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
<CardTitle <CardTitle className="flex flex-none items-center gap-0.5 text-md">
onClick={() => {
router.push(`/${locale}/`);
}}
className="flex flex-none cursor-pointer items-center gap-0.5 text-xl"
>
<BackIcon />
{serverName} {serverName}
</CardTitle> </CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">

View File

@ -12,13 +12,7 @@ export default function NetworkChartLoading() {
<Card> <Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row"> <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"> <div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
<CardTitle <CardTitle className="flex items-center gap-0.5 text-xl">
onClick={() => {
router.push(`/${locale}/`);
}}
className="flex items-center cursor-pointer gap-0.5 text-xl"
>
<BackIcon />
<div className="aspect-auto h-[20px] w-24 bg-muted"></div> <div className="aspect-auto h-[20px] w-24 bg-muted"></div>
</CardTitle> </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>

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { ServerDetailChartLoading } from "@/app/[locale]/(main)/ClientComponents/ServerDetailLoading"; import { ServerDetailChartLoading } from "@/app/[locale]/(main)/ClientComponents/ServerDetailLoading";
import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api"; import { NezhaAPISafe, ServerApi } from "@/app/[locale]/types/nezha-api";
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"; import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { ChartConfig, ChartContainer } from "@/components/ui/chart"; import { ChartConfig, ChartContainer } from "@/components/ui/chart";
@ -19,6 +19,7 @@ import {
YAxis, YAxis,
} from "recharts"; } from "recharts";
import useSWR from "swr"; import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
type cpuChartData = { type cpuChartData = {
timeStamp: string; timeStamp: string;
@ -62,12 +63,23 @@ export default function ServerDetailChartClient({
}) { }) {
const t = useTranslations("ServerDetailChartClient"); const t = useTranslations("ServerDetailChartClient");
const { data: allFallbackData } = useSWRImmutable<ServerApi>(
"/api/server",
nezhaFetcher,
);
const fallbackData = allFallbackData?.result?.find(
(item) => item.id === server_id,
);
const { data, error } = useSWR<NezhaAPISafe>( const { data, error } = useSWR<NezhaAPISafe>(
`/api/detail?server_id=${server_id}`, `/api/detail?server_id=${server_id}`,
nezhaFetcher, nezhaFetcher,
{ {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
isPaused: () => !show, isVisible: () => show,
fallbackData,
revalidateOnMount: false,
revalidateIfStale: false,
}, },
); );
@ -128,7 +140,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
} satisfies ChartConfig; } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card>
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -231,7 +243,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
} satisfies ChartConfig; } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card>
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -324,7 +336,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
} satisfies ChartConfig; } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card>
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center"> <div className="flex items-center">
@ -445,7 +457,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
} satisfies ChartConfig; } satisfies ChartConfig;
return ( return (
<Card className="rounded-sm"> <Card>
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -557,7 +569,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
} satisfies ChartConfig; } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card>
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center"> <div className="flex items-center">
@ -677,7 +689,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
} satisfies ChartConfig; } satisfies ChartConfig;
return ( return (
<Card className="rounded-sm"> <Card>
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center"> <div className="flex items-center">

View File

@ -1,15 +1,18 @@
"use client"; "use client";
import { ServerDetailLoading } from "@/app/[locale]/(main)/ClientComponents/ServerDetailLoading"; import { ServerDetailLoading } from "@/app/[locale]/(main)/ClientComponents/ServerDetailLoading";
import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api"; import { NezhaAPISafe, ServerApi } from "@/app/[locale]/types/nezha-api";
import { BackIcon } from "@/components/Icon"; import { BackIcon } from "@/components/Icon";
import ServerFlag from "@/components/ServerFlag";
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 getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"; import { cn, formatBytes, nezhaFetcher } from "@/lib/utils";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
export default function ServerDetailClient({ export default function ServerDetailClient({
server_id, server_id,
@ -19,11 +22,47 @@ export default function ServerDetailClient({
const t = useTranslations("ServerDetailClient"); const t = useTranslations("ServerDetailClient");
const router = useRouter(); const router = useRouter();
const locale = useLocale(); const locale = useLocale();
const [hasHistory, setHasHistory] = useState(false);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
useEffect(() => {
const previousPath = sessionStorage.getItem("lastPath");
if (previousPath) {
setHasHistory(true);
} else {
const currentPath = window.location.pathname;
sessionStorage.setItem("lastPath", currentPath);
}
}, []);
const linkClick = () => {
if (hasHistory) {
router.back();
} else {
router.push(`/${locale}/`);
}
};
const { data: allFallbackData } = useSWRImmutable<ServerApi>(
"/api/server",
nezhaFetcher,
);
const fallbackData = allFallbackData?.result?.find(
(item) => item.id === server_id,
);
const { data, error } = useSWR<NezhaAPISafe>( const { data, error } = useSWR<NezhaAPISafe>(
`/api/detail?server_id=${server_id}`, `/api/detail?server_id=${server_id}`,
nezhaFetcher, nezhaFetcher,
{ {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
fallbackData,
revalidateOnMount: false,
revalidateIfStale: false,
}, },
); );
@ -45,9 +84,7 @@ export default function ServerDetailClient({
return ( return (
<div> <div>
<div <div
onClick={() => { onClick={linkClick}
router.push(`/${locale}/`);
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
> >
<BackIcon /> <BackIcon />
@ -60,7 +97,7 @@ export default function ServerDetailClient({
<p className="text-xs text-muted-foreground">{t("status")}</p> <p className="text-xs text-muted-foreground">{t("status")}</p>
<Badge <Badge
className={cn( className={cn(
"text-[10px] rounded-[6px] w-fit px-1 py-0 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": data?.online_status,
" bg-red-600": !data?.online_status, " bg-red-600": !data?.online_status,
@ -115,6 +152,22 @@ export default function ServerDetailClient({
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<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("Region")}</p>
<section className="flex items-start gap-1">
<div className="text-xs text-start">
{data?.host.CountryCode.toUpperCase()}
</div>
<ServerFlag
className="text-[11px] -mt-[1px]"
country_code={data?.host.CountryCode}
/>
</section>
</section>
</CardContent>
</Card>
</section> </section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">

View File

@ -6,17 +6,56 @@ import Switch from "@/components/Switch";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { nezhaFetcher } from "@/lib/utils"; import { nezhaFetcher } from "@/lib/utils";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
export default function ServerListClient() { export default function ServerListClient() {
const t = useTranslations("ServerListClient"); const t = useTranslations("ServerListClient");
const containerRef = useRef<HTMLDivElement>(null);
const defaultTag = t("defaultTag");
const [tag, setTag] = useState<string>(t("defaultTag")); const [tag, setTag] = useState<string>(defaultTag);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag;
setTag(savedTag);
restoreScrollPosition();
setIsMounted(true);
}, []);
const handleTagChange = (newTag: string) => {
setTag(newTag);
sessionStorage.setItem("selectedTag", newTag);
sessionStorage.setItem(
"scrollPosition",
String(containerRef.current?.scrollTop || 0),
);
};
const restoreScrollPosition = () => {
const savedPosition = sessionStorage.getItem("scrollPosition");
if (savedPosition && containerRef.current) {
containerRef.current.scrollTop = Number(savedPosition);
}
};
useEffect(() => {
const handleRouteChange = () => {
restoreScrollPosition();
};
window.addEventListener("popstate", handleRouteChange);
return () => {
window.removeEventListener("popstate", handleRouteChange);
};
}, []);
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, { const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
}); });
if (error) if (error)
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
@ -24,32 +63,38 @@ export default function ServerListClient() {
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="text-sm font-medium opacity-40">{t("error_message")}</p>
</div> </div>
); );
if (!data?.result) return null;
if (!data?.result || !isMounted) return null;
const { result } = data; const { result } = data;
const sortedServers = result.sort((a, b) => { const sortedServers = result.sort((a, b) => {
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0); const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0);
if (displayIndexDiff !== 0) return displayIndexDiff; if (displayIndexDiff !== 0) return displayIndexDiff;
return a.id - b.id; return a.id - b.id;
}); });
const allTag = sortedServers.map((server) => server.tag).filter((tag) => tag); const allTag = sortedServers.map((server) => server.tag).filter(Boolean);
const uniqueTags = [...new Set(allTag)]; const uniqueTags = [...new Set(allTag)];
uniqueTags.unshift(defaultTag);
uniqueTags.unshift(t("defaultTag"));
const filteredServers = const filteredServers =
tag === t("defaultTag") tag === defaultTag
? sortedServers ? sortedServers
: sortedServers.filter((server) => server.tag === tag); : sortedServers.filter((server) => server.tag === tag);
return ( return (
<> <>
{getEnv("NEXT_PUBLIC_ShowTag") === "true" && uniqueTags.length > 1 && ( {getEnv("NEXT_PUBLIC_ShowTag") === "true" && uniqueTags.length > 1 && (
<Switch allTag={uniqueTags} nowTag={tag} setTag={setTag} /> <Switch
allTag={uniqueTags}
nowTag={tag}
onTagChange={handleTagChange}
/>
)} )}
<section className="grid grid-cols-1 gap-2 md:grid-cols-2"> <section
ref={containerRef}
className="grid grid-cols-1 gap-2 md:grid-cols-2"
>
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCard key={serverInfo.id} serverInfo={serverInfo} /> <ServerCard key={serverInfo.id} serverInfo={serverInfo} />
))} ))}

View File

@ -18,13 +18,16 @@ export default function ServerOverviewClient() {
); );
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"; const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true";
if (error) if (error) {
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="text-sm font-medium opacity-40">
Error status:{error.status} {error.info?.cause ?? error.message}
</p>
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="text-sm font-medium opacity-40">{t("error_message")}</p>
</div> </div>
); );
}
return ( return (
<> <>

View File

@ -29,12 +29,18 @@ export default function Page({ params }: { params: { id: string } }) {
</div> </div>
<Separator className="flex-1" /> <Separator className="flex-1" />
</section> </section>
{currentTab === tabs[0] && ( <div style={{ display: currentTab === tabs[0] ? "block" : "none" }}>
<ServerDetailChartClient server_id={Number(params.id)} show={true} /> <ServerDetailChartClient
)} server_id={Number(params.id)}
{currentTab === tabs[1] && ( show={currentTab === tabs[0]}
<NetworkChartClient server_id={Number(params.id)} show={true} /> />
)} </div>
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
<NetworkChartClient
server_id={Number(params.id)}
show={currentTab === tabs[1]}
/>
</div>
</div> </div>
); );
} }

View File

@ -1,7 +1,9 @@
import pack from "@/package.json";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
export default function Footer() { export default function Footer() {
const t = useTranslations("Footer"); const t = useTranslations("Footer");
const version = pack.version;
return ( return (
<footer className="mx-auto w-full max-w-5xl"> <footer className="mx-auto w-full max-w-5xl">
<section className="flex flex-col"> <section className="flex flex-col">
@ -14,6 +16,13 @@ export default function Footer() {
> >
{t("a_303-585_GitHub")} {t("a_303-585_GitHub")}
</a> </a>
<a
href={`https://github.com/hamster1963/nezha-dash/releases/tag/v${version}`}
target="_blank"
className="cursor-pointer font-normal underline decoration-yellow-500 decoration-2 underline-offset-2 dark:decoration-yellow-500/50"
>
v{version}
</a>
</p> </p>
<section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50"> <section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
{t("section_607-869_2020")} {t("section_607-869_2020")}

View File

@ -26,6 +26,7 @@ function Header() {
<section className="flex items-center justify-between"> <section className="flex items-center justify-between">
<section <section
onClick={() => { onClick={() => {
sessionStorage.removeItem("selectedTag");
router.push(`/${locale}/`); router.push(`/${locale}/`);
}} }}
className="flex cursor-pointer items-center text-base font-medium" className="flex cursor-pointer items-center text-base font-medium"
@ -98,7 +99,9 @@ function Overview() {
</p> </p>
{mouted ? ( {mouted ? (
<p className="opacity-1 text-sm font-medium">{timeString}</p> <p className="opacity-1 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"></Skeleton>
)}
</div> </div>
</section> </section>
); );

View File

@ -1,9 +1,8 @@
import Footer from "@/app/[locale]/(main)/footer"; import Footer from "@/app/[locale]/(main)/footer";
import Header from "@/app/[locale]/(main)/header"; import Header from "@/app/[locale]/(main)/header";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { SignIn } from "@/components/sign-in"; import { SignIn } from "@/components/SignIn";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { redirect } from "next/navigation";
import React from "react"; import React from "react";
type DashboardProps = { type DashboardProps = {
@ -12,19 +11,12 @@ type DashboardProps = {
export default async function MainLayout({ children }: DashboardProps) { export default async function MainLayout({ children }: DashboardProps) {
const session = await auth(); const session = await auth();
if (!session && getEnv("SitePassword")) {
// if (getEnv("CF_PAGES")) {
// redirect("/api/auth/signin");
// } else {
return <SignIn />;
// }
}
return ( return (
<div className="flex min-h-screen w-full flex-col"> <div className="flex min-h-screen w-full flex-col">
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:p-10 md:pt-8"> <main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:p-10 md:pt-8">
<Header /> <Header />
{children} {!session && getEnv("SitePassword") ? <SignIn /> : children}
<Footer /> <Footer />
</main> </main>
</div> </div>

View File

@ -1,5 +1,4 @@
// @auto-i18n-check. Please do not delete the line. // @auto-i18n-check. Please do not delete the line.
import { auth } from "@/auth";
import { locales } from "@/i18n-metadata"; import { locales } from "@/i18n-metadata";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -13,8 +12,6 @@ import { ThemeProvider } from "next-themes";
import { Inter as FontSans } from "next/font/google"; import { Inter as FontSans } from "next/font/google";
import React from "react"; import React from "react";
import "/node_modules/flag-icons/css/flag-icons.min.css";
const fontSans = FontSans({ const fontSans = FontSans({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-sans", variable: "--font-sans",
@ -65,6 +62,10 @@ export default function LocaleLayout({
<html lang={locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>
<head> <head>
<PublicEnvScript /> <PublicEnvScript />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css"
/>
</head> </head>
<body <body
className={cn( className={cn(

View File

@ -2,4 +2,4 @@ import { handlers } from "@/auth" // Referring to the auth.ts we just created
export const runtime = 'edge'; export const runtime = 'edge';
export const { GET, POST } = handlers export const { GET, POST } = handlers;

View File

@ -2,36 +2,45 @@ import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api";
import { auth } from "@/auth"; import { auth } from "@/auth";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { GetServerDetail } from "@/lib/serverFetch"; import { GetServerDetail } from "@/lib/serverFetch";
import { redirect } from "next/navigation";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const runtime = 'edge'; export const runtime = 'edge';
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
interface NezhaDataResponse {
error?: string;
data?: NezhaAPISafe;
}
export const GET = auth(async function GET(req) { export const GET = auth(async function GET(req) {
if (!req.auth && getEnv("SitePassword")) { if (!req.auth && getEnv("SitePassword")) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); redirect("/");
} }
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url);
const server_id = searchParams.get("server_id"); const server_id = searchParams.get("server_id");
if (!server_id) { if (!server_id) {
return NextResponse.json( return NextResponse.json(
{ error: "server_id is required" }, { error: "server_id is required" },
{ status: 400 }, { status: 400 },
); );
} }
const response = (await GetServerDetail({
server_id: parseInt(server_id), try {
})) as NezhaDataResponse; const serverIdNum = parseInt(server_id, 10);
if (response.error) { if (isNaN(serverIdNum)) {
console.log(response.error); return NextResponse.json(
return NextResponse.json({ error: response.error }, { status: 400 }); { error: "server_id must be a valid number" },
{ status: 400 },
);
}
const detailData = await GetServerDetail({ server_id: serverIdNum });
return NextResponse.json(detailData, { status: 200 });
} catch (error) {
console.error("Error in GET handler:", error);
// @ts-ignore
const statusCode = error.statusCode || 500;
// @ts-ignore
const message = error.message || "Internal Server Error";
return NextResponse.json({ error: message }, { status: statusCode });
} }
return NextResponse.json(response, { status: 200 });
}); });

View File

@ -1,21 +1,16 @@
import { ServerMonitorChart } from "@/app/[locale]/types/nezha-api";
import { auth } from "@/auth"; import { auth } from "@/auth";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { GetServerMonitor } from "@/lib/serverFetch"; import { GetServerMonitor } from "@/lib/serverFetch";
import { redirect } from "next/navigation";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const runtime = "edge"; export const runtime = "edge";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
interface NezhaDataResponse {
error?: string;
data?: ServerMonitorChart;
}
export const GET = auth(async function GET(req) { export const GET = auth(async function GET(req) {
if (!req.auth && getEnv("SitePassword")) { if (!req.auth && getEnv("SitePassword")) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); redirect("/");
} }
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url);
@ -26,12 +21,26 @@ export const GET = auth(async function GET(req) {
{ status: 400 }, { status: 400 },
); );
} }
const response = (await GetServerMonitor({
server_id: parseInt(server_id), try {
})) as NezhaDataResponse; const serverIdNum = parseInt(server_id, 10);
if (response.error) { if (isNaN(serverIdNum)) {
console.log(response.error); return NextResponse.json(
return NextResponse.json({ error: response.error }, { status: 400 }); { error: "server_id must be a number" },
{ status: 400 },
);
}
const monitorData = await GetServerMonitor({
server_id: serverIdNum,
});
return NextResponse.json(monitorData, { status: 200 });
} catch (error) {
console.error("Error in GET handler:", error);
// @ts-ignore
const statusCode = error.statusCode || 500;
// @ts-ignore
const message = error.message || "Internal Server Error";
return NextResponse.json({ error: message }, { status: statusCode });
} }
return NextResponse.json(response, { status: 200 });
}); });

View File

@ -1,83 +1,58 @@
import { NezhaAPI, ServerApi } from "@/app/[locale]/types/nezha-api";
import { MakeOptional } from "@/app/[locale]/types/utils";
import { auth } from "@/auth"; import { auth } from "@/auth";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { GetNezhaData } from "@/lib/serverFetch";
import { redirect } from "next/navigation";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const runtime = "edge"; export const runtime = "edge";
interface NezhaDataResponse {
error?: string;
data?: ServerApi;
}
export const GET = auth(async function GET(req) { export const GET = auth(async function GET(req) {
if (!req.auth && getEnv("SitePassword")) { if (!req.auth && getEnv("SitePassword")) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); redirect("/");
} }
const response = (await GetNezhaData()) as NezhaDataResponse;
if (response.error) {
console.log(response.error);
return NextResponse.json({ error: response.error }, { status: 400 });
}
return NextResponse.json(response, { status: 200 });
});
async function GetNezhaData() {
var nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) {
console.log("NezhaBaseUrl is not set");
return;
}
// Remove trailing slash
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") {
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
}
try { try {
const response = await fetch(nezhaBaseUrl + "/api/v1/server/details", { const data = await GetNezhaData();
headers: { return NextResponse.json(data, { status: 200 });
Authorization: getEnv("NezhaAuth") as string,
},
next: {
revalidate: 0,
},
});
const nezhaData = (await response.json()).result as NezhaAPI[];
const data: ServerApi = {
live_servers: 0,
offline_servers: 0,
total_in_bandwidth: 0,
total_out_bandwidth: 0,
result: [],
};
const timestamp = Date.now() / 1000;
data.result = nezhaData.map(
(element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => {
if (timestamp - element.last_active > 300) {
data.offline_servers += 1;
element.online_status = false;
} else {
data.live_servers += 1;
element.online_status = true;
}
data.total_in_bandwidth += element.status.NetInTransfer;
data.total_out_bandwidth += element.status.NetOutTransfer;
delete element.ipv4;
delete element.ipv6;
delete element.valid_ip;
return element;
},
);
return data;
} catch (error) { } catch (error) {
return error; console.error("Error in GET handler:", error);
// @ts-ignore
const statusCode = error.statusCode || 500;
// @ts-ignore
const message = error.message || "Internal Server Error";
return NextResponse.json({ error: message }, { status: statusCode });
} }
} });
import { auth } from "@/auth";
import getEnv from "@/lib/env-entry";
import { GetNezhaData } from "@/lib/serverFetch";
import { redirect } from "next/navigation";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export const runtime = "edge";
export const GET = auth(async function GET(req) {
if (!req.auth && getEnv("SitePassword")) {
redirect("/");
}
try {
const data = await GetNezhaData();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error in GET handler:", error);
// @ts-ignore
const statusCode = error.statusCode || 500;
// @ts-ignore
const message = error.message || "Internal Server Error";
return NextResponse.json({ error: message }, { status: statusCode });
}
});

31
auth.ts
View File

@ -1,21 +1,32 @@
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import getEnv from "./lib/env-entry"; import getEnv from "./lib/env-entry";
export const { handlers, signIn, signOut, auth } = NextAuth({ export const { handlers, signIn, signOut, auth } = NextAuth({
secret: "H7Fijn9veJRkbizIwUQEpBAzzhRwkv7/ZoB5sGF5cwm5", secret: process.env.AUTH_SECRET ?? "this_is_nezha_dash_web_secret",
trustHost: (process.env.AUTH_TRUST_HOST as boolean | undefined) ?? true,
providers: [ providers: [
Credentials({ CredentialsProvider({
credentials: { type: "credentials",
password: {}, credentials: { password: { label: "Password", type: "password" } },
}, // authorization function
authorize: async (credentials) => { async authorize(credentials) {
if (credentials.password === getEnv("SitePassword")) { const { password } = credentials;
return { id: "0" }; if (password === getEnv("SitePassword")) {
return { id: "nezha-dash-auth" };
} }
return null; return { error: "Invalid password" };
}, },
}), }),
], ],
callbacks: {
async signIn({ user }) {
// @ts-ignore
if (user.error) {
return false;
}
return true;
},
},
}); });

View File

@ -1,8 +1,15 @@
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { cn } from "@/lib/utils";
import getUnicodeFlagIcon from "country-flag-icons/unicode"; import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function ServerFlag({ country_code }: { country_code: string }) { export default function ServerFlag({
country_code,
className,
}: {
country_code: string;
className?: string;
}) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false); const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
const useSvgFlag = getEnv("NEXT_PUBLIC_ForceUseSvgFlag") === "true"; const useSvgFlag = getEnv("NEXT_PUBLIC_ForceUseSvgFlag") === "true";
@ -38,9 +45,9 @@ export default function ServerFlag({ country_code }: { country_code: string }) {
} }
return ( return (
<span className="text-[12px] text-muted-foreground"> <span className={cn("text-[12px] text-muted-foreground", className)}>
{useSvgFlag || !supportsEmojiFlags ? ( {useSvgFlag || !supportsEmojiFlags ? (
<span className={`fi fi-${country_code}`}></span> <span className={`fi fi-${country_code}`} />
) : ( ) : (
getUnicodeFlagIcon(country_code) getUnicodeFlagIcon(country_code)
)} )}

85
components/SignIn.tsx Normal file
View File

@ -0,0 +1,85 @@
"use client";
import { getCsrfToken, signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Loader } from "./loading/Loader";
export function SignIn() {
const t = useTranslations("SignIn");
const [csrfToken, setCsrfToken] = useState("");
const [loading, setLoading] = useState(false);
const [errorState, setErrorState] = useState(false);
const [successState, setSuccessState] = useState(false);
const router = useRouter();
useEffect(() => {
async function loadProviders() {
const csrf = await getCsrfToken();
setCsrfToken(csrf);
}
loadProviders();
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const formData = new FormData(e.currentTarget);
const password = formData.get("password") as string;
const res = await signIn("credentials", {
password: password,
redirect: false,
});
if (res?.error) {
console.log("login error");
setErrorState(true);
setSuccessState(false);
} else {
console.log("login success");
setErrorState(false);
setSuccessState(true);
router.push("/");
router.refresh();
}
setLoading(false);
};
return (
<form
className="flex flex-col items-center justify-start gap-4 p-4 "
onSubmit={handleSubmit}
>
<input type="hidden" name="csrfToken" value={csrfToken} />
<section className="flex flex-col items-start gap-2">
<label className="flex flex-col items-start gap-1 ">
{errorState && (
<p className="text-red-500 text-sm font-semibold">
{t("ErrorMessage")}
</p>
)}
{successState && (
<p className="text-green-500 text-sm font-semibold">
{t("SuccessMessage")}
</p>
)}
<p className="text-base font-semibold">{t("SignInMessage")}</p>
<input
className="px-1 border-[1px] rounded-[5px]"
name="password"
type="password"
/>
</label>
<button
className="px-1.5 py-0.5 w-fit flex items-center gap-1 text-sm font-semibold rounded-[8px] border bg-card hover:brightness-95 transition-all text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none"
disabled={loading}
>
{t("Submit")}
{loading && <Loader visible={true} />}
</button>
</section>
</form>
);
}

View File

@ -2,24 +2,68 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import React from "react"; import React, { createRef, useEffect, useRef, useState } from "react";
export default function Switch({ export default function Switch({
allTag, allTag,
nowTag, nowTag,
setTag, onTagChange,
}: { }: {
allTag: string[]; allTag: string[];
nowTag: string; nowTag: string;
setTag: (tag: string) => void; onTagChange: (tag: string) => void;
}) { }) {
const scrollRef = useRef<HTMLDivElement>(null);
const tagRefs = useRef(allTag.map(() => createRef<HTMLDivElement>()));
useEffect(() => {
const savedTag = sessionStorage.getItem("selectedTag");
if (savedTag && allTag.includes(savedTag)) {
onTagChange(savedTag);
}
}, [allTag]);
useEffect(() => {
const container = scrollRef.current;
if (!container) return;
const isOverflowing = container.scrollWidth > container.clientWidth;
if (!isOverflowing) return;
const onWheel = (e: WheelEvent) => {
e.preventDefault();
container.scrollLeft += e.deltaY;
};
container.addEventListener("wheel", onWheel, { passive: false });
return () => {
container.removeEventListener("wheel", onWheel);
};
}, []);
useEffect(() => {
const currentTagRef = tagRefs.current[allTag.indexOf(nowTag)];
if (currentTagRef && currentTagRef.current) {
currentTagRef.current.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
}, [nowTag]);
return ( return (
<div className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"> <div
ref={scrollRef}
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
>
<div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800"> <div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
{allTag.map((tag) => ( {allTag.map((tag, index) => (
<div <div
key={tag} key={tag}
onClick={() => setTag(tag)} ref={tagRefs.current[index]}
onClick={() => onTagChange(tag)}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
nowTag === tag nowTag === tag

View File

@ -14,7 +14,7 @@ export default function TabSwitch({
setCurrentTab: (tab: string) => void; setCurrentTab: (tab: string) => void;
}) { }) {
return ( return (
<div className="z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"> <div className="z-50 flex flex-col items-start rounded-[50px]">
<div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800"> <div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
{tabs.map((tab: string) => ( {tabs.map((tab: string) => (
<div <div

View File

@ -8,18 +8,17 @@ import { unstable_noStore as noStore } from "next/cache";
export async function GetNezhaData() { export async function GetNezhaData() {
noStore(); noStore();
var nezhaBaseUrl = getEnv("NezhaBaseUrl"); let nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.log("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set");
return { error: "NezhaBaseUrl is not set" }; throw new Error("NezhaBaseUrl is not set");
} }
// Remove trailing slash // Remove trailing slash
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") { nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "");
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
}
try { try {
const response = await fetch(nezhaBaseUrl + "/api/v1/server/details", { const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, {
headers: { headers: {
Authorization: getEnv("NezhaAuth") as string, Authorization: getEnv("NezhaAuth") as string,
}, },
@ -27,12 +26,19 @@ export async function GetNezhaData() {
revalidate: 0, revalidate: 0,
}, },
}); });
const resData = await response.json();
const nezhaData = resData.result as NezhaAPI[]; if (!response.ok) {
if (!nezhaData) { const errorText = await response.text();
console.log(resData); throw new Error(`Failed to fetch data: ${response.status} ${errorText}`);
return { error: "NezhaData fetch failed" };
} }
const resData = await response.json();
if (!resData.result) {
throw new Error("NezhaData fetch failed: 'result' field is missing");
}
const nezhaData = resData.result as NezhaAPI[];
const data: ServerApi = { const data: ServerApi = {
live_servers: 0, live_servers: 0,
offline_servers: 0, offline_servers: 0,
@ -41,30 +47,27 @@ export async function GetNezhaData() {
result: [], result: [],
}; };
var forceShowAllServers = getEnv("ForceShowAllServers"); const forceShowAllServers = getEnv("ForceShowAllServers") === "true";
let nezhaDataFiltered: NezhaAPI[]; const nezhaDataFiltered = forceShowAllServers
if (forceShowAllServers === "true") { ? nezhaData
nezhaDataFiltered = nezhaData; : nezhaData.filter((element) => !element.hide_for_guest);
} else {
// remove hidden servers
nezhaDataFiltered = nezhaData.filter(
(element) => !element.hide_for_guest,
);
}
const timestamp = Date.now() / 1000; const timestamp = Date.now() / 1000;
data.result = nezhaDataFiltered.map( data.result = nezhaDataFiltered.map(
(element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => { (element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => {
if (timestamp - element.last_active > 300) { const isOnline = timestamp - element.last_active <= 300;
data.offline_servers += 1; element.online_status = isOnline;
element.online_status = false;
} else { if (isOnline) {
data.live_servers += 1; data.live_servers += 1;
element.online_status = true; } else {
data.offline_servers += 1;
} }
data.total_out_bandwidth += element.status.NetOutTransfer; data.total_out_bandwidth += element.status.NetOutTransfer;
data.total_in_bandwidth += element.status.NetInTransfer; data.total_in_bandwidth += element.status.NetInTransfer;
// Remove unwanted properties
delete element.ipv4; delete element.ipv4;
delete element.ipv6; delete element.ipv6;
delete element.valid_ip; delete element.valid_ip;
@ -75,25 +78,24 @@ export async function GetNezhaData() {
return data; return data;
} catch (error) { } catch (error) {
return error; console.error("GetNezhaData error:", error);
throw error; // Rethrow the error to be caught by the caller
} }
} }
export async function GetServerMonitor({ server_id }: { server_id: number }) { export async function GetServerMonitor({ server_id }: { server_id: number }) {
var nezhaBaseUrl = getEnv("NezhaBaseUrl"); let nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.log("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set");
return { error: "NezhaBaseUrl is not set" }; throw new Error("NezhaBaseUrl is not set");
} }
// Remove trailing slash // Remove trailing slash
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") { nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "");
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
}
try { try {
const response = await fetch( const response = await fetch(
nezhaBaseUrl + `/api/v1/monitor/${server_id}`, `${nezhaBaseUrl}/api/v1/monitor/${server_id}`,
{ {
headers: { headers: {
Authorization: getEnv("NezhaAuth") as string, Authorization: getEnv("NezhaAuth") as string,
@ -103,33 +105,40 @@ export async function GetServerMonitor({ server_id }: { server_id: number }) {
}, },
}, },
); );
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`);
}
const resData = await response.json(); const resData = await response.json();
const monitorData = resData.result; const monitorData = resData.result;
if (!monitorData) { if (!monitorData) {
console.log(resData); console.error("MonitorData fetch failed:", resData);
return { error: "MonitorData fetch failed" }; throw new Error("MonitorData fetch failed: 'result' field is missing");
} }
return monitorData; return monitorData;
} catch (error) { } catch (error) {
return error; console.error("GetServerMonitor error:", error);
throw error;
} }
} }
export async function GetServerDetail({ server_id }: { server_id: number }) { export async function GetServerDetail({ server_id }: { server_id: number }) {
var nezhaBaseUrl = getEnv("NezhaBaseUrl"); let nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.log("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set");
return { error: "NezhaBaseUrl is not set" }; throw new Error("NezhaBaseUrl is not set");
} }
// Remove trailing slash // Remove trailing slash
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") { nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "");
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
}
try { try {
const response = await fetch( const response = await fetch(
nezhaBaseUrl + `/api/v1/server/details?id=${server_id}`, `${nezhaBaseUrl}/api/v1/server/details?id=${server_id}`,
{ {
headers: { headers: {
Authorization: getEnv("NezhaAuth") as string, Authorization: getEnv("NezhaAuth") as string,
@ -139,30 +148,38 @@ export async function GetServerDetail({ server_id }: { server_id: number }) {
}, },
}, },
); );
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`);
}
const resData = await response.json(); const resData = await response.json();
const detailDataList = resData.result; const detailDataList = resData.result;
if (!detailDataList) {
console.log(resData); if (
return { error: "MonitorData fetch failed" }; !detailDataList ||
!Array.isArray(detailDataList) ||
detailDataList.length === 0
) {
console.error("MonitorData fetch failed:", resData);
throw new Error(
"MonitorData fetch failed: 'result' field is missing or empty",
);
} }
const timestamp = Date.now() / 1000; const timestamp = Date.now() / 1000;
const detailData = detailDataList.map( const detailData = detailDataList.map((element) => {
(element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => { element.online_status = timestamp - element.last_active <= 300;
if (timestamp - element.last_active > 300) { delete element.ipv4;
element.online_status = false; delete element.ipv6;
} else { delete element.valid_ip;
element.online_status = true; return element;
} })[0];
delete element.ipv4;
delete element.ipv6;
delete element.valid_ip;
return element;
},
)[0];
return detailData; return detailData;
} catch (error) { } catch (error) {
return error; console.error("GetServerDetail error:", error);
throw error; // Rethrow the error to be handled by the caller
} }
} }

View File

@ -71,19 +71,20 @@ export const fetcher = (url: string) =>
throw err; throw err;
}); });
export const nezhaFetcher = (url: string) => export const nezhaFetcher = async (url: string) => {
fetch(url) const res = await fetch(url);
.then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error(res.statusText); const error = new Error("An error occurred while fetching the data.");
} // @ts-ignore
return res.json(); error.info = await res.json();
}) // @ts-ignore
.then((data) => data) error.status = res.status;
.catch((err) => { throw error;
console.error(err); }
throw err;
}); return res.json();
};
export function formatRelativeTime(timestamp: number): string { export function formatRelativeTime(timestamp: number): string {
const now = Date.now(); const now = Date.now();

View File

@ -23,6 +23,12 @@
"Detail": "Detail", "Detail": "Detail",
"Network": "Network" "Network": "Network"
}, },
"SignIn": {
"SignInMessage": "Please enter the password",
"Submit": "Login",
"ErrorMessage": "Invalid password",
"SuccessMessage": "Login successful"
},
"ServerCardPopover": { "ServerCardPopover": {
"System": "System", "System": "System",
"CPU": "CPU", "CPU": "CPU",
@ -52,6 +58,7 @@
"Arch": "Arch", "Arch": "Arch",
"Mem": "Mem", "Mem": "Mem",
"Disk": "Disk", "Disk": "Disk",
"Region": "Region",
"System": "System", "System": "System",
"CPU": "CPU" "CPU": "CPU"
}, },

View File

@ -23,6 +23,12 @@
"Detail": "詳細", "Detail": "詳細",
"Network": "ネットワーク" "Network": "ネットワーク"
}, },
"SignIn": {
"SignInMessage": "パスワードを入力してください",
"Submit": "ログイン",
"ErrorMessage": "パスワードが間違っています",
"SuccessMessage": "ログイン成功"
},
"ServerCardPopover": { "ServerCardPopover": {
"System": "システム", "System": "システム",
"CPU": "CPU", "CPU": "CPU",
@ -52,6 +58,7 @@
"Arch": "アーキテクチャ", "Arch": "アーキテクチャ",
"Mem": "メモリ", "Mem": "メモリ",
"Disk": "ディスク", "Disk": "ディスク",
"Region": "地域",
"System": "システム", "System": "システム",
"CPU": "CPU" "CPU": "CPU"
}, },

View File

@ -23,6 +23,12 @@
"Detail": "詳細", "Detail": "詳細",
"Network": "網路" "Network": "網路"
}, },
"SignIn": {
"SignInMessage": "請輸入密碼",
"Submit": "登入",
"ErrorMessage": "密碼錯誤",
"SuccessMessage": "登入成功"
},
"ServerCardPopover": { "ServerCardPopover": {
"System": "系統", "System": "系統",
"CPU": "CPU", "CPU": "CPU",
@ -52,6 +58,7 @@
"Arch": "架構", "Arch": "架構",
"Mem": "記憶體", "Mem": "記憶體",
"Disk": "磁碟", "Disk": "磁碟",
"Region": "地區",
"System": "系統", "System": "系統",
"CPU": "CPU" "CPU": "CPU"
}, },

View File

@ -23,6 +23,12 @@
"Detail": "详情", "Detail": "详情",
"Network": "网络" "Network": "网络"
}, },
"SignIn": {
"SignInMessage": "请输入密码",
"Submit": "登录",
"ErrorMessage": "密码错误",
"SuccessMessage": "登录成功"
},
"ServerCardPopover": { "ServerCardPopover": {
"System": "系统", "System": "系统",
"CPU": "CPU", "CPU": "CPU",
@ -52,6 +58,7 @@
"Arch": "架构", "Arch": "架构",
"Mem": "内存", "Mem": "内存",
"Disk": "磁盘", "Disk": "磁盘",
"Region": "地区",
"System": "系统", "System": "系统",
"CPU": "CPU" "CPU": "CPU"
}, },

View File

@ -23,11 +23,6 @@ const withPWA = withPWAInit({
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
reactStrictMode: true, reactStrictMode: true,
experimental: {
serverActions: {
allowedOrigins: ["*"],
},
},
logging: { logging: {
fetches: { fetches: {
fullUrl: true, fullUrl: true,

View File

@ -1,6 +1,6 @@
{ {
"name": "nezha-dash", "name": "nezha-dash",
"version": "0.1.0", "version": "0.8.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3020", "dev": "next dev -p 3020",

1638
styles/flag-icons.min.css vendored Normal file

File diff suppressed because it is too large Load Diff