mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
Merge branch 'cloudflare' into cloudflare-dev
This commit is contained in:
commit
51e66ef2f1
36
.github/workflows/Deploy.yml
vendored
36
.github/workflows/Deploy.yml
vendored
@ -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 }}
|
|
||||||
|
@ -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
|
||||||
|
10
README.md
10
README.md
@ -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 |
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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} />
|
||||||
))}
|
))}
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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")}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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 });
|
|
||||||
});
|
});
|
||||||
|
@ -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 });
|
|
||||||
});
|
});
|
||||||
|
@ -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
31
auth.ts
@ -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;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -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
85
components/SignIn.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
lib/utils.ts
27
lib/utils.ts
@ -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();
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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
1638
styles/flag-icons.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user