diff --git a/frontend/app/ops/overview/page.tsx b/frontend/app/ops/overview/page.tsx new file mode 100644 index 00000000..4da4bfd5 --- /dev/null +++ b/frontend/app/ops/overview/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import { requireOpsAdmin } from "@/lib/ops-admin"; +import { OverviewPageClient } from "@/components/ops/overview/OverviewPageClient"; + +export const metadata: Metadata = { title: "总览 — PolyWeather Ops" }; + +export default async function OverviewPage() { + await requireOpsAdmin("/ops/overview"); + return ; +} diff --git a/frontend/app/ops/page.tsx b/frontend/app/ops/page.tsx index 6f91d6da..919ba13e 100644 --- a/frontend/app/ops/page.tsx +++ b/frontend/app/ops/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function OpsPage() { - redirect("/ops/system"); + redirect("/ops/overview"); } diff --git a/frontend/components/ops/analytics/AnalyticsPageClient.tsx b/frontend/components/ops/analytics/AnalyticsPageClient.tsx index 59d65950..d066d8cd 100644 --- a/frontend/components/ops/analytics/AnalyticsPageClient.tsx +++ b/frontend/components/ops/analytics/AnalyticsPageClient.tsx @@ -5,6 +5,16 @@ import { RefreshCcw } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { opsApi } from "@/lib/ops-api"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from "recharts"; type FunnelStep = { label: string; count: number; pct_of_prev?: number }; @@ -17,14 +27,19 @@ export function AnalyticsPageClient() { setLoading(true); try { const data = await opsApi.funnel(days); - const steps = (data as unknown as { steps?: FunnelStep[] }).steps ?? []; - setFunnel(steps); + setFunnel((data as unknown as { steps?: FunnelStep[] }).steps ?? []); } catch { /* */ } setLoading(false); }; useEffect(() => { void load(); }, [days]); + const chartData = [...funnel].reverse().map((s) => ({ + name: s.label, + count: s.count, + pct: s.pct_of_prev, + })); + const maxCount = Math.max(...funnel.map((s) => s.count), 1); if (loading) return
加载中...
; @@ -50,34 +65,100 @@ export function AnalyticsPageClient() { + {/* Summary cards */} + {funnel.length > 0 && ( +
+ + +
总注册
+
{funnel[0]?.count ?? 0}
+
+
+ + +
点击付费
+
{funnel[2]?.count ?? 0}
+
+
+ + +
发起支付
+
{funnel[4]?.count ?? 0}
+
+
+ + +
支付成功
+
{funnel[5]?.count ?? 0}
+
+ 总体转化 {maxCount > 0 ? `${((funnel[5]?.count ?? 0) / maxCount * 100).toFixed(1)}%` : "—"} +
+
+
+
+ )} + + {/* Funnel chart */} - 用户转化漏斗 + 转化漏斗 (Recharts) -
- {funnel.map((step, i) => { - const pct = (step.count / maxCount) * 100; - const prevPct = step.pct_of_prev != null ? `${step.pct_of_prev}%` : "—"; - return ( -
-
- {step.label} - - {step.count} - 转化 {prevPct} - -
-
-
-
-
- ); - })} - {funnel.length === 0 && ( -

暂无数据

- )} + {chartData.length === 0 ? ( +

暂无数据

+ ) : ( + + + + + + [`${value} 人`, "数量"]} + /> + + {chartData.map((_, i) => { + const colors = ["#06b6d4", "#0ea5e9", "#6366f1", "#f59e0b", "#22c55e", "#10b981"]; + return ; + })} + + + + )} + + + + {/* Drop-off table */} + + 各阶段详情 + +
+ + + + + + + + + + + {funnel.map((step, i) => { + const pct = step.pct_of_prev; + const dropPct = pct != null ? 100 - pct : 0; + return ( + + + + + + + ); + })} + +
阶段人数转化率流失率
{step.label}{step.count}{pct != null ? `${pct}%` : "—"}{i > 0 ? `${dropPct}%` : "—"}
diff --git a/frontend/components/ops/layout/AdminSidebar.tsx b/frontend/components/ops/layout/AdminSidebar.tsx index de63a656..30dbe37f 100644 --- a/frontend/components/ops/layout/AdminSidebar.tsx +++ b/frontend/components/ops/layout/AdminSidebar.tsx @@ -20,6 +20,7 @@ const navGroups = [ { label: "监控", items: [ + { href: "/ops/overview", icon: LayoutDashboard, label: "总览" }, { href: "/ops/system", icon: Cpu, label: "系统状态" }, { href: "/ops/training", icon: Database, label: "训练数据" }, { href: "/ops/analytics", icon: BarChart3, label: "转化分析" }, diff --git a/frontend/components/ops/overview/OverviewPageClient.tsx b/frontend/components/ops/overview/OverviewPageClient.tsx new file mode 100644 index 00000000..12bfa1c6 --- /dev/null +++ b/frontend/components/ops/overview/OverviewPageClient.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { RefreshCcw, TrendingUp, Users, CreditCard, Database, Activity } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { opsApi } from "@/lib/ops-api"; +import Link from "next/link"; +import type { SystemStatusPayload, MembershipsPayload, FunnelPayload } from "@/types/ops"; +import { + PieChart, Pie, Cell, ResponsiveContainer, Tooltip, +} from "recharts"; + +export function OverviewPageClient() { + const [loading, setLoading] = useState(true); + const [status, setStatus] = useState(null); + const [memberships, setMemberships] = useState(null); + const [funnel, setFunnel] = useState(null); + + const load = async () => { + setLoading(true); + try { + const [s, m, f] = await Promise.all([ + opsApi.systemStatus() as Promise, + opsApi.memberships() as Promise, + opsApi.funnel(7) as Promise, + ]); + setStatus(s); + setMemberships(m); + setFunnel(f); + } catch { /* */ } + setLoading(false); + }; + + useEffect(() => { void load(); }, []); + + if (loading) return
加载中...
; + + const cacheAnalysis = status?.cache?.analysis; + const cacheData = cacheAnalysis ? [ + { name: "命中", value: cacheAnalysis.cache_hits ?? 0, color: "#22c55e" }, + { name: "未命中", value: cacheAnalysis.cache_misses ?? 0, color: "#f59e0b" }, + { name: "强制刷新", value: cacheAnalysis.force_refresh_requests ?? 0, color: "#3b82f6" }, + ].filter((d) => d.value > 0) : []; + + const mems = memberships?.memberships ?? []; + const paid = mems.filter((m) => !(m as Record).is_trial).length; + const trials = mems.filter((m) => (m as Record).is_trial).length; + + const steps = funnel?.steps ?? []; + const totalUsers = steps[0]?.count ?? 0; + const payingUsers = steps[5]?.count ?? 0; + const convRate = totalUsers > 0 ? ((payingUsers / totalUsers) * 100).toFixed(1) : "—"; + + return ( +
+
+

总览

+ +
+ + {/* KPI cards */} +
+ + + +
+ + 系统状态 +
+
{status?.db?.ok ? "OK" : "—"}
+
+
+ + + + +
+ + 付费会员 +
+
{paid}
+ {trials > 0 &&
体验 {trials}
} +
+
+ + + + +
+ + 支付成功 +
+
{payingUsers}
+
+
+ + + + +
+ + 转化率 +
+
{convRate}%
+
+
+ + + + +
+ + 训练数据 +
+
+ {status?.training_data?.truth_records?.row_count ?? "—"} +
+
+
+ +
+ + {/* Cache pie chart */} + {cacheData.length > 0 && ( + + +

缓存命中率

+
+
+ + + + {cacheData.map((d, i) => ( + + ))} + + + + +
+
+ {cacheData.map((d) => ( +
+ + {d.name} + {d.value} +
+ ))} + {cacheAnalysis?.hit_rate != null && ( +
+ 命中率 + {(cacheAnalysis.hit_rate * 100).toFixed(1)}% +
+ )} +
+
+
+
+ )} +
+ ); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 78429ff9..b1e18365 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-leaflet": "^5.0.0", + "recharts": "^3.8.1", "tailwind-merge": "^3.3.0" }, "devDependencies": { @@ -370,6 +371,42 @@ "react-dom": "^19.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@reown/appkit": { "version": "1.8.17-wc-circular-dependencies-fix.0", "hasInstallScript": true, @@ -1129,6 +1166,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.99.1", "license": "MIT", @@ -1220,6 +1269,69 @@ "version": "0.6.0", "license": "MIT" }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/geojson": { "version": "7946.0.16", "dev": true, @@ -1265,6 +1377,12 @@ "version": "2.0.7", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "license": "MIT", @@ -2070,6 +2188,127 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/dayjs": { "version": "1.11.13", "license": "MIT" @@ -2081,6 +2320,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/defu": { "version": "6.1.4", "license": "MIT" @@ -2305,6 +2550,25 @@ ], "license": "BSD-3-Clause" }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "license": "MIT", @@ -2615,16 +2879,6 @@ "version": "1.6.7", "license": "MIT" }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "license": "MIT", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-mock-http": { "version": "1.0.4", "license": "MIT" @@ -3041,6 +3295,13 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT", + "peer": true + }, "node_modules/react-leaflet": { "version": "5.0.0", "license": "Hippocratic-2.1", @@ -3053,6 +3314,30 @@ "react-dom": "^19.0.0" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, @@ -3079,6 +3364,52 @@ "node": ">= 12.13.0" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -3090,6 +3421,12 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "dev": true, @@ -3379,6 +3716,12 @@ "real-require": "^0.2.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "dev": true, @@ -3625,6 +3968,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -3652,6 +4004,28 @@ } } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/viem": { "version": "2.47.2", "funding": [ diff --git a/frontend/package.json b/frontend/package.json index 00cad140..90a27c16 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-leaflet": "^5.0.0", + "recharts": "^3.8.1", "tailwind-merge": "^3.3.0" }, "devDependencies": {