diff --git a/README.md b/README.md index d8007ed..37cec3b 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,27 @@ Monitor your API usage through the PostgreSQL logs: - Identify usage patterns and optimize costs - Maintain compliance with audit requirements +### Metrics Dashboard + +OpenProxy includes a lightweight Next.js dashboard for real-time metrics visualization: + +```bash +cd dashboard +npm install +cp .env.example .env +# Configure DATABASE_URL in .env +npm run dev +``` + +The dashboard (available at `http://localhost:3008`) provides: +- **Real-time Overview**: Total requests, tokens, costs, and response times +- **Model Breakdown**: Usage statistics grouped by LLM model +- **Hourly Trends**: Visual charts showing request patterns over time +- **Recent Requests**: Detailed table of recent API calls +- **Auto-refresh**: Automatic updates every 30 seconds + +See [dashboard/README.md](./dashboard/README.md) for detailed setup instructions. + ## 🤝 Contributing Feel free to submit issues and enhancement requests! diff --git a/dashboard/.env.example b/dashboard/.env.example new file mode 100644 index 0000000..9c0bc76 --- /dev/null +++ b/dashboard/.env.example @@ -0,0 +1,5 @@ +# PostgreSQL connection string (same as proxy server) +DATABASE_URL=postgresql://user:password@localhost:5432/database + +# Database table name (default: llm_proxy) +DATABASE_TABLE=llm_proxy diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 0000000..8ccc874 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..49016ca --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,195 @@ +# OpenProxy Metrics Dashboard + +A lightweight Next.js dashboard for visualizing OpenProxy LLM request metrics in real-time. + +## Features + +- **Real-time Metrics Overview**: Total requests, tokens, costs, and response times +- **Model Breakdown**: Usage statistics grouped by LLM model +- **Hourly Trends**: Visual charts showing request patterns over time +- **Recent Requests**: Detailed table of recent API calls +- **Auto-refresh**: Automatic updates every 30 seconds +- **Time Range Selection**: View metrics for the last hour, 6 hours, 24 hours, or 7 days + +## Prerequisites + +- Node.js 18 or higher +- PostgreSQL database (same as the proxy server) +- OpenProxy proxy server running + +## Installation + +1. Navigate to the dashboard directory: + ```bash + cd dashboard + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Create a `.env` file (copy from `.env.example`): + ```bash + cp .env.example .env + ``` + +4. Configure your `.env` file: + ```env + DATABASE_URL=postgresql://user:password@localhost:5432/database + DATABASE_TABLE=llm_proxy + ``` + +## Running the Dashboard + +### Development Mode + +```bash +npm run dev +``` + +The dashboard will be available at `http://localhost:3008` + +### Production Mode + +1. Build the application: + ```bash + npm run build + ``` + +2. Start the production server: + ```bash + npm start + ``` + +## Dashboard Sections + +### 1. Overview Cards +Displays key metrics at a glance: +- Total requests processed +- Total tokens consumed +- Total cost incurred +- Average response time +- Number of unique models used +- Number of unique client IPs + +### 2. Hourly Trends +Two charts showing: +- Requests count and average response time over time +- Token usage and costs over time + +### 3. Model Breakdown +Table showing per-model statistics: +- Request count +- Total tokens used +- Total cost +- Average response time + +### 4. Recent Requests +Detailed table of recent API calls showing: +- Timestamp +- Model used +- Token breakdown (prompt + completion = total) +- Cost +- Response time +- HTTP status code +- Client IP address +- Whether the request was streamed + +## Configuration + +### Port +The dashboard runs on port 3008 by default. To change this, modify the `dev` and `start` scripts in `package.json`: + +```json +"dev": "next dev -p YOUR_PORT", +"start": "next start -p YOUR_PORT" +``` + +### Database Connection +Ensure the `DATABASE_URL` in your `.env` file matches the PostgreSQL connection string used by the proxy server. + +### Time Ranges +Available time ranges: +- Last Hour (1 hour) +- Last 6 Hours +- Last 24 Hours (default) +- Last 7 Days (168 hours) + +## Troubleshooting + +### "Failed to fetch metrics" Error +- Verify that the `DATABASE_URL` in `.env` is correct +- Ensure PostgreSQL is running and accessible +- Check that the `llm_proxy` table exists in your database +- Verify network connectivity to the database + +### Empty Dashboard +- Ensure the proxy server is running and processing requests +- Verify that requests are being logged to the database +- Check that the `DATABASE_TABLE` name matches your configuration + +### Port Conflicts +If port 3008 is already in use, change the port in `package.json` scripts. + +## Technology Stack + +- **Framework**: Next.js 14 (React 18) +- **Charts**: Recharts +- **Database**: PostgreSQL (via `pg` driver) +- **Language**: TypeScript +- **Styling**: Inline CSS (no external dependencies) + +## Architecture + +``` +dashboard/ +├── app/ +│ ├── api/ +│ │ └── metrics/ +│ │ └── route.ts # API endpoint for fetching metrics +│ ├── layout.tsx # Root layout +│ └── page.tsx # Main dashboard page +├── components/ +│ ├── MetricsOverview.tsx # Overview cards component +│ ├── ModelBreakdown.tsx # Model statistics table +│ ├── RecentRequests.tsx # Recent requests table +│ └── TrendsChart.tsx # Hourly trends charts +├── package.json +├── tsconfig.json +├── next.config.js +└── README.md +``` + +## API Endpoints + +### GET `/api/metrics` + +Query parameters: +- `hours` (optional): Number of hours to look back (default: 24) +- `limit` (optional): Maximum number of recent requests to return (default: 100) + +Response: +```json +{ + "success": true, + "data": { + "summary": { + "totalRequests": 1234, + "totalTokens": 567890, + "totalCost": 12.34, + "avgResponseTime": 450.5, + "uniqueModels": 3, + "uniqueClients": 15 + }, + "recentRequests": [...], + "modelBreakdown": [...], + "hourlyTrends": [...] + }, + "timeRange": "24 hours" +} +``` + +## License + +Same as OpenProxy parent project. diff --git a/dashboard/app/api/metrics/route.ts b/dashboard/app/api/metrics/route.ts new file mode 100644 index 0000000..b8d72be --- /dev/null +++ b/dashboard/app/api/metrics/route.ts @@ -0,0 +1,123 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { Pool } from 'pg'; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +const TABLE_NAME = process.env.DATABASE_TABLE || 'llm_proxy'; + +// Validate table name against whitelist to prevent SQL injection +const ALLOWED_TABLES = ['llm_proxy', 'llm_proxy_dev', 'llm_proxy_test']; +const validatedTableName = ALLOWED_TABLES.includes(TABLE_NAME) ? TABLE_NAME : 'llm_proxy'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const hours = parseInt(searchParams.get('hours') || '24', 10); + const limit = parseInt(searchParams.get('limit') || '100', 10); + + try { + const client = await pool.connect(); + + try { + // Get summary statistics + const summaryQuery = ` + SELECT + COUNT(*) as total_requests, + SUM(total_tokens) as total_tokens_used, + SUM(total_cost) as total_cost, + AVG(response_time) as avg_response_time, + COUNT(DISTINCT model) as unique_models, + COUNT(DISTINCT client_ip) as unique_clients + FROM ${validatedTableName} + WHERE timestamp >= NOW() - INTERVAL '$1 hours' + `; + const summaryResult = await client.query(summaryQuery, [hours]); + const summary = summaryResult.rows[0]; + + // Get recent requests + const recentQuery = ` + SELECT + request_id, + timestamp, + model, + prompt_tokens, + completion_tokens, + total_tokens, + total_cost, + response_time, + response_status, + client_ip, + stream + FROM ${validatedTableName} + WHERE timestamp >= NOW() - INTERVAL '$1 hours' + ORDER BY timestamp DESC + LIMIT $2 + `; + const recentResult = await client.query(recentQuery, [hours, limit]); + const recentRequests = recentResult.rows; + + // Get model breakdown + const modelQuery = ` + SELECT + model, + COUNT(*) as request_count, + SUM(total_tokens) as total_tokens, + SUM(total_cost) as total_cost, + AVG(response_time) as avg_response_time + FROM ${validatedTableName} + WHERE timestamp >= NOW() - INTERVAL '$1 hours' + GROUP BY model + ORDER BY request_count DESC + `; + const modelResult = await client.query(modelQuery, [hours]); + const modelBreakdown = modelResult.rows; + + // Get hourly trends + const trendsQuery = ` + SELECT + DATE_TRUNC('hour', timestamp) as hour, + COUNT(*) as requests, + SUM(total_tokens) as tokens, + SUM(total_cost) as cost, + AVG(response_time) as avg_response_time + FROM ${validatedTableName} + WHERE timestamp >= NOW() - INTERVAL '$1 hours' + GROUP BY hour + ORDER BY hour ASC + `; + const trendsResult = await client.query(trendsQuery, [hours]); + const hourlyTrends = trendsResult.rows; + + return NextResponse.json({ + success: true, + data: { + summary: { + totalRequests: parseInt(summary.total_requests || '0'), + totalTokens: parseInt(summary.total_tokens_used || '0'), + totalCost: parseFloat(summary.total_cost || '0'), + avgResponseTime: parseFloat(summary.avg_response_time || '0'), + uniqueModels: parseInt(summary.unique_models || '0'), + uniqueClients: parseInt(summary.unique_clients || '0'), + }, + recentRequests, + modelBreakdown, + hourlyTrends, + }, + timeRange: `${hours} hours`, + }); + } finally { + client.release(); + } + } catch (error) { + console.error('Database error:', error); + return NextResponse.json( + { + success: false, + error: 'Failed to fetch metrics', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} diff --git a/dashboard/app/layout.tsx b/dashboard/app/layout.tsx new file mode 100644 index 0000000..5f9562a --- /dev/null +++ b/dashboard/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'OpenProxy Metrics Dashboard', + description: 'Real-time metrics and analytics for OpenProxy LLM requests', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + + {children} + + ) +} diff --git a/dashboard/app/page.tsx b/dashboard/app/page.tsx new file mode 100644 index 0000000..331a627 --- /dev/null +++ b/dashboard/app/page.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import MetricsOverview from '@/components/MetricsOverview'; +import ModelBreakdown from '@/components/ModelBreakdown'; +import RecentRequests from '@/components/RecentRequests'; +import TrendsChart from '@/components/TrendsChart'; + +interface MetricsData { + summary: { + totalRequests: number; + totalTokens: number; + totalCost: number; + avgResponseTime: number; + uniqueModels: number; + uniqueClients: number; + }; + recentRequests: any[]; + modelBreakdown: any[]; + hourlyTrends: any[]; +} + +export default function Dashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [timeRange, setTimeRange] = useState(24); + const [autoRefresh, setAutoRefresh] = useState(true); + + const fetchMetrics = async () => { + try { + const response = await fetch(`/api/metrics?hours=${timeRange}`); + const result = await response.json(); + + if (result.success) { + setData(result.data); + setError(null); + } else { + setError(result.error || 'Failed to fetch metrics'); + } + } catch (err) { + setError('Network error: Unable to fetch metrics'); + console.error('Fetch error:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMetrics(); + }, [timeRange]); + + useEffect(() => { + if (!autoRefresh) return; + + const interval = setInterval(() => { + fetchMetrics(); + }, 30000); // Refresh every 30 seconds + + return () => clearInterval(interval); + }, [autoRefresh, timeRange]); + + if (loading) { + return ( +
+
Loading metrics...
+
+ ); + } + + if (error) { + return ( +
+
+

Error

+

{error}

+ +
+
+ ); + } + + if (!data) { + return ( +
+
No data available
+
+ ); + } + + return ( +
+
+

OpenProxy Metrics Dashboard

+
+ + + +
+
+ +
+ + + + +
+ +
+

Last updated: {new Date().toLocaleString()}

+
+
+ ); +} + +const styles = { + container: { + minHeight: '100vh', + backgroundColor: '#f5f7fa', + }, + header: { + backgroundColor: '#fff', + padding: '1.5rem 2rem', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap' as const, + gap: '1rem', + }, + title: { + fontSize: '1.8rem', + color: '#2c3e50', + fontWeight: 600, + }, + controls: { + display: 'flex', + gap: '1rem', + alignItems: 'center', + }, + select: { + padding: '0.5rem 1rem', + borderRadius: '6px', + border: '1px solid #ddd', + fontSize: '0.9rem', + cursor: 'pointer', + }, + checkboxLabel: { + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + fontSize: '0.9rem', + }, + refreshButton: { + padding: '0.5rem 1.5rem', + backgroundColor: '#3498db', + color: '#fff', + border: 'none', + borderRadius: '6px', + cursor: 'pointer', + fontSize: '0.9rem', + fontWeight: 500, + }, + main: { + maxWidth: '1400px', + margin: '0 auto', + padding: '2rem', + }, + loading: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100vh', + fontSize: '1.2rem', + color: '#7f8c8d', + }, + error: { + display: 'flex', + flexDirection: 'column' as const, + justifyContent: 'center', + alignItems: 'center', + height: '100vh', + gap: '1rem', + color: '#e74c3c', + }, + retryButton: { + padding: '0.5rem 1.5rem', + backgroundColor: '#e74c3c', + color: '#fff', + border: 'none', + borderRadius: '6px', + cursor: 'pointer', + fontSize: '0.9rem', + }, + footer: { + textAlign: 'center' as const, + padding: '2rem', + color: '#7f8c8d', + fontSize: '0.9rem', + }, +}; diff --git a/dashboard/components/MetricsOverview.tsx b/dashboard/components/MetricsOverview.tsx new file mode 100644 index 0000000..0562e2b --- /dev/null +++ b/dashboard/components/MetricsOverview.tsx @@ -0,0 +1,103 @@ +interface MetricsOverviewProps { + summary: { + totalRequests: number; + totalTokens: number; + totalCost: number; + avgResponseTime: number; + uniqueModels: number; + uniqueClients: number; + }; +} + +export default function MetricsOverview({ summary }: MetricsOverviewProps) { + const metrics = [ + { + label: 'Total Requests', + value: summary.totalRequests.toLocaleString(), + icon: '📊', + }, + { + label: 'Total Tokens', + value: summary.totalTokens.toLocaleString(), + icon: '🔢', + }, + { + label: 'Total Cost', + value: `$${summary.totalCost.toFixed(4)}`, + icon: '💰', + }, + { + label: 'Avg Response Time', + value: `${Math.round(summary.avgResponseTime)}ms`, + icon: '⚡', + }, + { + label: 'Unique Models', + value: summary.uniqueModels.toString(), + icon: '🤖', + }, + { + label: 'Unique Clients', + value: summary.uniqueClients.toString(), + icon: '👥', + }, + ]; + + return ( +
+

Overview

+
+ {metrics.map((metric) => ( +
+
{metric.icon}
+
+
{metric.label}
+
{metric.value}
+
+
+ ))} +
+
+ ); +} + +const styles = { + container: { + marginBottom: '2rem', + }, + title: { + fontSize: '1.5rem', + marginBottom: '1.5rem', + color: '#2c3e50', + }, + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', + gap: '1rem', + }, + card: { + backgroundColor: '#fff', + padding: '1.5rem', + borderRadius: '8px', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + display: 'flex', + gap: '1rem', + alignItems: 'center', + }, + icon: { + fontSize: '2rem', + }, + content: { + flex: 1, + }, + label: { + fontSize: '0.9rem', + color: '#7f8c8d', + marginBottom: '0.25rem', + }, + value: { + fontSize: '1.5rem', + fontWeight: 600, + color: '#2c3e50', + }, +}; diff --git a/dashboard/components/ModelBreakdown.tsx b/dashboard/components/ModelBreakdown.tsx new file mode 100644 index 0000000..9a9b420 --- /dev/null +++ b/dashboard/components/ModelBreakdown.tsx @@ -0,0 +1,99 @@ +interface ModelBreakdownProps { + models: { + model: string; + request_count: string; + total_tokens: string; + total_cost: string; + avg_response_time: string; + }[]; +} + +export default function ModelBreakdown({ models }: ModelBreakdownProps) { + if (!models || models.length === 0) { + return ( +
+

Model Breakdown

+
+

No model data available

+
+
+ ); + } + + return ( +
+

Model Breakdown

+
+ + + + + + + + + + + + {models.map((model) => ( + + + + + + + + ))} + +
ModelRequestsTotal TokensTotal CostAvg Response Time
+ {model.model} + {parseInt(model.request_count).toLocaleString()}{parseInt(model.total_tokens).toLocaleString()}${parseFloat(model.total_cost).toFixed(4)}{Math.round(parseFloat(model.avg_response_time))}ms
+
+
+ ); +} + +const styles = { + container: { + marginBottom: '2rem', + }, + title: { + fontSize: '1.5rem', + marginBottom: '1.5rem', + color: '#2c3e50', + }, + card: { + backgroundColor: '#fff', + borderRadius: '8px', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + overflow: 'hidden', + }, + table: { + width: '100%', + borderCollapse: 'collapse' as const, + }, + headerRow: { + backgroundColor: '#f8f9fa', + }, + th: { + padding: '1rem', + textAlign: 'left' as const, + fontSize: '0.9rem', + fontWeight: 600, + color: '#2c3e50', + borderBottom: '2px solid #e9ecef', + }, + row: { + borderBottom: '1px solid #e9ecef', + }, + td: { + padding: '1rem', + fontSize: '0.9rem', + color: '#495057', + }, + noData: { + padding: '2rem', + textAlign: 'center' as const, + color: '#7f8c8d', + }, +}; diff --git a/dashboard/components/RecentRequests.tsx b/dashboard/components/RecentRequests.tsx new file mode 100644 index 0000000..0fe57eb --- /dev/null +++ b/dashboard/components/RecentRequests.tsx @@ -0,0 +1,166 @@ +interface RecentRequestsProps { + requests: { + request_id: string; + timestamp: string; + model: string; + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + total_cost: string; + response_time: number; + response_status: number; + client_ip: string; + stream: boolean; + }[]; +} + +export default function RecentRequests({ requests }: RecentRequestsProps) { + if (!requests || requests.length === 0) { + return ( +
+

Recent Requests

+
+

No recent requests

+
+
+ ); + } + + return ( +
+

Recent Requests

+
+
+ + + + + + + + + + + + + + + {requests.map((req) => ( + + + + + + + + + + + ))} + +
TimestampModelTokensCostResponse TimeStatusClient IPStream
+ {new Date(req.timestamp).toLocaleString()} + + {req.model} + +
+ + {req.prompt_tokens} + {req.completion_tokens} = {req.total_tokens} + +
+
${parseFloat(req.total_cost).toFixed(4)}{req.response_time}ms + + {req.response_status} + + {req.client_ip}{req.stream ? '✓' : '✗'}
+
+
+
+ ); +} + +const styles = { + container: { + marginBottom: '2rem', + }, + title: { + fontSize: '1.5rem', + marginBottom: '1.5rem', + color: '#2c3e50', + }, + card: { + backgroundColor: '#fff', + borderRadius: '8px', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + overflow: 'hidden', + }, + tableWrapper: { + overflowX: 'auto' as const, + }, + table: { + width: '100%', + borderCollapse: 'collapse' as const, + minWidth: '1000px', + }, + headerRow: { + backgroundColor: '#f8f9fa', + }, + th: { + padding: '1rem', + textAlign: 'left' as const, + fontSize: '0.85rem', + fontWeight: 600, + color: '#2c3e50', + borderBottom: '2px solid #e9ecef', + }, + row: { + borderBottom: '1px solid #e9ecef', + }, + td: { + padding: '0.75rem 1rem', + fontSize: '0.85rem', + color: '#495057', + }, + modelBadge: { + backgroundColor: '#e3f2fd', + color: '#1976d2', + padding: '0.25rem 0.5rem', + borderRadius: '4px', + fontSize: '0.8rem', + fontWeight: 500, + }, + tokenBreakdown: { + display: 'flex', + flexDirection: 'column' as const, + }, + tokenDetail: { + color: '#7f8c8d', + fontSize: '0.75rem', + }, + statusBadge: { + padding: '0.25rem 0.5rem', + borderRadius: '4px', + fontSize: '0.8rem', + fontWeight: 500, + }, + statusSuccess: { + backgroundColor: '#d4edda', + color: '#155724', + }, + statusError: { + backgroundColor: '#f8d7da', + color: '#721c24', + }, + noData: { + padding: '2rem', + textAlign: 'center' as const, + color: '#7f8c8d', + }, +}; diff --git a/dashboard/components/TrendsChart.tsx b/dashboard/components/TrendsChart.tsx new file mode 100644 index 0000000..67f6473 --- /dev/null +++ b/dashboard/components/TrendsChart.tsx @@ -0,0 +1,160 @@ +'use client'; + +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; + +interface TrendsChartProps { + trends: { + hour: string; + requests: string; + tokens: string; + cost: string; + avg_response_time: string; + }[]; +} + +export default function TrendsChart({ trends }: TrendsChartProps) { + if (!trends || trends.length === 0) { + return ( +
+

Hourly Trends

+
+

No trend data available

+
+
+ ); + } + + const chartData = trends.map((trend) => ({ + time: new Date(trend.hour).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + }), + requests: parseInt(trend.requests), + tokens: parseInt(trend.tokens), + cost: parseFloat(trend.cost), + responseTime: Math.round(parseFloat(trend.avg_response_time)), + })); + + return ( +
+

Hourly Trends

+
+
+

Requests & Response Time

+ + + + + + + + + + + + +
+ +
+

Tokens & Cost

+ + + + + + + + + + + + +
+
+
+ ); +} + +const styles = { + container: { + marginBottom: '2rem', + }, + title: { + fontSize: '1.5rem', + marginBottom: '1.5rem', + color: '#2c3e50', + }, + card: { + backgroundColor: '#fff', + borderRadius: '8px', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + padding: '1.5rem', + }, + chartContainer: { + marginBottom: '2rem', + }, + chartTitle: { + fontSize: '1.1rem', + marginBottom: '1rem', + color: '#2c3e50', + }, + noData: { + padding: '2rem', + textAlign: 'center' as const, + color: '#7f8c8d', + }, +}; diff --git a/dashboard/next.config.js b/dashboard/next.config.js new file mode 100644 index 0000000..a843cbe --- /dev/null +++ b/dashboard/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..319ada8 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,1029 @@ +{ + "name": "openproxy-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openproxy-dashboard", + "version": "1.0.0", + "dependencies": { + "next": "^14.2.0", + "pg": "^8.16.3", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "recharts": "^2.12.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/pg": "^8.11.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@next/env": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.33.tgz", + "integrity": "sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", + "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz", + "integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.33", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..017653d --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,26 @@ +{ + "name": "openproxy-dashboard", + "version": "1.0.0", + "description": "Lightweight Next.js dashboard for OpenProxy metrics", + "private": true, + "scripts": { + "dev": "next dev -p 3008", + "build": "next build", + "start": "next start -p 3008", + "lint": "next lint" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "pg": "^8.16.3", + "recharts": "^2.12.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/pg": "^8.11.0", + "typescript": "^5.9.0" + } +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 0000000..5ddf5a5 --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}