diff --git a/README.md b/README.md index e215bc4..e7fb288 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ +## 日志分析面板(Next.js) + +基于 Next.js App Router,提供与给定 Fastify 示例等价的接口与前端仪表盘: + +- 列出站点:`GET /api/sites` 与页面 `/sites` +- 站点概览:`GET /api/sites/[dir]/analysis` 与页面 `/sites/[dir]` +- 原始日志:`GET /api/sites/[dir]/raw` + +默认读取目录:`/opt/1panel/apps/openresty/openresty/www/sites/`,可用环境变量覆盖: + +WEBSITE_FILE_DIR=/your/sites/root + +### 开发 + +```bash +# 安装依赖(使用 Bun) +bun install + +# 运行开发 +WEBSITE_FILE_DIR=/opt/1panel/apps/openresty/openresty/www/sites bun dev + +# 构建与运行生产 +bun run build +WEBSITE_FILE_DIR=/opt/1panel/apps/openresty/openresty/www/sites bun start +``` + +### 注意 + +- 本项目在 Node 运行时访问系统文件(access.log),请确保 Next.js 运行在服务器端(默认)。 +- `lib/config.ts` 对站点名做了简单的字符白名单,避免路径穿越。 This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started diff --git a/app/api/sites/[dir]/analysis/route.ts b/app/api/sites/[dir]/analysis/route.ts new file mode 100644 index 0000000..0218ae0 --- /dev/null +++ b/app/api/sites/[dir]/analysis/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { readFile } from "node:fs/promises"; +import { safeJoinSitePath } from "@/lib/config"; +import { analyzeLogs } from "@/lib/analyzeLogs"; + +export const runtime = "nodejs"; + +export async function GET( + _req: Request, + ctx: { params: Promise<{ dir: string }> } +) { + try { + const { dir } = await ctx.params; + const filePath = safeJoinSitePath(dir, "log", "access.log"); + const content = await readFile(filePath, { encoding: "utf-8" }); + const data = await analyzeLogs(content); + return NextResponse.json(data); + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 404 }); + } +} diff --git a/app/api/sites/[dir]/raw/route.ts b/app/api/sites/[dir]/raw/route.ts new file mode 100644 index 0000000..0928b77 --- /dev/null +++ b/app/api/sites/[dir]/raw/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { readFile } from "node:fs/promises"; +import { safeJoinSitePath } from "@/lib/config"; + +export const runtime = "nodejs"; + +export async function GET( + _req: Request, + ctx: { params: Promise<{ dir: string }> } +) { + try { + const { dir } = await ctx.params; + const filePath = safeJoinSitePath(dir, "log", "access.log"); + const content = await readFile(filePath, { encoding: "utf-8" }); + return new NextResponse(content, { headers: { "content-type": "text/plain; charset=utf-8" } }); + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 404 }); + } +} diff --git a/app/api/sites/route.ts b/app/api/sites/route.ts new file mode 100644 index 0000000..4a77ef7 --- /dev/null +++ b/app/api/sites/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { readdir, stat } from "node:fs/promises"; +import { WEBSITE_FILE_DIR } from "@/lib/config"; + +export const runtime = "nodejs"; + +export async function GET() { + try { + const entries = await readdir(WEBSITE_FILE_DIR, { withFileTypes: true }); + const dirs: string[] = []; + for (const e of entries) { + if (e.isDirectory()) { + // 仅列出存在 log/access.log 的站点 + try { + const s = await stat(`${WEBSITE_FILE_DIR}/${e.name}/log/access.log`); + if (s.isFile()) dirs.push(e.name); + } catch {} + } + } + dirs.sort(); + return NextResponse.json({ sites: dirs }); + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..b4d947d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,13 +5,6 @@ --foreground: #171717; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - @media (prefers-color-scheme: dark) { :root { --background: #0a0a0a; @@ -24,3 +17,6 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +a { color: inherit; } + diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..ea35bdf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "日志分析面板", + description: "基于 Next.js 的访问日志分析面板", }; export default function RootLayout({ @@ -24,10 +24,17 @@ export default function RootLayout({ }>) { return ( -
- {children} + +
- app/page.tsx
-
- .
-