diff --git a/deployment/install.sh b/deployment/install.sh index 9be40423..1bae1421 100755 --- a/deployment/install.sh +++ b/deployment/install.sh @@ -320,6 +320,8 @@ configure_interactive() { ENCRYPTION_KEY="$(openssl rand -hex 32)" VL_USERNAME="admin" VL_PASSWORD="$(openssl rand -hex 16)" + VM_USERNAME="admin" + VM_PASSWORD="$(openssl rand -hex 16)" REGISTRY_USERNAME="admin" REGISTRY_PASSWORD="$(openssl rand -hex 16)" REGISTRY_HTTP_SECRET="$(openssl rand -hex 32)" @@ -371,6 +373,10 @@ VL_USERNAME=${VL_USERNAME} VL_PASSWORD=${VL_PASSWORD} VL_RETENTION=7d +VM_USERNAME=${VM_USERNAME} +VM_PASSWORD=${VM_PASSWORD} +VM_RETENTION=30d + REGISTRY_USERNAME=${REGISTRY_USERNAME} REGISTRY_PASSWORD=${REGISTRY_PASSWORD} REGISTRY_HTTP_SECRET=${REGISTRY_HTTP_SECRET} diff --git a/docs/installation.mdx b/docs/installation.mdx index 25fe7e20..e9663467 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -139,6 +139,14 @@ use the common commands below when investigating a self-hosted service. | `VL_PASSWORD` | Logs service password | | `VL_RETENTION` | Log retention period (default: `7d`) | +### Victoria Metrics + +| Variable | Description | +| --- | --- | +| `VM_USERNAME` | Metrics service username | +| `VM_PASSWORD` | Metrics service password | +| `VM_RETENTION` | Metrics retention period (default: `30d`) | + ### Registry | Variable | Description | diff --git a/web/SELF-HOSTING.md b/web/SELF-HOSTING.md index 325b67cf..d995bcab 100644 --- a/web/SELF-HOSTING.md +++ b/web/SELF-HOSTING.md @@ -78,6 +78,14 @@ mutable tags such as `latest` or `tip`. | `VL_PASSWORD` | Logs service password | | `VL_RETENTION` | Log retention period (default: `7d`) | +### Victoria Metrics + +| Variable | Description | +|----------|-------------| +| `VM_USERNAME` | Metrics service username | +| `VM_PASSWORD` | Metrics service password | +| `VM_RETENTION` | Metrics retention period (default: `30d`) | + ### Registry | Variable | Description | diff --git a/web/app/(dashboard)/dashboard/metrics/loading.tsx b/web/app/(dashboard)/dashboard/metrics/loading.tsx new file mode 100644 index 00000000..c0e0f864 --- /dev/null +++ b/web/app/(dashboard)/dashboard/metrics/loading.tsx @@ -0,0 +1,60 @@ +import { SetBreadcrumbs } from "@/components/core/breadcrumb-data"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Loading() { + return ( + <> + + +
+ Loading metrics +
+ + ); +} diff --git a/web/app/(dashboard)/dashboard/metrics/page.tsx b/web/app/(dashboard)/dashboard/metrics/page.tsx new file mode 100644 index 00000000..26064760 --- /dev/null +++ b/web/app/(dashboard)/dashboard/metrics/page.tsx @@ -0,0 +1,36 @@ +import { SetBreadcrumbs } from "@/components/core/breadcrumb-data"; +import { MetricsHistoryCharts } from "@/components/metrics/metrics-history-charts"; +import { listServers } from "@/db/queries"; + +export default async function MetricsPage() { + const servers = await listServers(); + + return ( + <> + +
+
+

Metrics

+

+ Cluster-level infrastructure health and historical resource usage +

+
+ + ({ + id: server.id, + name: server.name, + }))} + /> +
+ + ); +} diff --git a/web/app/api/cluster-metrics/route.ts b/web/app/api/cluster-metrics/route.ts new file mode 100644 index 00000000..57404906 --- /dev/null +++ b/web/app/api/cluster-metrics/route.ts @@ -0,0 +1,65 @@ +import { headers } from "next/headers"; +import { listServers } from "@/db/queries"; +import { auth } from "@/lib/auth"; +import { + isMetricsEnabled, + METRIC_RANGE_OPTIONS, + parseMetricRange, + queryServersMetricsHistory, + warnMissingMetricsConfig, +} from "@/lib/victoria-metrics"; + +export async function GET(request: Request) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + + const url = new URL(request.url); + const range = parseMetricRange(url.searchParams.get("range")); + const serverId = url.searchParams.get("serverId"); + + if (!isMetricsEnabled()) { + warnMissingMetricsConfig("cluster"); + return Response.json({ + range, + series: [], + enabled: false, + }); + } + + const end = new Date(); + const option = METRIC_RANGE_OPTIONS[range]; + const start = new Date(end.getTime() - option.durationMs); + const servers = await listServers(); + const selectedServers = + serverId && serverId !== "all" + ? servers.filter((server) => server.id === serverId) + : servers; + + try { + const series = await queryServersMetricsHistory({ + servers: selectedServers.map((server) => ({ + id: server.id, + name: server.name, + })), + start, + end, + stepSeconds: option.stepSeconds, + }); + + return Response.json({ + range, + series, + }); + } catch (error) { + console.error("[metrics:cluster] failed to query metrics:", error); + return Response.json({ + range, + series: [], + }); + } +} diff --git a/web/app/api/servers/[id]/metrics/route.ts b/web/app/api/servers/[id]/metrics/route.ts index 8ae62d1b..4c5dd4f0 100644 --- a/web/app/api/servers/[id]/metrics/route.ts +++ b/web/app/api/servers/[id]/metrics/route.ts @@ -3,19 +3,13 @@ import { auth } from "@/lib/auth"; import { emptyHistory, isMetricsEnabled, + METRIC_RANGE_OPTIONS, + parseMetricRange, queryNodeMetricsHistory, queryNodeMetricsSnapshot, + warnMissingMetricsConfig, } from "@/lib/victoria-metrics"; -const RANGE_OPTIONS = { - "1h": { durationMs: 60 * 60 * 1000, stepSeconds: 30 }, - "6h": { durationMs: 6 * 60 * 60 * 1000, stepSeconds: 60 }, - "24h": { durationMs: 24 * 60 * 60 * 1000, stepSeconds: 5 * 60 }, - "7d": { durationMs: 7 * 24 * 60 * 60 * 1000, stepSeconds: 30 * 60 }, -} as const; - -type RangeKey = keyof typeof RANGE_OPTIONS; - export async function GET( request: Request, { params }: { params: Promise<{ id: string }> }, @@ -30,9 +24,10 @@ export async function GET( const { id: serverId } = await params; const url = new URL(request.url); - const range = parseRange(url.searchParams.get("range")); + const range = parseMetricRange(url.searchParams.get("range")); if (!isMetricsEnabled()) { + warnMissingMetricsConfig("server"); return Response.json({ current: null, history: emptyHistory(), @@ -42,7 +37,7 @@ export async function GET( } const end = new Date(); - const option = RANGE_OPTIONS[range]; + const option = METRIC_RANGE_OPTIONS[range]; const start = new Date(end.getTime() - option.durationMs); try { @@ -60,7 +55,6 @@ export async function GET( current, history, range, - enabled: true, }); } catch (error) { console.error("[metrics:server] failed to query metrics:", error); @@ -68,14 +62,6 @@ export async function GET( current: null, history: emptyHistory(), range, - enabled: true, }); } } - -function parseRange(value: string | null): RangeKey { - if (value && value in RANGE_OPTIONS) { - return value as RangeKey; - } - return "1h"; -} diff --git a/web/components/cluster/cluster-health-summary.tsx b/web/components/cluster/cluster-health-summary.tsx index 0944477b..66f1508e 100644 --- a/web/components/cluster/cluster-health-summary.tsx +++ b/web/components/cluster/cluster-health-summary.tsx @@ -1,7 +1,9 @@ "use client"; -import { Activity, Cpu, Network, Server } from "lucide-react"; +import { Activity, BarChart3, Cpu, Network, Server } from "lucide-react"; +import Link from "next/link"; import useSWR from "swr"; +import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { fetcher } from "@/lib/fetcher"; @@ -18,10 +20,12 @@ type ClusterHealthData = { interface ClusterHealthSummaryProps { initialData: ClusterHealthData; + showMetricsLink?: boolean; } export function ClusterHealthSummary({ initialData, + showMetricsLink = true, }: ClusterHealthSummaryProps) { const { data } = useSWR("/api/cluster-health", fetcher, { fallbackData: initialData, @@ -63,11 +67,24 @@ export function ClusterHealthSummary({ return (
-
-

Cluster Health

-

- Real-time infrastructure status -

+
+
+

Cluster Health

+

+ Real-time infrastructure status +

+
+ {showMetricsLink && ( + + )}
{stats.map((stat) => ( diff --git a/web/components/metrics/metrics-history-charts.tsx b/web/components/metrics/metrics-history-charts.tsx new file mode 100644 index 00000000..811a4cd1 --- /dev/null +++ b/web/components/metrics/metrics-history-charts.tsx @@ -0,0 +1,492 @@ +"use client"; + +import { Activity, Cpu, HardDrive, MemoryStick } from "lucide-react"; +import { useMemo, useState } from "react"; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import useSWR from "swr"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + NativeSelect, + NativeSelectOption, +} from "@/components/ui/native-select"; +import { Spinner } from "@/components/ui/spinner"; +import { fetcher } from "@/lib/fetcher"; +import { METRIC_RANGE_KEYS, type MetricRange } from "@/lib/metric-ranges"; +import type { + MetricsHistory, + ServerMetricsHistory, +} from "@/lib/victoria-metrics"; + +type MetricsResponse = { + range: MetricRange; + series: ServerMetricsHistory[]; +}; + +type ChartRow = { + timestamp: string; +} & Record; + +type MetricsHistoryChartsProps = { + endpoint: string; + title: string; + description: string; + servers: Array<{ id: string; name: string }>; +}; + +type ChartConfig = { + title: string; + description: string; + icon: typeof Cpu; + percentKey: keyof MetricsHistory; + bytesKey?: keyof MetricsHistory; + percentColor: string; +}; + +type TooltipPayload = { + name?: string; + value?: unknown; + color?: string; + dataKey?: string; +}; + +type MetricsTooltipProps = { + active?: boolean; + label?: string | number; + payload?: readonly TooltipPayload[]; +}; + +const CHARTS: ChartConfig[] = [ + { + title: "CPU", + description: "Usage percent by server", + icon: Cpu, + percentKey: "cpuUsagePercent", + percentColor: "#10b981", + }, + { + title: "Memory", + description: "Usage percent and used memory by server", + icon: MemoryStick, + percentKey: "memoryUsagePercent", + bytesKey: "memoryUsedBytes", + percentColor: "#0ea5e9", + }, + { + title: "Disk", + description: "Usage percent and used storage by server", + icon: HardDrive, + percentKey: "diskUsagePercent", + bytesKey: "diskUsedBytes", + percentColor: "#f59e0b", + }, +]; + +const SERVER_COLORS = [ + "#10b981", + "#0ea5e9", + "#f59e0b", + "#ec4899", + "#8b5cf6", + "#14b8a6", + "#f43f5e", + "#84cc16", +]; + +export function MetricsHistoryCharts({ + endpoint, + title, + description, + servers, +}: MetricsHistoryChartsProps) { + const [range, setRange] = useState("1h"); + const [selectedServerId, setSelectedServerId] = useState("all"); + const requestUrl = useMemo(() => { + const params = new URLSearchParams({ range }); + if (selectedServerId !== "all") { + params.set("serverId", selectedServerId); + } + return `${endpoint}?${params.toString()}`; + }, [endpoint, range, selectedServerId]); + const { data, error, isLoading } = useSWR( + requestUrl, + fetcher, + { refreshInterval: 60000 }, + ); + + const series = data?.series ?? []; + const rows = useMemo(() => buildChartRows(series), [series]); + const hasData = series.some((server) => + Object.values(server.history).some((points) => points.length > 0), + ); + + return ( +
+
+
+

{title}

+

{description}

+
+
+ setSelectedServerId(event.target.value)} + aria-label="Server" + > + All + {servers.map((server) => ( + + {server.name} + + ))} + + setRange(event.target.value as MetricRange)} + aria-label="Metrics range" + > + {METRIC_RANGE_KEYS.map((option) => ( + + {option} + + ))} + +
+
+ + {error ? ( + + ) : isLoading ? ( + + ) : !hasData ? ( + + ) : ( +
+ {CHARTS.map((chart) => ( + + ))} +
+ )} +
+ ); +} + +function MetricChartCard({ + chart, + rows, + series, +}: { + chart: ChartConfig; + rows: ChartRow[]; + series: ServerMetricsHistory[]; +}) { + const Icon = chart.icon; + + return ( + + +
+
+
+ +
+
+ {chart.title} + {chart.description} +
+
+
+
+ +
+ + + + + `${value}%`} + className="text-xs" + /> + {chart.bytesKey && ( + + )} + + {thresholdsForChart().map((threshold) => ( + + ))} + ( + + )} + /> + {series.map((server, index) => ( + + ))} + {chart.bytesKey ? renderByteLines(chart.bytesKey, series) : null} + + +
+
+
+ ); +} + +function MetricsStateCard({ + icon: Icon, + title, + description, + loading = false, +}: { + icon: typeof Activity; + title: string; + description: string; + loading?: boolean; +}) { + return ( + + +
+ {loading ? ( + + ) : ( + + )} +
+
+

{title}

+

{description}

+
+
+
+ ); +} + +function renderByteLines( + bytesKey: keyof MetricsHistory, + series: ServerMetricsHistory[], +) { + return series.map((server, index) => ( + + )); +} + +function MetricsTooltip({ active, payload, label }: MetricsTooltipProps) { + if (!active || !payload?.length) return null; + + return ( +
+

{formatTooltipTime(String(label))}

+
+ {payload.map((item) => ( +
+ + + {item.name} + + + {formatTooltipValue(item)} + +
+ ))} +
+
+ ); +} + +function buildChartRows(series: ServerMetricsHistory[]): ChartRow[] { + const rows = new Map(); + for (const server of series) { + for (const [key, points] of Object.entries(server.history) as Array< + [keyof MetricsHistory, MetricsHistory[keyof MetricsHistory]] + >) { + for (const point of points) { + const row = rows.get(point.timestamp) ?? { timestamp: point.timestamp }; + row[getSeriesKey(key, server.serverId)] = point.value; + rows.set(point.timestamp, row); + } + } + } + + return Array.from(rows.values()).sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(), + ); +} + +function thresholdsForChart() { + return [ + { value: 70, color: "#f59e0b" }, + { value: 90, color: "#f43f5e" }, + ]; +} + +function formatTooltipValue(item: TooltipPayload) { + const value = Number(item.value); + if (!Number.isFinite(value)) return "-"; + if (isBytesSeries(String(item.dataKey))) return formatBytes(value); + return `${value.toFixed(1)}%`; +} + +function getSeriesKey(metricKey: keyof MetricsHistory, serverId: string) { + return `${metricKey}:${serverId}`; +} + +function getServerColor(index: number) { + return SERVER_COLORS[index % SERVER_COLORS.length]; +} + +function isBytesSeries(dataKey: string) { + return ( + dataKey.startsWith("memoryUsedBytes:") || + dataKey.startsWith("diskUsedBytes:") + ); +} + +function formatShortTime(value: string) { + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(new Date(value)); +} + +function formatTooltipTime(value: string) { + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }).format(new Date(value)); +} + +function formatBytesCompact(value: number) { + if (value >= 1024 ** 4) return `${(value / 1024 ** 4).toFixed(1)}T`; + if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)}G`; + if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(0)}M`; + return `${Math.round(value / 1024)}K`; +} + +function formatBytes(value: number) { + if (value >= 1024 ** 4) return `${(value / 1024 ** 4).toFixed(2)} TB`; + if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(2)} GB`; + if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB`; + return `${Math.round(value / 1024)} KB`; +} diff --git a/web/lib/metric-ranges.ts b/web/lib/metric-ranges.ts new file mode 100644 index 00000000..5fa5f669 --- /dev/null +++ b/web/lib/metric-ranges.ts @@ -0,0 +1,20 @@ +export const METRIC_RANGE_OPTIONS = { + "1h": { durationMs: 60 * 60 * 1000, stepSeconds: 60 }, + "6h": { durationMs: 6 * 60 * 60 * 1000, stepSeconds: 60 }, + "24h": { durationMs: 24 * 60 * 60 * 1000, stepSeconds: 5 * 60 }, + "7d": { durationMs: 7 * 24 * 60 * 60 * 1000, stepSeconds: 30 * 60 }, + "30d": { durationMs: 30 * 24 * 60 * 60 * 1000, stepSeconds: 2 * 60 * 60 }, +} as const; + +export type MetricRange = keyof typeof METRIC_RANGE_OPTIONS; + +export const METRIC_RANGE_KEYS = Object.keys( + METRIC_RANGE_OPTIONS, +) as MetricRange[]; + +export function parseMetricRange(value: string | null): MetricRange { + if (value && value in METRIC_RANGE_OPTIONS) { + return value as MetricRange; + } + return "1h"; +} diff --git a/web/lib/victoria-metrics.ts b/web/lib/victoria-metrics.ts index 5b34c957..23984e7f 100644 --- a/web/lib/victoria-metrics.ts +++ b/web/lib/victoria-metrics.ts @@ -1,5 +1,14 @@ +import { + METRIC_RANGE_OPTIONS, + type MetricRange, + parseMetricRange, +} from "@/lib/metric-ranges"; + +export { METRIC_RANGE_OPTIONS, type MetricRange, parseMetricRange }; + const VICTORIA_METRICS_URL = process.env.VICTORIA_METRICS_URL; const VICTORIA_METRICS_PRIVATE_URL = process.env.VICTORIA_METRICS_PRIVATE_URL; +let hasWarnedMissingMetricsConfig = false; type EndpointConfig = { url: string; @@ -50,6 +59,14 @@ export type NodeMetricsHistory = { diskUsedBytes: NodeMetricPoint[]; }; +export type MetricsHistory = NodeMetricsHistory; + +export type ServerMetricsHistory = { + serverId: string; + serverName: string; + history: MetricsHistory; +}; + const METRIC_NAMES = { cpuUsagePercent: "techulus_node_cpu_usage_percent", memoryUsagePercent: "techulus_node_memory_usage_percent", @@ -87,6 +104,15 @@ export function isMetricsEnabled(): boolean { return !!(VICTORIA_METRICS_PRIVATE_URL || VICTORIA_METRICS_URL); } +export function warnMissingMetricsConfig(context: string) { + if (hasWarnedMissingMetricsConfig) return; + + hasWarnedMissingMetricsConfig = true; + console.warn( + `[metrics:${context}] Missing VictoriaMetrics configuration: set VICTORIA_METRICS_URL or VICTORIA_METRICS_PRIVATE_URL to enable metrics history.`, + ); +} + export async function queryNodeMetricsSnapshots( serverIds: string[], ): Promise> { @@ -180,6 +206,62 @@ export async function queryNodeMetricsHistory(options: { return Object.fromEntries(entries) as NodeMetricsHistory; } +export async function queryServersMetricsHistory(options: { + servers: Array<{ id: string; name: string }>; + start: Date; + end: Date; + stepSeconds: number; +}): Promise { + const endpoint = getQueryEndpoint(); + if (!endpoint || options.servers.length === 0) return []; + + const [cpuMap, memPctMap, memBytesMap, diskPctMap, diskBytesMap] = + await Promise.all([ + queryRangeMetricGroup(endpoint, { + metricName: METRIC_NAMES.cpuUsagePercent, + start: options.start, + end: options.end, + stepSeconds: options.stepSeconds, + }).catch(() => new Map()), + queryRangeMetricGroup(endpoint, { + metricName: METRIC_NAMES.memoryUsagePercent, + start: options.start, + end: options.end, + stepSeconds: options.stepSeconds, + }).catch(() => new Map()), + queryRangeMetricGroup(endpoint, { + metricName: METRIC_NAMES.memoryUsedBytes, + start: options.start, + end: options.end, + stepSeconds: options.stepSeconds, + }).catch(() => new Map()), + queryRangeMetricGroup(endpoint, { + metricName: METRIC_NAMES.diskUsagePercent, + start: options.start, + end: options.end, + stepSeconds: options.stepSeconds, + }).catch(() => new Map()), + queryRangeMetricGroup(endpoint, { + metricName: METRIC_NAMES.diskUsedBytes, + start: options.start, + end: options.end, + stepSeconds: options.stepSeconds, + }).catch(() => new Map()), + ]); + + return options.servers.map((server) => ({ + serverId: server.id, + serverName: server.name, + history: { + cpuUsagePercent: cpuMap.get(server.id) ?? [], + memoryUsagePercent: memPctMap.get(server.id) ?? [], + memoryUsedBytes: memBytesMap.get(server.id) ?? [], + diskUsagePercent: diskPctMap.get(server.id) ?? [], + diskUsedBytes: diskBytesMap.get(server.id) ?? [], + }, + })); +} + async function queryInstantMetric( endpoint: EndpointConfig, metricName: string, @@ -285,6 +367,53 @@ async function queryRangeMetric( .filter((point) => Number.isFinite(point.value)); } +async function queryRangeMetricGroup( + endpoint: EndpointConfig, + options: { + metricName: string; + start: Date; + end: Date; + stepSeconds: number; + }, +): Promise> { + const url = new URL(`${endpoint.url}/api/v1/query_range`); + url.searchParams.set("query", options.metricName); + url.searchParams.set( + "start", + String(Math.floor(options.start.getTime() / 1000)), + ); + url.searchParams.set("end", String(Math.floor(options.end.getTime() / 1000))); + url.searchParams.set("step", String(options.stepSeconds)); + + const response = await fetch(url.toString(), buildFetchOptions(endpoint)); + if (!response.ok) { + throw new Error( + `Failed to query metrics range group: ${response.status} ${response.statusText}`, + ); + } + + const data = (await response.json()) as VictoriaMatrixResponse; + if (data.status !== "success") { + throw new Error(data.error || "Failed to query metrics range group"); + } + + const byServer = new Map(); + for (const result of data.data?.result ?? []) { + const serverId = result.metric.server_id; + if (!serverId) continue; + byServer.set( + serverId, + result.values + .map(([timestamp, rawValue]) => ({ + timestamp: new Date(timestamp * 1000).toISOString(), + value: Number.parseFloat(rawValue), + })) + .filter((point) => Number.isFinite(point.value)), + ); + } + return byServer; +} + export function emptyHistory(): NodeMetricsHistory { return { cpuUsagePercent: [], diff --git a/web/package.json b/web/package.json index ff8e9f7a..d62fff4b 100644 --- a/web/package.json +++ b/web/package.json @@ -39,6 +39,7 @@ "pg": "^8.16.3", "react": "19.2.7", "react-dom": "19.2.7", + "recharts": "^3.8.1", "shadcn": "^3.6.2", "sonner": "^2.0.7", "swr": "^2.3.8", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 29eed22b..00a9689c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: react-dom: specifier: 19.2.7 version: 19.2.7(react@19.2.7) + recharts: + specifier: ^3.8.1 + version: 3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@16.13.1)(react@19.2.7)(redux@5.0.1) shadcn: specifier: ^3.6.2 version: 3.8.5(@types/node@24.13.2)(typescript@5.9.3) @@ -2239,6 +2242,17 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + 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 + '@rolldown/binding-android-arm64@1.0.3': resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2392,6 +2406,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2513,6 +2530,33 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -2575,6 +2619,9 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -3229,6 +3276,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -3269,6 +3360,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -3540,6 +3634,9 @@ packages: resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} engines: {node: '>= 0.4'} + es-toolkit@1.48.1: + resolution: {integrity: sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==} + esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -3698,6 +3795,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.1.0: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} @@ -4048,6 +4148,12 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.8: + resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -4110,6 +4216,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -5061,6 +5171,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': 19.2.17 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -5069,6 +5191,22 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + 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 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -5096,6 +5234,9 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} @@ -5597,6 +5738,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7961,6 +8105,18 @@ snapshots: dependencies: react: 19.2.7 + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.8 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.2.0 + optionalDependencies: + react: 19.2.7 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) + '@rolldown/binding-android-arm64@1.0.3': optional: true @@ -8075,6 +8231,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -8189,6 +8347,30 @@ snapshots: dependencies: '@types/node': 24.13.2 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -8261,6 +8443,8 @@ snapshots: dependencies: '@types/node': 24.13.2 + '@types/use-sync-external-store@0.0.6': {} + '@types/validate-npm-package-name@4.0.2': {} '@types/validator@13.15.10': {} @@ -8884,6 +9068,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + damerau-levenshtein@1.0.8: {} data-uri-to-buffer@4.0.1: {} @@ -8918,6 +9140,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + dedent@1.7.2: {} deep-is@0.1.4: {} @@ -9158,6 +9382,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.48.1: {} + esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -9252,7 +9478,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.9 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -9275,7 +9501,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -9290,14 +9516,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -9312,7 +9538,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -9460,6 +9686,8 @@ snapshots: etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.1.0: {} eventsource@3.0.7: @@ -9878,6 +10106,10 @@ snapshots: ignore@7.0.5: {} + immer@10.2.0: {} + + immer@11.1.8: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -9944,6 +10176,8 @@ snapshots: hasown: 2.0.4 side-channel: 1.1.1 + internmap@2.0.3: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -10784,6 +11018,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + redux: 5.0.1 + react@19.2.7: {} recast@0.23.11: @@ -10794,6 +11037,32 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + recharts@3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@16.13.1)(react@19.2.7)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.48.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-is: 16.13.1 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.7) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -10835,6 +11104,8 @@ snapshots: transitivePeerDependencies: - supports-color + reselect@5.1.1: {} + reselect@5.2.0: {} resolve-from@4.0.0: {} @@ -11475,6 +11746,23 @@ snapshots: vary@1.1.2: {} + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0