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
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
@ -50,30 +49,19 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
changelog:
|
||||
name: Generate Changelog
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
outputs:
|
||||
release_body: ${{ steps.git-cliff.outputs.content }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
id: git-cliff
|
||||
|
||||
- name: Set node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
config: git-cliff-config/cliff.toml
|
||||
args: -vv --latest --strip 'footer'
|
||||
registry-url: https://registry.npmjs.org/
|
||||
node-version: lts/*
|
||||
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
OUTPUT: CHANGES.md
|
||||
- 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 }}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1 AS base
|
||||
FROM --platform=$BUILDPLATFORM oven/bun:1 AS base
|
||||
|
||||
# Stage 1: Install dependencies
|
||||
FROM base AS deps
|
||||
|
@ -6,9 +6,9 @@
|
||||
</div>
|
||||
|
||||
| 一键部署到 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) |
|
||||
| [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 |
|
||||
| NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi |
|
||||
| SitePassword | 页面密码 | 123456 |
|
||||
| SitePassword | 页面密码 | **默认**:无密码 |
|
||||
| DefaultLocale | 面板默认显示语言 | **默认**:en [简中:zh 繁中:zh-t 英语:en 日语:ja] |
|
||||
| ForceShowAllServers | 是否强制显示所有服务器 | **默认**:false |
|
||||
| NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**:2000 |
|
||||
|
@ -51,7 +51,7 @@ export function NetworkChartClient({
|
||||
{
|
||||
refreshInterval:
|
||||
Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000,
|
||||
isPaused: () => !show,
|
||||
isVisible: () => show,
|
||||
},
|
||||
);
|
||||
|
||||
@ -181,13 +181,7 @@ export const NetworkChart = React.memo(function NetworkChart({
|
||||
<Card>
|
||||
<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">
|
||||
<CardTitle
|
||||
onClick={() => {
|
||||
router.push(`/${locale}/`);
|
||||
}}
|
||||
className="flex flex-none cursor-pointer items-center gap-0.5 text-xl"
|
||||
>
|
||||
<BackIcon />
|
||||
<CardTitle className="flex flex-none items-center gap-0.5 text-md">
|
||||
{serverName}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
|
@ -12,13 +12,7 @@ export default function NetworkChartLoading() {
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
||||
<CardTitle
|
||||
onClick={() => {
|
||||
router.push(`/${locale}/`);
|
||||
}}
|
||||
className="flex items-center cursor-pointer gap-0.5 text-xl"
|
||||
>
|
||||
<BackIcon />
|
||||
<CardTitle className="flex items-center gap-0.5 text-xl">
|
||||
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
|
||||
</CardTitle>
|
||||
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { Card, CardContent } from "@/components/ui/card";
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||
@ -19,6 +19,7 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import useSWR from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
type cpuChartData = {
|
||||
timeStamp: string;
|
||||
@ -62,12 +63,23 @@ export default function 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>(
|
||||
`/api/detail?server_id=${server_id}`,
|
||||
nezhaFetcher,
|
||||
{
|
||||
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;
|
||||
|
||||
return (
|
||||
<Card className=" rounded-sm">
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -231,7 +243,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<Card className=" rounded-sm">
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -324,7 +336,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<Card className=" rounded-sm">
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
@ -445,7 +457,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<Card className="rounded-sm">
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -557,7 +569,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<Card className=" rounded-sm">
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
@ -677,7 +689,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<Card className="rounded-sm">
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
|
@ -1,15 +1,18 @@
|
||||
"use client";
|
||||
|
||||
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 ServerFlag from "@/components/ServerFlag";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import getEnv from "@/lib/env-entry";
|
||||
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
export default function ServerDetailClient({
|
||||
server_id,
|
||||
@ -19,11 +22,47 @@ export default function ServerDetailClient({
|
||||
const t = useTranslations("ServerDetailClient");
|
||||
const router = useRouter();
|
||||
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>(
|
||||
`/api/detail?server_id=${server_id}`,
|
||||
nezhaFetcher,
|
||||
{
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
|
||||
fallbackData,
|
||||
revalidateOnMount: false,
|
||||
revalidateIfStale: false,
|
||||
},
|
||||
);
|
||||
|
||||
@ -45,9 +84,7 @@ export default function ServerDetailClient({
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onClick={() => {
|
||||
router.push(`/${locale}/`);
|
||||
}}
|
||||
onClick={linkClick}
|
||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||
>
|
||||
<BackIcon />
|
||||
@ -60,7 +97,7 @@ export default function ServerDetailClient({
|
||||
<p className="text-xs text-muted-foreground">{t("status")}</p>
|
||||
<Badge
|
||||
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-red-600": !data?.online_status,
|
||||
@ -115,6 +152,22 @@ export default function ServerDetailClient({
|
||||
</section>
|
||||
</CardContent>
|
||||
</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 className="flex flex-wrap gap-2 mt-1">
|
||||
<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 { nezhaFetcher } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function 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, {
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
|
||||
});
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
if (!data?.result) return null;
|
||||
|
||||
if (!data?.result || !isMounted) return null;
|
||||
|
||||
const { result } = data;
|
||||
|
||||
const sortedServers = result.sort((a, b) => {
|
||||
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0);
|
||||
if (displayIndexDiff !== 0) return displayIndexDiff;
|
||||
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)];
|
||||
|
||||
uniqueTags.unshift(t("defaultTag"));
|
||||
uniqueTags.unshift(defaultTag);
|
||||
|
||||
const filteredServers =
|
||||
tag === t("defaultTag")
|
||||
tag === defaultTag
|
||||
? sortedServers
|
||||
: sortedServers.filter((server) => server.tag === tag);
|
||||
|
||||
return (
|
||||
<>
|
||||
{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) => (
|
||||
<ServerCard key={serverInfo.id} serverInfo={serverInfo} />
|
||||
))}
|
||||
|
@ -18,13 +18,16 @@ export default function ServerOverviewClient() {
|
||||
);
|
||||
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true";
|
||||
|
||||
if (error)
|
||||
if (error) {
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -29,12 +29,18 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
</div>
|
||||
<Separator className="flex-1" />
|
||||
</section>
|
||||
{currentTab === tabs[0] && (
|
||||
<ServerDetailChartClient server_id={Number(params.id)} show={true} />
|
||||
)}
|
||||
{currentTab === tabs[1] && (
|
||||
<NetworkChartClient server_id={Number(params.id)} show={true} />
|
||||
)}
|
||||
<div style={{ display: currentTab === tabs[0] ? "block" : "none" }}>
|
||||
<ServerDetailChartClient
|
||||
server_id={Number(params.id)}
|
||||
show={currentTab === tabs[0]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
|
||||
<NetworkChartClient
|
||||
server_id={Number(params.id)}
|
||||
show={currentTab === tabs[1]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import pack from "@/package.json";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Footer() {
|
||||
const t = useTranslations("Footer");
|
||||
const version = pack.version;
|
||||
return (
|
||||
<footer className="mx-auto w-full max-w-5xl">
|
||||
<section className="flex flex-col">
|
||||
@ -14,6 +16,13 @@ export default function Footer() {
|
||||
>
|
||||
{t("a_303-585_GitHub")}
|
||||
</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>
|
||||
<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")}
|
||||
|
@ -26,6 +26,7 @@ function Header() {
|
||||
<section className="flex items-center justify-between">
|
||||
<section
|
||||
onClick={() => {
|
||||
sessionStorage.removeItem("selectedTag");
|
||||
router.push(`/${locale}/`);
|
||||
}}
|
||||
className="flex cursor-pointer items-center text-base font-medium"
|
||||
@ -98,7 +99,9 @@ function Overview() {
|
||||
</p>
|
||||
{mouted ? (
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
|
@ -1,9 +1,8 @@
|
||||
import Footer from "@/app/[locale]/(main)/footer";
|
||||
import Header from "@/app/[locale]/(main)/header";
|
||||
import { auth } from "@/auth";
|
||||
import { SignIn } from "@/components/sign-in";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
import getEnv from "@/lib/env-entry";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
type DashboardProps = {
|
||||
@ -12,19 +11,12 @@ type DashboardProps = {
|
||||
export default async function MainLayout({ children }: DashboardProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session && getEnv("SitePassword")) {
|
||||
// if (getEnv("CF_PAGES")) {
|
||||
// redirect("/api/auth/signin");
|
||||
// } else {
|
||||
return <SignIn />;
|
||||
// }
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Header />
|
||||
{children}
|
||||
{!session && getEnv("SitePassword") ? <SignIn /> : children}
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
// @auto-i18n-check. Please do not delete the line.
|
||||
import { auth } from "@/auth";
|
||||
import { locales } from "@/i18n-metadata";
|
||||
import getEnv from "@/lib/env-entry";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -13,8 +12,6 @@ import { ThemeProvider } from "next-themes";
|
||||
import { Inter as FontSans } from "next/font/google";
|
||||
import React from "react";
|
||||
|
||||
import "/node_modules/flag-icons/css/flag-icons.min.css";
|
||||
|
||||
const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
@ -65,6 +62,10 @@ export default function LocaleLayout({
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
<PublicEnvScript />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
|
@ -2,4 +2,4 @@ import { handlers } from "@/auth" // Referring to the auth.ts we just created
|
||||
|
||||
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 getEnv from "@/lib/env-entry";
|
||||
import { GetServerDetail } from "@/lib/serverFetch";
|
||||
import { redirect } from "next/navigation";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface NezhaDataResponse {
|
||||
error?: string;
|
||||
data?: NezhaAPISafe;
|
||||
}
|
||||
|
||||
export const GET = auth(async function GET(req) {
|
||||
if (!req.auth && getEnv("SitePassword")) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const server_id = searchParams.get("server_id");
|
||||
|
||||
if (!server_id) {
|
||||
return NextResponse.json(
|
||||
{ error: "server_id is required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const response = (await GetServerDetail({
|
||||
server_id: parseInt(server_id),
|
||||
})) as NezhaDataResponse;
|
||||
if (response.error) {
|
||||
console.log(response.error);
|
||||
return NextResponse.json({ error: response.error }, { status: 400 });
|
||||
|
||||
try {
|
||||
const serverIdNum = parseInt(server_id, 10);
|
||||
if (isNaN(serverIdNum)) {
|
||||
return NextResponse.json(
|
||||
{ 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 getEnv from "@/lib/env-entry";
|
||||
import { GetServerMonitor } from "@/lib/serverFetch";
|
||||
import { redirect } from "next/navigation";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface NezhaDataResponse {
|
||||
error?: string;
|
||||
data?: ServerMonitorChart;
|
||||
}
|
||||
|
||||
export const GET = auth(async function GET(req) {
|
||||
if (!req.auth && getEnv("SitePassword")) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
@ -26,12 +21,26 @@ export const GET = auth(async function GET(req) {
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const response = (await GetServerMonitor({
|
||||
server_id: parseInt(server_id),
|
||||
})) as NezhaDataResponse;
|
||||
if (response.error) {
|
||||
console.log(response.error);
|
||||
return NextResponse.json({ error: response.error }, { status: 400 });
|
||||
|
||||
try {
|
||||
const serverIdNum = parseInt(server_id, 10);
|
||||
if (isNaN(serverIdNum)) {
|
||||
return NextResponse.json(
|
||||
{ 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 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";
|
||||
|
||||
interface NezhaDataResponse {
|
||||
error?: string;
|
||||
data?: ServerApi;
|
||||
}
|
||||
|
||||
|
||||
export const GET = auth(async function GET(req) {
|
||||
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 {
|
||||
const response = await fetch(nezhaBaseUrl + "/api/v1/server/details", {
|
||||
headers: {
|
||||
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;
|
||||
const data = await GetNezhaData();
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} 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 Credentials from "next-auth/providers/credentials";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
||||
import getEnv from "./lib/env-entry";
|
||||
|
||||
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: [
|
||||
Credentials({
|
||||
credentials: {
|
||||
password: {},
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
if (credentials.password === getEnv("SitePassword")) {
|
||||
return { id: "0" };
|
||||
CredentialsProvider({
|
||||
type: "credentials",
|
||||
credentials: { password: { label: "Password", type: "password" } },
|
||||
// authorization function
|
||||
async authorize(credentials) {
|
||||
const { password } = credentials;
|
||||
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 { cn } from "@/lib/utils";
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||
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 useSvgFlag = getEnv("NEXT_PUBLIC_ForceUseSvgFlag") === "true";
|
||||
@ -38,9 +45,9 @@ export default function ServerFlag({ country_code }: { country_code: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="text-[12px] text-muted-foreground">
|
||||
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
||||
{useSvgFlag || !supportsEmojiFlags ? (
|
||||
<span className={`fi fi-${country_code}`}></span>
|
||||
<span className={`fi fi-${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 { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
import React, { createRef, useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function Switch({
|
||||
allTag,
|
||||
nowTag,
|
||||
setTag,
|
||||
onTagChange,
|
||||
}: {
|
||||
allTag: 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 (
|
||||
<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">
|
||||
{allTag.map((tag) => (
|
||||
{allTag.map((tag, index) => (
|
||||
<div
|
||||
key={tag}
|
||||
onClick={() => setTag(tag)}
|
||||
ref={tagRefs.current[index]}
|
||||
onClick={() => onTagChange(tag)}
|
||||
className={cn(
|
||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
||||
nowTag === tag
|
||||
|
@ -14,7 +14,7 @@ export default function TabSwitch({
|
||||
setCurrentTab: (tab: string) => void;
|
||||
}) {
|
||||
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">
|
||||
{tabs.map((tab: string) => (
|
||||
<div
|
||||
|
@ -8,18 +8,17 @@ import { unstable_noStore as noStore } from "next/cache";
|
||||
export async function GetNezhaData() {
|
||||
noStore();
|
||||
|
||||
var nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
if (!nezhaBaseUrl) {
|
||||
console.log("NezhaBaseUrl is not set");
|
||||
return { error: "NezhaBaseUrl is not set" };
|
||||
console.error("NezhaBaseUrl is not set");
|
||||
throw new Error("NezhaBaseUrl is not set");
|
||||
}
|
||||
|
||||
// Remove trailing slash
|
||||
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") {
|
||||
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
|
||||
}
|
||||
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "");
|
||||
|
||||
try {
|
||||
const response = await fetch(nezhaBaseUrl + "/api/v1/server/details", {
|
||||
const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, {
|
||||
headers: {
|
||||
Authorization: getEnv("NezhaAuth") as string,
|
||||
},
|
||||
@ -27,12 +26,19 @@ export async function GetNezhaData() {
|
||||
revalidate: 0,
|
||||
},
|
||||
});
|
||||
const resData = await response.json();
|
||||
const nezhaData = resData.result as NezhaAPI[];
|
||||
if (!nezhaData) {
|
||||
console.log(resData);
|
||||
return { error: "NezhaData fetch failed" };
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
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 = {
|
||||
live_servers: 0,
|
||||
offline_servers: 0,
|
||||
@ -41,30 +47,27 @@ export async function GetNezhaData() {
|
||||
result: [],
|
||||
};
|
||||
|
||||
var forceShowAllServers = getEnv("ForceShowAllServers");
|
||||
let nezhaDataFiltered: NezhaAPI[];
|
||||
if (forceShowAllServers === "true") {
|
||||
nezhaDataFiltered = nezhaData;
|
||||
} else {
|
||||
// remove hidden servers
|
||||
nezhaDataFiltered = nezhaData.filter(
|
||||
(element) => !element.hide_for_guest,
|
||||
);
|
||||
}
|
||||
const forceShowAllServers = getEnv("ForceShowAllServers") === "true";
|
||||
const nezhaDataFiltered = forceShowAllServers
|
||||
? nezhaData
|
||||
: nezhaData.filter((element) => !element.hide_for_guest);
|
||||
|
||||
const timestamp = Date.now() / 1000;
|
||||
data.result = nezhaDataFiltered.map(
|
||||
(element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => {
|
||||
if (timestamp - element.last_active > 300) {
|
||||
data.offline_servers += 1;
|
||||
element.online_status = false;
|
||||
} else {
|
||||
const isOnline = timestamp - element.last_active <= 300;
|
||||
element.online_status = isOnline;
|
||||
|
||||
if (isOnline) {
|
||||
data.live_servers += 1;
|
||||
element.online_status = true;
|
||||
} else {
|
||||
data.offline_servers += 1;
|
||||
}
|
||||
|
||||
data.total_out_bandwidth += element.status.NetOutTransfer;
|
||||
data.total_in_bandwidth += element.status.NetInTransfer;
|
||||
|
||||
// Remove unwanted properties
|
||||
delete element.ipv4;
|
||||
delete element.ipv6;
|
||||
delete element.valid_ip;
|
||||
@ -75,25 +78,24 @@ export async function GetNezhaData() {
|
||||
|
||||
return data;
|
||||
} 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 }) {
|
||||
var nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
if (!nezhaBaseUrl) {
|
||||
console.log("NezhaBaseUrl is not set");
|
||||
return { error: "NezhaBaseUrl is not set" };
|
||||
console.error("NezhaBaseUrl is not set");
|
||||
throw new Error("NezhaBaseUrl is not set");
|
||||
}
|
||||
|
||||
// Remove trailing slash
|
||||
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") {
|
||||
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
|
||||
}
|
||||
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "");
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
nezhaBaseUrl + `/api/v1/monitor/${server_id}`,
|
||||
`${nezhaBaseUrl}/api/v1/monitor/${server_id}`,
|
||||
{
|
||||
headers: {
|
||||
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 monitorData = resData.result;
|
||||
|
||||
if (!monitorData) {
|
||||
console.log(resData);
|
||||
return { error: "MonitorData fetch failed" };
|
||||
console.error("MonitorData fetch failed:", resData);
|
||||
throw new Error("MonitorData fetch failed: 'result' field is missing");
|
||||
}
|
||||
|
||||
return monitorData;
|
||||
} catch (error) {
|
||||
return error;
|
||||
console.error("GetServerMonitor error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GetServerDetail({ server_id }: { server_id: number }) {
|
||||
var nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
if (!nezhaBaseUrl) {
|
||||
console.log("NezhaBaseUrl is not set");
|
||||
return { error: "NezhaBaseUrl is not set" };
|
||||
console.error("NezhaBaseUrl is not set");
|
||||
throw new Error("NezhaBaseUrl is not set");
|
||||
}
|
||||
|
||||
// Remove trailing slash
|
||||
if (nezhaBaseUrl[nezhaBaseUrl.length - 1] === "/") {
|
||||
nezhaBaseUrl = nezhaBaseUrl.slice(0, -1);
|
||||
}
|
||||
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "");
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
nezhaBaseUrl + `/api/v1/server/details?id=${server_id}`,
|
||||
`${nezhaBaseUrl}/api/v1/server/details?id=${server_id}`,
|
||||
{
|
||||
headers: {
|
||||
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 detailDataList = resData.result;
|
||||
if (!detailDataList) {
|
||||
console.log(resData);
|
||||
return { error: "MonitorData fetch failed" };
|
||||
|
||||
if (
|
||||
!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 detailData = detailDataList.map(
|
||||
(element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => {
|
||||
if (timestamp - element.last_active > 300) {
|
||||
element.online_status = false;
|
||||
} else {
|
||||
element.online_status = true;
|
||||
}
|
||||
const detailData = detailDataList.map((element) => {
|
||||
element.online_status = timestamp - element.last_active <= 300;
|
||||
delete element.ipv4;
|
||||
delete element.ipv6;
|
||||
delete element.valid_ip;
|
||||
return element;
|
||||
},
|
||||
)[0];
|
||||
})[0];
|
||||
|
||||
return detailData;
|
||||
} catch (error) {
|
||||
return error;
|
||||
console.error("GetServerDetail error:", error);
|
||||
throw error; // Rethrow the error to be handled by the caller
|
||||
}
|
||||
}
|
||||
|
21
lib/utils.ts
21
lib/utils.ts
@ -71,19 +71,20 @@ export const fetcher = (url: string) =>
|
||||
throw err;
|
||||
});
|
||||
|
||||
export const nezhaFetcher = (url: string) =>
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
export const nezhaFetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
const error = new Error("An error occurred while fetching the data.");
|
||||
// @ts-ignore
|
||||
error.info = await res.json();
|
||||
// @ts-ignore
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => data)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
export function formatRelativeTime(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
|
@ -23,6 +23,12 @@
|
||||
"Detail": "Detail",
|
||||
"Network": "Network"
|
||||
},
|
||||
"SignIn": {
|
||||
"SignInMessage": "Please enter the password",
|
||||
"Submit": "Login",
|
||||
"ErrorMessage": "Invalid password",
|
||||
"SuccessMessage": "Login successful"
|
||||
},
|
||||
"ServerCardPopover": {
|
||||
"System": "System",
|
||||
"CPU": "CPU",
|
||||
@ -52,6 +58,7 @@
|
||||
"Arch": "Arch",
|
||||
"Mem": "Mem",
|
||||
"Disk": "Disk",
|
||||
"Region": "Region",
|
||||
"System": "System",
|
||||
"CPU": "CPU"
|
||||
},
|
||||
|
@ -23,6 +23,12 @@
|
||||
"Detail": "詳細",
|
||||
"Network": "ネットワーク"
|
||||
},
|
||||
"SignIn": {
|
||||
"SignInMessage": "パスワードを入力してください",
|
||||
"Submit": "ログイン",
|
||||
"ErrorMessage": "パスワードが間違っています",
|
||||
"SuccessMessage": "ログイン成功"
|
||||
},
|
||||
"ServerCardPopover": {
|
||||
"System": "システム",
|
||||
"CPU": "CPU",
|
||||
@ -52,6 +58,7 @@
|
||||
"Arch": "アーキテクチャ",
|
||||
"Mem": "メモリ",
|
||||
"Disk": "ディスク",
|
||||
"Region": "地域",
|
||||
"System": "システム",
|
||||
"CPU": "CPU"
|
||||
},
|
||||
|
@ -23,6 +23,12 @@
|
||||
"Detail": "詳細",
|
||||
"Network": "網路"
|
||||
},
|
||||
"SignIn": {
|
||||
"SignInMessage": "請輸入密碼",
|
||||
"Submit": "登入",
|
||||
"ErrorMessage": "密碼錯誤",
|
||||
"SuccessMessage": "登入成功"
|
||||
},
|
||||
"ServerCardPopover": {
|
||||
"System": "系統",
|
||||
"CPU": "CPU",
|
||||
@ -52,6 +58,7 @@
|
||||
"Arch": "架構",
|
||||
"Mem": "記憶體",
|
||||
"Disk": "磁碟",
|
||||
"Region": "地區",
|
||||
"System": "系統",
|
||||
"CPU": "CPU"
|
||||
},
|
||||
|
@ -23,6 +23,12 @@
|
||||
"Detail": "详情",
|
||||
"Network": "网络"
|
||||
},
|
||||
"SignIn": {
|
||||
"SignInMessage": "请输入密码",
|
||||
"Submit": "登录",
|
||||
"ErrorMessage": "密码错误",
|
||||
"SuccessMessage": "登录成功"
|
||||
},
|
||||
"ServerCardPopover": {
|
||||
"System": "系统",
|
||||
"CPU": "CPU",
|
||||
@ -52,6 +58,7 @@
|
||||
"Arch": "架构",
|
||||
"Mem": "内存",
|
||||
"Disk": "磁盘",
|
||||
"Region": "地区",
|
||||
"System": "系统",
|
||||
"CPU": "CPU"
|
||||
},
|
||||
|
@ -23,11 +23,6 @@ const withPWA = withPWAInit({
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
serverActions: {
|
||||
allowedOrigins: ["*"],
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
fetches: {
|
||||
fullUrl: true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nezha-dash",
|
||||
"version": "0.1.0",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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