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} + +
+
+ 日志分析面板 + +
+
+
{children}
); diff --git a/app/page.tsx b/app/page.tsx index 21b686d..cb73b3f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,103 +1,13 @@ -import Image from "next/image"; - export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- +
); } diff --git a/app/sites/[dir]/analysis.json/route.ts b/app/sites/[dir]/analysis.json/route.ts new file mode 100644 index 0000000..c5838fd --- /dev/null +++ b/app/sites/[dir]/analysis.json/route.ts @@ -0,0 +1,2 @@ +export const dynamic = "force-dynamic"; +export { GET } from "@/app/api/sites/[dir]/analysis/route"; diff --git a/app/sites/[dir]/page.tsx b/app/sites/[dir]/page.tsx new file mode 100644 index 0000000..6beaf7e --- /dev/null +++ b/app/sites/[dir]/page.tsx @@ -0,0 +1,203 @@ +"use client"; +import { useMemo } from "react"; +import useSWR from "swr"; +import { Bar, Line, Pie } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + PointElement, + LineElement, + ArcElement, + Tooltip, + Legend, + TimeScale, +} from "chart.js"; +import Link from "next/link"; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + PointElement, + LineElement, + ArcElement, + Tooltip, + Legend, + TimeScale, +); + +type Analysis = import("@/lib/analyzeLogs").LogAnalysisResult; + +const fetcher = (url: string) => fetch(url).then((r) => r.json()); + +export default function SiteDashboard({ params }: { params: { dir: string } }) { + const { data, error, isLoading } = useSWR( + `/api/sites/${encodeURIComponent(params.dir)}/analysis`, + fetcher, + { refreshInterval: 60_000 } + ); + + const topUrlsData = useMemo(() => { + const labels = data?.topUrls.map((x) => x.url) ?? []; + const values = data?.topUrls.map((x) => x.count) ?? []; + return { + labels, + datasets: [{ label: "URL 访问次数", data: values, backgroundColor: "#60a5fa" }], + }; + }, [data]); + + const statusPieData = useMemo(() => { + const labels = Object.keys(data?.statusCodeDistribution || {}); + const values = Object.values(data?.statusCodeDistribution || {}); + return { + labels, + datasets: [{ data: values, backgroundColor: ["#34d399", "#60a5fa", "#fbbf24", "#f87171", "#a78bfa"] }], + }; + }, [data]); + + const timelineData = useMemo(() => { + const labels = (data?.timeAccess || []).map((p) => new Date(p.time).toLocaleString()); + const values = (data?.timeAccess || []).map((p) => p.count); + return { + labels, + datasets: [{ label: "每小时请求量", data: values, borderColor: "#34d399", backgroundColor: "#34d39933" }], + }; + }, [data]); + + const devicePieData = useMemo(() => { + const entries = Object.entries(data?.deviceTypes || {}); + return { + labels: entries.map(([k]) => k), + datasets: [{ data: entries.map(([, v]) => v), backgroundColor: ["#60a5fa", "#f472b6", "#fbbf24", "#a78bfa"] }], + }; + }, [data]); + + const provinceData = useMemo(() => { + const entries = Object.entries(data?.provinceCityProportions || {}).sort((a, b) => b[1] - a[1]).slice(0, 15); + return { + labels: entries.map(([k]) => k), + datasets: [{ label: "区域占比%", data: entries.map(([, v]) => v), backgroundColor: "#a78bfa" }], + }; + }, [data]); + + return ( +
+
+
+

{decodeURIComponent(params.dir)} · 访问分析

+

版本 {data?.version ?? "-"}

+
+
+ 返回列表 + 原始日志 +
+
+ + {isLoading &&

加载中...

} + {error &&

加载失败:{String(error)}

} + {data && ( +
+
+ + + + +
+ +
+ + + + + + +
+ + + + + +
+ + + + + + +
+ + + + + + + + + + +
{data.recentRawLog.join("\n")}
+
+
+ )} +
+ ); +} + +function Stat({ label, value }: { label: string; value: number | string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function Card({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +function BrowserOsTable({ + kind, + data, +}: { + kind: "browser" | "os"; + data: Record>; +}) { + const rows = useMemo(() => { + const out: { group: string; version: string; count: number }[] = []; + Object.entries(data || {}).forEach(([g, versions]) => { + Object.entries(versions).forEach(([v, c]) => out.push({ group: g, version: v, count: c })); + }); + return out.sort((a, b) => b.count - a.count).slice(0, 10); + }, [data]); + + return ( +
+ + + + + + + + + + {rows.map((r, i) => ( + + + + + + ))} + +
{kind === "browser" ? "浏览器" : "操作系统"}版本次数
{r.group}{r.version}{r.count}
+
+ ); +} diff --git a/app/sites/[dir]/raw/route.ts b/app/sites/[dir]/raw/route.ts new file mode 100644 index 0000000..e766987 --- /dev/null +++ b/app/sites/[dir]/raw/route.ts @@ -0,0 +1,2 @@ +export const dynamic = "force-dynamic"; +export { GET } from "@/app/api/sites/[dir]/raw/route"; diff --git a/app/sites/page.tsx b/app/sites/page.tsx new file mode 100644 index 0000000..d71d490 --- /dev/null +++ b/app/sites/page.tsx @@ -0,0 +1,51 @@ +import Link from "next/link"; +import { readdir, stat } from "node:fs/promises"; +import { WEBSITE_FILE_DIR } from "@/lib/config"; + +export const dynamic = "force-dynamic"; + +export default async function SitesPage() { + let sites: string[] = []; + try { + const entries = await readdir(WEBSITE_FILE_DIR, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory()) { + try { + const s = await stat(`${WEBSITE_FILE_DIR}/${e.name}/log/access.log`); + if (s.isFile()) sites.push(e.name); + } catch {} + } + } + } catch (error) { + // 显示错误信息 + return ( +
+

Site 列表

+

读取目录失败:{String(error)}

+
+ ); + } + + sites.sort(); + + return ( +
+

Site 列表

+ +
+ ); +} diff --git a/bun.lock b/bun.lock index d615b3d..df58bc6 100644 --- a/bun.lock +++ b/bun.lock @@ -4,13 +4,19 @@ "": { "name": "admin-panel", "dependencies": { + "chart.js": "^4.5.0", + "geoip-lite": "^1.4.10", "next": "15.5.2", "react": "19.1.0", + "react-chartjs-2": "^5.3.0", "react-dom": "19.1.0", + "swr": "^2.3.6", + "ua-parser-js": "^2.0.4", }, "devDependencies": { "@biomejs/biome": "2.2.0", "@tailwindcss/postcss": "^4", + "@types/geoip-lite": "^1.4.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -98,6 +104,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@next/env": ["@next/env@15.5.2", "", {}, "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ=="], @@ -148,14 +156,36 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", "tailwindcss": "4.1.12" } }, "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ=="], + "@types/geoip-lite": ["@types/geoip-lite@1.4.4", "", {}, "sha512-2uVfn+C6bX/H356H6mjxsWUA5u8LO8dJgSBIRO/NFlpMe4DESzacutD/rKYrTDKm1Ugv78b4Wz1KvpHrlv3jSw=="], + "@types/node": ["@types/node@20.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], "@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "async": ["async@2.6.4", "", { "dependencies": { "lodash": "^4.17.14" } }, "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001737", "", {}, "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -168,18 +198,78 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-europe-js": ["detect-europe-js@0.1.2", "", {}, "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow=="], + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "geoip-lite": ["geoip-lite@1.4.10", "", { "dependencies": { "async": "2.1 - 2.6.4", "chalk": "4.1 - 4.1.2", "iconv-lite": "0.4.13 - 0.6.3", "ip-address": "5.8.9 - 5.9.4", "lazy": "1.0.11", "rimraf": "2.5.2 - 2.7.1", "yauzl": "2.9.2 - 2.10.0" } }, "sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@5.9.4", "", { "dependencies": { "jsbn": "1.1.0", "lodash": "^4.17.15", "sprintf-js": "1.1.2" } }, "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw=="], + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-standalone-pwa": ["is-standalone-pwa@0.1.1", "", {}, "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g=="], + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], + + "lazy": ["lazy@1.0.11", "", {}, "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], @@ -202,8 +292,18 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], @@ -214,14 +314,28 @@ "next": ["next@15.5.2", "", { "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.2", "@next/swc-darwin-x64": "15.5.2", "@next/swc-linux-arm64-gnu": "15.5.2", "@next/swc-linux-arm64-musl": "15.5.2", "@next/swc-linux-x64-gnu": "15.5.2", "@next/swc-linux-x64-musl": "15.5.2", "@next/swc-win32-arm64-msvc": "15.5.2", "@next/swc-win32-x64-msvc": "15.5.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react-chartjs-2": ["react-chartjs-2@5.3.0", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw=="], + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -232,22 +346,44 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "sprintf-js": ["sprintf-js@1.1.2", "", {}, "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "swr": ["swr@2.3.6", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw=="], + "tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="], "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="], "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "ua-is-frozen": ["ua-is-frozen@0.1.2", "", {}, "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw=="], + + "ua-parser-js": ["ua-parser-js@2.0.4", "", { "dependencies": { "@types/node-fetch": "^2.6.12", "detect-europe-js": "^0.1.2", "is-standalone-pwa": "^0.1.1", "node-fetch": "^2.7.0", "ua-is-frozen": "^0.1.2" }, "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-XiBOnM/UpUq21ZZ91q2AVDOnGROE6UQd37WrO9WBgw4u2eGvUCNOheMmZ3EfEUj7DLHr8tre+Um/436Of/Vwzg=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], diff --git a/lib/analyzeLogs.ts b/lib/analyzeLogs.ts new file mode 100644 index 0000000..9c3f82b --- /dev/null +++ b/lib/analyzeLogs.ts @@ -0,0 +1,187 @@ +// 移植自用户提供的 Fastify 版本,做了少量调整以适配 Next.js 环境 +import { UAParser } from "ua-parser-js"; + +export interface LogAnalysisResult { + uniqueIPCount: number; + indexAccessCount: number; + totalBytes: number; + provinceCityProportions: { [key: string]: number }; + statusCodeDistribution: { [key: string]: number }; + topUrls: { url: string; count: number }[]; + timeAccess: { time: number; count: number }[]; + browserVersions: { [browser: string]: { [version: string]: number } }; + deviceTypes: { [type: string]: number }; + osVersions: { [os: string]: { [version: string]: number } }; + recentRawLog: string[]; + version: string; +} + +export async function analyzeLogs(log: string): Promise { + const uniqueIPs = new Set(); + let indexAccessCount = 0; + let totalBytes = 0; + const ipCounts: Map = new Map(); + const statusCodeCounts: Map = new Map(); + const urlCounts: Map = new Map(); + const timeCounts: Map = new Map(); + + const browserVersionCounts: Map> = new Map(); + const deviceTypeCounts: Map = new Map(); + const osVersionCounts: Map> = new Map(); + + const lines = log.split("\n"); + const logRegex = + /^(\S+) - - \[(.*?)\] "(\S+) (\S+) (\S+)" (\d{3}) (\d+|-) "(.*?)" "(.*?)" ".*?"$/; + + for (const line of lines) { + const match = logRegex.exec(line); + if (match) { + const ip = match[1]; + const datetime = match[2]; + const url = match[4]; + const statusCode = match[6]; + const bytesStr = match[7]; + const bytes = bytesStr === "-" ? 0 : parseInt(bytesStr, 10); + const userAgentStr = match[9]; + + uniqueIPs.add(ip); + if (url === "/" || url === "/index.html") indexAccessCount++; + totalBytes += bytes; + ipCounts.set(ip, (ipCounts.get(ip) || 0) + 1); + statusCodeCounts.set(statusCode, (statusCodeCounts.get(statusCode) || 0) + 1); + urlCounts.set(url, (urlCounts.get(url) || 0) + 1); + + const dateTimeParts = datetime.split(":"); + if (dateTimeParts.length >= 2) { + const datePart = dateTimeParts[0]; + const hourPart = dateTimeParts[1]; + const timeKey = `${datePart}:${hourPart}`; + const date = new Date(parseLogDateTime(timeKey)).getTime(); + if (!Number.isNaN(date)) { + timeCounts.set(date, (timeCounts.get(date) || 0) + 1); + } + } + + const parser = new UAParser(userAgentStr as any); + const browser = parser.getBrowser(); + const device = parser.getDevice(); + const os = parser.getOS(); + + const browserName = browser.name || "Unknown"; + const browserVersion = browser.version || "Unknown"; + if (!browserVersionCounts.has(browserName)) { + browserVersionCounts.set(browserName, new Map()); + } + const browserMap = browserVersionCounts.get(browserName)!; + browserMap.set(browserVersion, (browserMap.get(browserVersion) || 0) + 1); + + const deviceType = (device.type as string) || "Desktop"; + deviceTypeCounts.set(deviceType, (deviceTypeCounts.get(deviceType) || 0) + 1); + + const osName = os.name || "Unknown"; + const osVersion = os.version || "Unknown"; + if (!osVersionCounts.has(osName)) { + osVersionCounts.set(osName, new Map()); + } + const osMap = osVersionCounts.get(osName)!; + osMap.set(osVersion, (osMap.get(osVersion) || 0) + 1); + } + } + + const provinceCityCounts: Map = new Map(); + // 动态加载 geoip-lite,避免构建期读取数据文件 + const geoip = await import("geoip-lite"); + for (const [ip, count] of ipCounts.entries()) { + const geo = geoip.lookup(ip as any); + if (geo) { + const country = geo.country || "Unknown"; + const region = (geo as any).region || "Unknown"; // geoip-lite typings + const city = geo.city || "Unknown"; + const key = `${country}-${region}-${city}`; + provinceCityCounts.set(key, (provinceCityCounts.get(key) || 0) + count); + } else { + provinceCityCounts.set("Unknown", (provinceCityCounts.get("Unknown") || 0) + count); + } + } + + const totalAccesses = [...ipCounts.values()].reduce((a, b) => a + b, 0) || 1; + const provinceCityProportions: { [key: string]: number } = {}; + for (const [key, count] of provinceCityCounts.entries()) { + provinceCityProportions[key] = parseFloat(((count / totalAccesses) * 100).toFixed(2)); + } + + const topUrls = Array.from(urlCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 20) + .map(([url, count]) => ({ url, count })); + + const timeAccess = Array.from(timeCounts.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([time, count]) => ({ time, count })); + + const statusCodeDistribution: { [key: string]: number } = {}; + for (const [code, count] of statusCodeCounts.entries()) { + statusCodeDistribution[code] = count; + } + + const browserVersions: { [browser: string]: { [version: string]: number } } = {}; + for (const [browser, versions] of browserVersionCounts.entries()) { + browserVersions[browser] = {}; + for (const [version, count] of versions.entries()) { + browserVersions[browser][version] = count; + } + } + + const deviceTypes: { [type: string]: number } = {}; + for (const [type, count] of deviceTypeCounts.entries()) { + deviceTypes[type] = count; + } + + const osVersions: { [os: string]: { [version: string]: number } } = {}; + for (const [os, versions] of osVersionCounts.entries()) { + osVersions[os] = {}; + for (const [version, count] of versions.entries()) { + osVersions[os][version] = count; + } + } + + return { + uniqueIPCount: uniqueIPs.size, + indexAccessCount, + totalBytes, + provinceCityProportions, + statusCodeDistribution, + topUrls, + timeAccess, + browserVersions, + deviceTypes, + osVersions, + recentRawLog: lines.slice(-100), + version: "1.0.3", + }; +} + +function parseLogDateTime(timeKey: string): string { + const [datePart, hourPart] = timeKey.split(":"); + const [day, monthStr, year] = datePart.split("/"); + const month = monthStringToNumber(monthStr); + return `${year}-${month}-${day}T${hourPart}:00:00+08:00`; +} + +function monthStringToNumber(monthStr: string): string { + const months: { [key: string]: string } = { + Jan: "01", + Feb: "02", + Mar: "03", + Apr: "04", + May: "05", + Jun: "06", + Jul: "07", + Aug: "08", + Sep: "09", + Oct: "10", + Nov: "11", + Dec: "12", + }; + return months[monthStr] || "01"; +} diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..70243f6 --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,9 @@ +export const WEBSITE_FILE_DIR = + process.env.WEBSITE_FILE_DIR || + "/opt/1panel/apps/openresty/openresty/www/sites/"; + +export function safeJoinSitePath(site: string, ...parts: string[]) { + // 防止路径穿越,仅允许字母数字、点、短横线和下划线 + const safe = site.replace(/[^a-zA-Z0-9._-]/g, ""); + return [WEBSITE_FILE_DIR, safe, ...parts].join("/"); +} diff --git a/package.json b/package.json index de36fdf..099cd2c 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,23 @@ "format": "biome format --write" }, "dependencies": { + "chart.js": "^4.5.0", + "geoip-lite": "^1.4.10", + "next": "15.5.2", "react": "19.1.0", + "react-chartjs-2": "^5.3.0", "react-dom": "19.1.0", - "next": "15.5.2" + "swr": "^2.3.6", + "ua-parser-js": "^2.0.4" }, "devDependencies": { - "typescript": "^5", + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", + "@types/geoip-lite": "^1.4.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", "tailwindcss": "^4", - "@biomejs/biome": "2.2.0" + "typescript": "^5" } }