From 4e19b992df12abec133be0c2e8620fbac9812160 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 6 Oct 2025 22:43:28 +0000 Subject: [PATCH] feat: Complete production deployment pipeline with admin dashboard - Add unified deployment script with Node.js 22 installation - Create comprehensive database migration script (28 migrations + admin tables) - Add production start/stop scripts for all services - Integrate admin dashboard (parentflow-admin) into PM2 ecosystem - Configure all services: Backend (3020), Frontend (3030), Admin (3335) - Update ecosystem.config.js with admin dashboard configuration - Add invite codes module for user registration management --- DEPLOY_NOW.md | 170 - DEPLOY_PRODUCTION_SERVER.md | 76 - deploy-production.sh | 331 ++ deploy-to-production.sh | 197 - ecosystem.config.js | 36 + .../maternal-app-backend/create-admin-user.js | 32 + .../maternal-app-backend/src/app.module.ts | 2 + .../migrations/V028_create_invite_codes.sql | 189 + .../invite-codes/invite-codes.controller.ts | 80 + .../modules/invite-codes/invite-codes.dto.ts | 57 + .../invite-codes/invite-codes.entity.ts | 113 + .../invite-codes/invite-codes.module.ts | 13 + .../invite-codes/invite-codes.service.ts | 204 + migrate-production.sh | 278 ++ parentflow-admin/.gitignore | 41 + parentflow-admin/README.md | 36 + parentflow-admin/next.config.ts | 7 + parentflow-admin/package-lock.json | 3564 +++++++++++++++++ parentflow-admin/package.json | 34 + parentflow-admin/postcss.config.mjs | 5 + parentflow-admin/public/file.svg | 1 + parentflow-admin/public/globe.svg | 1 + parentflow-admin/public/next.svg | 1 + parentflow-admin/public/vercel.svg | 1 + parentflow-admin/public/window.svg | 1 + parentflow-admin/src/app/favicon.ico | Bin 0 -> 25931 bytes parentflow-admin/src/app/globals.css | 26 + .../src/app/invite-codes/page.tsx | 393 ++ parentflow-admin/src/app/layout.tsx | 30 + parentflow-admin/src/app/login/page.tsx | 105 + parentflow-admin/src/app/page.tsx | 393 ++ .../src/components/AdminLayout.tsx | 196 + parentflow-admin/src/lib/api-client.ts | 183 + parentflow-admin/src/lib/theme.ts | 106 + parentflow-admin/tsconfig.json | 27 + start-production.sh | 353 +- stop-production.sh | 160 +- 37 files changed, 6767 insertions(+), 675 deletions(-) delete mode 100644 DEPLOY_NOW.md delete mode 100644 DEPLOY_PRODUCTION_SERVER.md create mode 100755 deploy-production.sh delete mode 100755 deploy-to-production.sh create mode 100644 maternal-app/maternal-app-backend/create-admin-user.js create mode 100644 maternal-app/maternal-app-backend/src/database/migrations/V028_create_invite_codes.sql create mode 100644 maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.controller.ts create mode 100644 maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.dto.ts create mode 100644 maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.entity.ts create mode 100644 maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.module.ts create mode 100644 maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.service.ts create mode 100755 migrate-production.sh create mode 100644 parentflow-admin/.gitignore create mode 100644 parentflow-admin/README.md create mode 100644 parentflow-admin/next.config.ts create mode 100644 parentflow-admin/package-lock.json create mode 100644 parentflow-admin/package.json create mode 100644 parentflow-admin/postcss.config.mjs create mode 100644 parentflow-admin/public/file.svg create mode 100644 parentflow-admin/public/globe.svg create mode 100644 parentflow-admin/public/next.svg create mode 100644 parentflow-admin/public/vercel.svg create mode 100644 parentflow-admin/public/window.svg create mode 100644 parentflow-admin/src/app/favicon.ico create mode 100644 parentflow-admin/src/app/globals.css create mode 100644 parentflow-admin/src/app/invite-codes/page.tsx create mode 100644 parentflow-admin/src/app/layout.tsx create mode 100644 parentflow-admin/src/app/login/page.tsx create mode 100644 parentflow-admin/src/app/page.tsx create mode 100644 parentflow-admin/src/components/AdminLayout.tsx create mode 100644 parentflow-admin/src/lib/api-client.ts create mode 100644 parentflow-admin/src/lib/theme.ts create mode 100644 parentflow-admin/tsconfig.json diff --git a/DEPLOY_NOW.md b/DEPLOY_NOW.md deleted file mode 100644 index d8b9d98..0000000 --- a/DEPLOY_NOW.md +++ /dev/null @@ -1,170 +0,0 @@ -# 🚀 Deploy ParentFlow to Production - Step by Step - -## Quick Deploy Instructions - -### Step 1: Connect to Production Server - -Open your terminal and connect via SSH: -```bash -ssh root@10.0.0.240 -# Password: a3pq5t50yA@# -``` - -### Step 2: Download and Run Deployment Script - -Once connected to the server, run these commands: - -```bash -# Download the deployment script -wget https://git.noru1.ro/andrei/maternal-app/raw/branch/main/deploy-to-production.sh - -# Make it executable -chmod +x deploy-to-production.sh - -# Run the deployment -./deploy-to-production.sh -``` - -### Step 3: Edit Environment Variables (First Time Only) - -The script will pause and ask you to edit `.env.production`. -You need to update these critical values: - -```bash -# Edit the file -nano /root/maternal-app/.env.production -``` - -Key values to update: -- `JWT_SECRET` - Generate with: `openssl rand -base64 64` -- `JWT_REFRESH_SECRET` - Generate with: `openssl rand -base64 64` -- `OPENAI_API_KEY` - Your OpenAI API key for AI features -- Redis, MongoDB, MinIO passwords (keep defaults or change) - -Press `Ctrl+X`, then `Y`, then `Enter` to save and exit. - -Then press `Enter` in the deployment script to continue. - -### Step 4: Verify Deployment - -After the script completes, verify everything is running: - -```bash -# Check PM2 processes -pm2 status - -# Check Docker containers -docker ps - -# Check if services are accessible -curl http://localhost:3020/api/health -curl http://localhost:3030 -``` - -## What the Script Does - -1. ✅ Clones the repository from Gitea -2. ✅ Installs Node.js 18, PM2, Docker, Docker Compose -3. ✅ Installs all npm dependencies -4. ✅ Builds backend and frontend -5. ✅ Starts Docker containers (Redis, MongoDB, MinIO) -6. ✅ Runs database migrations on 10.0.0.207 -7. ✅ Starts PM2 processes for backend (3020) and frontend (3030) -8. ✅ Sets up PM2 to restart on system reboot - -## After Deployment - -### Test the Application - -From your local machine: -1. Backend Health: http://10.0.0.240:3020/api/health -2. Frontend: http://10.0.0.240:3030 - -### Production URLs (Already configured in Nginx) - -- API: https://api.parentflowapp.com -- Web: https://web.parentflowapp.com - -### Management Commands - -On the production server: - -```bash -# View logs -pm2 logs - -# Restart services -pm2 restart all - -# Stop services -pm2 stop all - -# View real-time metrics -pm2 monit - -# Docker logs -docker logs parentflow-redis-prod -docker logs parentflow-mongodb-prod -``` - -## Troubleshooting - -### If backend doesn't start: -```bash -pm2 logs parentflow-backend-prod --err -cd /root/maternal-app/maternal-app/maternal-app-backend -npm run build -pm2 restart parentflow-backend-prod -``` - -### If frontend doesn't start: -```bash -pm2 logs parentflow-frontend-prod --err -cd /root/maternal-app/maternal-web -npm run build -pm2 restart parentflow-frontend-prod -``` - -### If database connection fails: -```bash -# Test connection -PGPASSWORD=a3ppq psql -h 10.0.0.207 -p 5432 -U postgres -d parentflow -c "SELECT version();" - -# Check migrations -cd /root/maternal-app/maternal-app/maternal-app-backend -./scripts/check-migrations.sh -``` - -### To update after changes: -```bash -cd /root/maternal-app -git pull origin main -cd maternal-app/maternal-app-backend -npm install -npm run build -cd ../../maternal-web -npm install -npm run build -pm2 restart all -``` - -## Success Indicators - -You'll know deployment is successful when: -- ✅ `pm2 status` shows both processes as "online" -- ✅ `docker ps` shows 3 containers running -- ✅ Backend responds at http://10.0.0.240:3020/api/health -- ✅ Frontend loads at http://10.0.0.240:3030 -- ✅ You can register/login at https://web.parentflowapp.com - -## Support - -If you encounter issues: -1. Check logs: `pm2 logs` -2. Check Docker: `docker ps` and `docker logs [container-name]` -3. Verify PostgreSQL connection to 10.0.0.207 -4. Ensure ports 3020 and 3030 are not blocked - ---- - -**Ready to deploy! Just SSH to the server and run the 3 commands in Step 2.** \ No newline at end of file diff --git a/DEPLOY_PRODUCTION_SERVER.md b/DEPLOY_PRODUCTION_SERVER.md deleted file mode 100644 index ce549ad..0000000 --- a/DEPLOY_PRODUCTION_SERVER.md +++ /dev/null @@ -1,76 +0,0 @@ -# Production Server Deployment Instructions - -## Quick Deploy on Server 10.0.0.240 - -Since the repository has already been cloned, run these commands: - -```bash -# Navigate to the project directory -cd /root/maternal-app - -# Switch to the main branch (where all the code is) -git checkout main -git pull origin main - -# Make deployment script executable and run it -chmod +x deploy-to-production.sh -./deploy-to-production.sh -``` - -## What the Deployment Script Does - -1. **Installs System Dependencies** - - Node.js 20.x - - PM2 process manager - - Docker and Docker Compose - - PostgreSQL client tools - -2. **Sets Up Database** - - Connects to PostgreSQL at 10.0.0.207 - - Creates parentflow database if needed - - Runs all migrations in sequence - -3. **Starts Services** - - Redis, MongoDB, MinIO via Docker Compose - - Backend API on port 3020 via PM2 - - Frontend on port 3005 via PM2 - -4. **Configures PM2** - - Sets up auto-restart on system reboot - - Saves PM2 configuration - -## Manual Commands if Needed - -### Check out the main branch: -```bash -git checkout main -git pull origin main -``` - -### Start all services: -```bash -./start-production.sh -``` - -### Stop all services: -```bash -./stop-production.sh -``` - -### Check PM2 status: -```bash -pm2 status -pm2 logs -``` - -### Check Docker services: -```bash -docker-compose -f docker-compose.production.yml ps -docker-compose -f docker-compose.production.yml logs -``` - -## Important Notes - -- **Database**: PostgreSQL is on dedicated server 10.0.0.207 (not in Docker) -- **Default branch issue**: The repository default branch is 'master' but all code is on 'main' -- **Always use main branch**: Make sure to checkout main branch after cloning \ No newline at end of file diff --git a/deploy-production.sh b/deploy-production.sh new file mode 100755 index 0000000..979c9f5 --- /dev/null +++ b/deploy-production.sh @@ -0,0 +1,331 @@ +#!/bin/bash + +# ParentFlow Production Deployment Pipeline +# This script deploys the complete ParentFlow application suite to production +# Run on production server 10.0.0.240 as root + +set -e + +# Configuration +REPO_URL="https://andrei:33edc%40%40NHY%5E%5E@git.noru1.ro/andrei/maternal-app.git" +DEPLOY_DIR="/root/parentflow-production" +DB_HOST="10.0.0.207" +DB_PORT="5432" +DB_USER="postgres" +DB_PASSWORD="a3ppq" +DB_NAME="parentflow" +DB_NAME_ADMIN="parentflowadmin" + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Logging function +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 + exit 1 +} + +success() { + echo -e "${GREEN}✓${NC} $1" +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# Header +echo "" +echo "============================================" +echo " ParentFlow Production Deployment v2.0 " +echo "============================================" +echo "" + +# Step 1: Install Node.js 22 +log "${CYAN}Step 1: Installing Node.js 22...${NC}" +if ! command -v node &> /dev/null || [[ $(node -v | cut -d'v' -f2 | cut -d'.' -f1) -lt 22 ]]; then + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + success "Node.js $(node -v) installed" +else + success "Node.js $(node -v) already installed" +fi + +# Step 2: Install PM2 globally +log "${CYAN}Step 2: Installing PM2...${NC}" +if ! command -v pm2 &> /dev/null; then + npm install -g pm2@latest + success "PM2 installed" +else + pm2 update + success "PM2 updated" +fi + +# Step 3: Install Docker and Docker Compose +log "${CYAN}Step 3: Checking Docker installation...${NC}" +if ! command -v docker &> /dev/null; then + curl -fsSL https://get.docker.com | sh + success "Docker installed" +else + success "Docker already installed" +fi + +if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then + curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + success "Docker Compose installed" +else + success "Docker Compose already installed" +fi + +# Step 4: Install PostgreSQL client +log "${CYAN}Step 4: Installing PostgreSQL client...${NC}" +if ! command -v psql &> /dev/null; then + apt-get update + apt-get install -y postgresql-client + success "PostgreSQL client installed" +else + success "PostgreSQL client already installed" +fi + +# Step 5: Clone or update repository +log "${CYAN}Step 5: Fetching latest code from main branch...${NC}" +if [ -d "$DEPLOY_DIR" ]; then + warning "Deployment directory exists, pulling latest changes..." + cd "$DEPLOY_DIR" + git fetch origin main + git reset --hard origin/main + git clean -fd +else + log "Cloning repository..." + git clone -b main "$REPO_URL" "$DEPLOY_DIR" + cd "$DEPLOY_DIR" +fi +success "Repository updated to latest main branch" + +# Step 6: Stop existing services +log "${CYAN}Step 6: Stopping existing services...${NC}" +if [ -f "./stop-production.sh" ]; then + ./stop-production.sh || warning "No services were running" +else + warning "Stop script not found, continuing..." +fi + +# Step 7: Install dependencies +log "${CYAN}Step 7: Installing application dependencies...${NC}" + +# Backend +log "Installing backend dependencies..." +cd "$DEPLOY_DIR/maternal-app/maternal-app-backend" +rm -rf node_modules package-lock.json +npm install --production=false +npm update +success "Backend dependencies installed" + +# Frontend +log "Installing frontend dependencies..." +cd "$DEPLOY_DIR/maternal-web" +rm -rf node_modules package-lock.json .next +npm install --production=false +npm update +success "Frontend dependencies installed" + +# Admin Dashboard +log "Installing admin dashboard dependencies..." +cd "$DEPLOY_DIR/parentflow-admin" +rm -rf node_modules package-lock.json .next +npm install --production=false +npm update +success "Admin dashboard dependencies installed" + +# Step 8: Set up environment files +log "${CYAN}Step 8: Configuring environment...${NC}" +cd "$DEPLOY_DIR" + +# Backend .env +cat > "$DEPLOY_DIR/maternal-app/maternal-app-backend/.env.production" << EOF +# Production Environment Configuration +NODE_ENV=production +API_PORT=3020 +API_URL=https://api.parentflowapp.com + +# Database Configuration +DATABASE_HOST=${DB_HOST} +DATABASE_PORT=${DB_PORT} +DATABASE_NAME=${DB_NAME} +DATABASE_USER=${DB_USER} +DATABASE_PASSWORD=${DB_PASSWORD} +DATABASE_SSL=true + +# Redis Configuration +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_URL=redis://localhost:6379 + +# MongoDB Configuration +MONGODB_URI=mongodb://localhost:27017/parentflow_production + +# MinIO Configuration +MINIO_ENDPOINT=localhost +MINIO_PORT=9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=parentflow_minio_prod_2024 +MINIO_BUCKET=parentflow-files +MINIO_USE_SSL=false + +# JWT Configuration +JWT_SECRET=parentflow_jwt_secret_production_2024_secure +JWT_EXPIRATION=1h +JWT_REFRESH_SECRET=parentflow_refresh_secret_production_2024_secure +JWT_REFRESH_EXPIRATION=7d + +# AI Services (copy from development .env) +AI_PROVIDER=azure +AZURE_OPENAI_ENABLED=true +AZURE_OPENAI_CHAT_ENDPOINT=https://footprints-open-ai.openai.azure.com +AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-5-mini +AZURE_OPENAI_CHAT_API_VERSION=2025-04-01-preview +AZURE_OPENAI_CHAT_API_KEY=a5f7e3e70a454a399f9216853b45e18b +AZURE_OPENAI_CHAT_MAX_TOKENS=1000 +AZURE_OPENAI_REASONING_EFFORT=medium +AZURE_OPENAI_WHISPER_ENDPOINT=https://footprints-ai.openai.azure.com +AZURE_OPENAI_WHISPER_DEPLOYMENT=whisper +AZURE_OPENAI_WHISPER_API_VERSION=2024-06-01 +AZURE_OPENAI_WHISPER_API_KEY=42702a67a41547919877a2ab8e4837f9 +AZURE_OPENAI_EMBEDDINGS_ENDPOINT=https://footprints-ai.openai.azure.com +AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=Text-Embedding-ada-002-V2 +AZURE_OPENAI_EMBEDDINGS_API_VERSION=2023-05-15 +AZURE_OPENAI_EMBEDDINGS_API_KEY=42702a67a41547919877a2ab8e4837f9 + +# CORS Configuration +CORS_ORIGIN=https://web.parentflowapp.com,https://admin.parentflowapp.com + +# Rate Limiting +RATE_LIMIT_TTL=60 +RATE_LIMIT_MAX=100 + +# Email Service +MAILGUN_API_KEY= +MAILGUN_DOMAIN= +EMAIL_FROM=noreply@parentflowapp.com +EMAIL_FROM_NAME=ParentFlow + +# Error Tracking +SENTRY_ENABLED=false +SENTRY_DSN= +EOF + +# Frontend .env.production +cat > "$DEPLOY_DIR/maternal-web/.env.production" << EOF +# Frontend Production Configuration +NEXT_PUBLIC_API_URL=https://api.parentflowapp.com/api/v1 +NEXT_PUBLIC_GRAPHQL_URL=https://api.parentflowapp.com/graphql +NEXT_PUBLIC_WS_URL=wss://api.parentflowapp.com +NEXT_PUBLIC_APP_URL=https://web.parentflowapp.com +NEXT_PUBLIC_APP_NAME=ParentFlow +NEXT_PUBLIC_ENABLE_PWA=true +NEXT_PUBLIC_ENABLE_ANALYTICS=true +EOF + +# Admin Dashboard .env.production +cat > "$DEPLOY_DIR/parentflow-admin/.env.production" << EOF +# Admin Dashboard Production Configuration +NEXT_PUBLIC_API_URL=https://api.parentflowapp.com/api/v1 +NEXT_PUBLIC_APP_URL=https://adminpf.parentflowapp.com +NEXT_PUBLIC_APP_NAME=ParentFlow Admin +EOF + +success "Environment files configured" + +# Step 9: Run database migrations +log "${CYAN}Step 9: Running database migrations...${NC}" +cd "$DEPLOY_DIR" +./migrate-production.sh || error "Database migration failed" +success "Database migrations completed" + +# Step 10: Build applications +log "${CYAN}Step 10: Building applications for production...${NC}" + +# Build backend +log "Building backend..." +cd "$DEPLOY_DIR/maternal-app/maternal-app-backend" +NODE_ENV=production npm run build +success "Backend built" + +# Build frontend +log "Building frontend..." +cd "$DEPLOY_DIR/maternal-web" +NODE_ENV=production npm run build +success "Frontend built" + +# Build admin dashboard +log "Building admin dashboard..." +cd "$DEPLOY_DIR/parentflow-admin" +NODE_ENV=production npm run build +success "Admin dashboard built" + +# Step 11: Start Docker services +log "${CYAN}Step 11: Starting Docker services...${NC}" +cd "$DEPLOY_DIR" +if docker compose version &> /dev/null; then + docker compose -f docker-compose.production.yml up -d +else + docker-compose -f docker-compose.production.yml up -d +fi +success "Docker services started" + +# Step 12: Start application services +log "${CYAN}Step 12: Starting application services...${NC}" +cd "$DEPLOY_DIR" +./start-production.sh || error "Failed to start services" +success "Application services started" + +# Step 13: Verify deployment +log "${CYAN}Step 13: Verifying deployment...${NC}" +sleep 10 + +verify_service() { + local service=$1 + local port=$2 + if lsof -i:$port > /dev/null 2>&1; then + success "$service is running on port $port" + else + error "$service is not running on port $port" + fi +} + +verify_service "Backend API" 3020 +verify_service "Frontend" 3030 +verify_service "Admin Dashboard" 3335 + +# Final summary +echo "" +echo "============================================" +echo -e "${GREEN} Deployment Completed Successfully! ${NC}" +echo "============================================" +echo "" +echo "Services running at:" +echo " Backend API: http://10.0.0.240:3020" +echo " Frontend: http://10.0.0.240:3030" +echo " Admin Dashboard: http://10.0.0.240:3335" +echo "" +echo "Configure your nginx proxy to route:" +echo " api.parentflowapp.com -> 10.0.0.240:3020" +echo " web.parentflowapp.com -> 10.0.0.240:3030" +echo " adminpf.parentflowapp.com -> 10.0.0.240:3335" +echo "" +echo "Management commands:" +echo " Start services: ./start-production.sh" +echo " Stop services: ./stop-production.sh" +echo " View logs: pm2 logs" +echo " Monitor: pm2 monit" +echo "" +log "Deployment completed at $(date)" \ No newline at end of file diff --git a/deploy-to-production.sh b/deploy-to-production.sh deleted file mode 100755 index e688063..0000000 --- a/deploy-to-production.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/bin/bash - -# ParentFlow Production Deployment Script -# Run this on server 10.0.0.240 as root - -set -e - -# Color codes -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo "==========================================" -echo "ParentFlow Production Deployment" -echo "==========================================" -echo "" - -# Step 1: Check if already deployed -if [ -d "/root/maternal-app" ]; then - echo -e "${YELLOW}Repository already exists. Pulling latest changes...${NC}" - cd /root/maternal-app - git pull origin main -else - echo -e "${BLUE}Step 1: Cloning repository...${NC}" - cd /root - git clone https://git.noru1.ro/andrei/maternal-app.git - cd maternal-app -fi - -echo -e "${GREEN}✓ Repository ready${NC}" - -# Step 2: Install Node.js if not present -echo "" -echo -e "${BLUE}Step 2: Checking Node.js installation...${NC}" -if ! command -v node &> /dev/null; then - echo -e "${YELLOW}Installing Node.js 20...${NC}" - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - apt-get install -y nodejs -fi -echo -e "${GREEN}✓ Node.js $(node -v)${NC}" - -# Step 3: Install PM2 globally if not present -echo "" -echo -e "${BLUE}Step 3: Checking PM2 installation...${NC}" -if ! command -v pm2 &> /dev/null; then - echo -e "${YELLOW}Installing PM2...${NC}" - npm install -g pm2 -fi -echo -e "${GREEN}✓ PM2 installed${NC}" - -# Step 4: Install Docker and Docker Compose if not present -echo "" -echo -e "${BLUE}Step 4: Checking Docker installation...${NC}" -if ! command -v docker &> /dev/null; then - echo -e "${YELLOW}Installing Docker...${NC}" - curl -fsSL https://get.docker.com | sh -fi -echo -e "${GREEN}✓ Docker installed${NC}" - -if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then - echo -e "${YELLOW}Installing Docker Compose...${NC}" - curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose -fi -echo -e "${GREEN}✓ Docker Compose installed${NC}" - -# Step 5: Install PostgreSQL client for migrations -echo "" -echo -e "${BLUE}Step 5: Installing PostgreSQL client...${NC}" -apt-get update -apt-get install -y postgresql-client -echo -e "${GREEN}✓ PostgreSQL client installed${NC}" - -# Step 6: Install dependencies -echo "" -echo -e "${BLUE}Step 6: Installing application dependencies...${NC}" - -echo -e "${YELLOW}Installing backend dependencies...${NC}" -cd /root/maternal-app/maternal-app/maternal-app-backend -npm install - -echo -e "${YELLOW}Installing frontend dependencies...${NC}" -cd /root/maternal-app/maternal-web -npm install - -echo -e "${GREEN}✓ Dependencies installed${NC}" - -# Step 7: Build applications -echo "" -echo -e "${BLUE}Step 7: Building applications...${NC}" - -echo -e "${YELLOW}Building backend...${NC}" -cd /root/maternal-app/maternal-app/maternal-app-backend -npm run build - -echo -e "${YELLOW}Building frontend...${NC}" -cd /root/maternal-app/maternal-web -npm run build - -echo -e "${GREEN}✓ Applications built${NC}" - -# Step 8: Create .env.production if it doesn't exist -echo "" -echo -e "${BLUE}Step 8: Setting up environment variables...${NC}" -cd /root/maternal-app -if [ ! -f ".env.production" ]; then - cp .env.production.example .env.production - echo -e "${YELLOW}Created .env.production from example${NC}" - echo -e "${RED}IMPORTANT: Edit .env.production with your actual values!${NC}" - echo "Press Enter to continue after editing .env.production..." - read -fi - -# Step 9: Start Docker services -echo "" -echo -e "${BLUE}Step 9: Starting Docker services...${NC}" -cd /root/maternal-app -if docker compose version &> /dev/null; then - docker compose -f docker-compose.production.yml up -d -else - docker-compose -f docker-compose.production.yml up -d -fi -echo -e "${GREEN}✓ Docker services started${NC}" - -# Step 10: Run database migrations -echo "" -echo -e "${BLUE}Step 10: Running database migrations...${NC}" -cd /root/maternal-app/maternal-app/maternal-app-backend -chmod +x scripts/master-migration.sh scripts/check-migrations.sh - -echo -e "${YELLOW}Testing database connection...${NC}" -PGPASSWORD=a3ppq psql -h 10.0.0.207 -p 5432 -U postgres -d parentflow -c "SELECT version();" > /dev/null 2>&1 -if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ Database connection successful${NC}" - - DATABASE_HOST=10.0.0.207 \ - DATABASE_PORT=5432 \ - DATABASE_NAME=parentflow \ - DATABASE_USER=postgres \ - DATABASE_PASSWORD=a3ppq \ - ./scripts/master-migration.sh -else - echo -e "${RED}✗ Cannot connect to database${NC}" - echo "Please check database server connectivity" -fi - -# Step 11: Start PM2 processes -echo "" -echo -e "${BLUE}Step 11: Starting PM2 processes...${NC}" -cd /root/maternal-app -pm2 delete all 2>/dev/null || true -pm2 start ecosystem.config.js --env production -pm2 save -pm2 startup - -echo -e "${GREEN}✓ PM2 processes started${NC}" - -# Step 12: Verify deployment -echo "" -echo -e "${BLUE}Step 12: Verifying deployment...${NC}" -sleep 5 - -# Check if services are running -if lsof -i:3020 > /dev/null 2>&1; then - echo -e "${GREEN}✓ Backend is running on port 3020${NC}" -else - echo -e "${RED}✗ Backend not detected on port 3020${NC}" -fi - -if lsof -i:3030 > /dev/null 2>&1; then - echo -e "${GREEN}✓ Frontend is running on port 3030${NC}" -else - echo -e "${RED}✗ Frontend not detected on port 3030${NC}" -fi - -# Show status -echo "" -echo "==========================================" -echo -e "${GREEN}Deployment Complete!${NC}" -echo "==========================================" -echo "" -echo "Services:" -echo " Backend API: http://10.0.0.240:3020" -echo " Frontend: http://10.0.0.240:3030" -echo "" -echo "Public URLs (configure DNS/Nginx):" -echo " API: https://api.parentflowapp.com → 10.0.0.240:3020" -echo " Web: https://web.parentflowapp.com → 10.0.0.240:3030" -echo "" -echo "Management:" -echo " PM2 status: pm2 status" -echo " PM2 logs: pm2 logs" -echo " Docker status: docker ps" -echo "" -echo -e "${GREEN}✓ Production deployment successful!${NC}" \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js index fcdaddb..4902799 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -94,5 +94,41 @@ module.exports = { wait_ready: true, listen_timeout: 30000 }, + { + name: 'parentflow-admin-prod', + cwd: './parentflow-admin', + script: 'node_modules/next/dist/bin/next', + args: 'start', + instances: 1, + exec_mode: 'fork', + autorestart: true, + watch: false, + max_memory_restart: '300M', + env: { + NODE_ENV: 'production', + PORT: 3335, + HOSTNAME: '0.0.0.0', + NEXT_PUBLIC_API_URL: 'https://api.parentflowapp.com/api/v1', + NEXT_PUBLIC_APP_URL: 'https://adminpf.parentflowapp.com', + NEXT_PUBLIC_APP_NAME: 'ParentFlow Admin', + }, + env_production: { + NODE_ENV: 'production', + PORT: 3335, + HOSTNAME: '0.0.0.0', + NEXT_PUBLIC_API_URL: 'https://api.parentflowapp.com/api/v1', + NEXT_PUBLIC_APP_URL: 'https://adminpf.parentflowapp.com', + NEXT_PUBLIC_APP_NAME: 'ParentFlow Admin', + }, + error_file: './logs/admin-error.log', + out_file: './logs/admin-out.log', + log_file: './logs/admin-combined.log', + time: true, + merge_logs: true, + node_args: '--max-old-space-size=300', + kill_timeout: 5000, + wait_ready: true, + listen_timeout: 30000 + }, ], }; diff --git a/maternal-app/maternal-app-backend/create-admin-user.js b/maternal-app/maternal-app-backend/create-admin-user.js new file mode 100644 index 0000000..6e556d2 --- /dev/null +++ b/maternal-app/maternal-app-backend/create-admin-user.js @@ -0,0 +1,32 @@ +const bcrypt = require('bcrypt'); + +async function createAdminUser() { + // Password to hash + const password = 'admin123'; + + // Generate salt and hash + const saltRounds = 10; + const hash = await bcrypt.hash(password, saltRounds); + + console.log('Password:', password); + console.log('Hash:', hash); + + // SQL command + const sql = ` +INSERT INTO admin_users (email, password_hash, name, role) +VALUES ( + 'admin@parentflowapp.com', + '${hash}', + 'System Administrator', + 'super_admin' +) ON CONFLICT (email) +DO UPDATE SET + password_hash = '${hash}', + updated_at = CURRENT_TIMESTAMP; + `; + + console.log('\nSQL Command:'); + console.log(sql); +} + +createAdminUser().catch(console.error); \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/app.module.ts b/maternal-app/maternal-app-backend/src/app.module.ts index 0b1b8ea..25b167f 100644 --- a/maternal-app/maternal-app-backend/src/app.module.ts +++ b/maternal-app/maternal-app-backend/src/app.module.ts @@ -22,6 +22,7 @@ import { AnalyticsModule } from './modules/analytics/analytics.module'; import { FeedbackModule } from './modules/feedback/feedback.module'; import { PhotosModule } from './modules/photos/photos.module'; import { ComplianceModule } from './modules/compliance/compliance.module'; +import { InviteCodesModule } from './modules/invite-codes/invite-codes.module'; import { GraphQLCustomModule } from './graphql/graphql.module'; import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'; import { ErrorTrackingService } from './common/services/error-tracking.service'; @@ -72,6 +73,7 @@ import { HealthController } from './common/controllers/health.controller'; FeedbackModule, PhotosModule, ComplianceModule, + InviteCodesModule, GraphQLCustomModule, ], controllers: [AppController, HealthController], diff --git a/maternal-app/maternal-app-backend/src/database/migrations/V028_create_invite_codes.sql b/maternal-app/maternal-app-backend/src/database/migrations/V028_create_invite_codes.sql new file mode 100644 index 0000000..729ae02 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/database/migrations/V028_create_invite_codes.sql @@ -0,0 +1,189 @@ +-- V028: Create invite codes system for controlled registration +-- Purpose: Implement invite-based registration to control app access during beta/launch + +-- Create invite codes table +CREATE TABLE IF NOT EXISTS invite_codes ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + code VARCHAR(20) NOT NULL UNIQUE, + created_by VARCHAR(36) REFERENCES users(id), + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Usage tracking + uses INTEGER DEFAULT 0, + max_uses INTEGER, -- NULL means unlimited + + -- Validity + expires_at TIMESTAMP WITH TIME ZONE, + is_active BOOLEAN DEFAULT true, + + -- Metadata + description TEXT, + metadata JSONB DEFAULT '{}', + + -- Constraints + CONSTRAINT check_uses CHECK (uses >= 0), + CONSTRAINT check_max_uses CHECK (max_uses IS NULL OR max_uses > 0), + CONSTRAINT check_code_format CHECK (code ~ '^[A-Z0-9\-]+$') +); + +-- Create index for fast code lookup +CREATE INDEX idx_invite_codes_code ON invite_codes(code) WHERE is_active = true; +CREATE INDEX idx_invite_codes_expires ON invite_codes(expires_at) WHERE expires_at IS NOT NULL; +CREATE INDEX idx_invite_codes_created_by ON invite_codes(created_by); + +-- Create invite code usage tracking table +CREATE TABLE IF NOT EXISTS invite_code_uses ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + invite_code_id VARCHAR(36) REFERENCES invite_codes(id) ON DELETE CASCADE, + used_by VARCHAR(36) REFERENCES users(id), + used_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + user_email VARCHAR(255), + user_ip VARCHAR(45), + user_agent TEXT, + + -- Ensure one use per user + UNIQUE(invite_code_id, used_by) +); + +CREATE INDEX idx_invite_code_uses_code ON invite_code_uses(invite_code_id); +CREATE INDEX idx_invite_code_uses_user ON invite_code_uses(used_by); + +-- Create admin users table for admin dashboard access +CREATE TABLE IF NOT EXISTS admin_users ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(100), + role VARCHAR(50) DEFAULT 'admin', + + -- Status + is_active BOOLEAN DEFAULT true, + last_login_at TIMESTAMP WITH TIME ZONE, + + -- Timestamps + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Permissions (JSON array of permission strings) + permissions JSONB DEFAULT '["users:read", "users:write", "invites:read", "invites:write", "analytics:read"]', + + -- Two-factor auth + two_factor_secret VARCHAR(255), + two_factor_enabled BOOLEAN DEFAULT false +); + +CREATE INDEX idx_admin_users_email ON admin_users(email); + +-- Create admin sessions table +CREATE TABLE IF NOT EXISTS admin_sessions ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + admin_user_id VARCHAR(36) REFERENCES admin_users(id) ON DELETE CASCADE, + token_hash VARCHAR(255) NOT NULL UNIQUE, + ip_address VARCHAR(45), + user_agent TEXT, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_admin_sessions_token ON admin_sessions(token_hash); +CREATE INDEX idx_admin_sessions_admin ON admin_sessions(admin_user_id); +CREATE INDEX idx_admin_sessions_expires ON admin_sessions(expires_at); + +-- Create audit log for admin actions +CREATE TABLE IF NOT EXISTS admin_audit_logs ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + admin_user_id VARCHAR(36) REFERENCES admin_users(id), + action VARCHAR(100) NOT NULL, + entity_type VARCHAR(50), + entity_id VARCHAR(36), + details JSONB DEFAULT '{}', + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_admin_audit_logs_admin ON admin_audit_logs(admin_user_id); +CREATE INDEX idx_admin_audit_logs_entity ON admin_audit_logs(entity_type, entity_id); +CREATE INDEX idx_admin_audit_logs_created ON admin_audit_logs(created_at); + +-- Function to increment invite code uses +CREATE OR REPLACE FUNCTION increment_invite_code_uses() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE invite_codes + SET uses = uses + 1 + WHERE id = NEW.invite_code_id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to automatically increment uses +CREATE TRIGGER trigger_increment_invite_uses +AFTER INSERT ON invite_code_uses +FOR EACH ROW +EXECUTE FUNCTION increment_invite_code_uses(); + +-- Function to check if invite code is valid +CREATE OR REPLACE FUNCTION is_invite_code_valid( + p_code VARCHAR(20) +) RETURNS TABLE ( + is_valid BOOLEAN, + reason VARCHAR(100) +) AS $$ +DECLARE + v_invite RECORD; +BEGIN + -- Find the invite code + SELECT * INTO v_invite + FROM invite_codes + WHERE code = p_code AND is_active = true; + + -- Check if code exists + IF NOT FOUND THEN + RETURN QUERY SELECT false, 'Invalid invite code'; + RETURN; + END IF; + + -- Check if expired + IF v_invite.expires_at IS NOT NULL AND v_invite.expires_at < CURRENT_TIMESTAMP THEN + RETURN QUERY SELECT false, 'Invite code has expired'; + RETURN; + END IF; + + -- Check if max uses reached + IF v_invite.max_uses IS NOT NULL AND v_invite.uses >= v_invite.max_uses THEN + RETURN QUERY SELECT false, 'Invite code has reached maximum uses'; + RETURN; + END IF; + + -- Code is valid + RETURN QUERY SELECT true, 'Valid'; +END; +$$ LANGUAGE plpgsql; + +-- Insert default admin user (password: admin123 - CHANGE THIS!) +-- Password hash is for 'admin123' using bcrypt +INSERT INTO admin_users (email, password_hash, name, role) +VALUES ( + 'admin@parentflowapp.com', + '$2b$10$YourHashHere', -- Replace with actual bcrypt hash + 'System Administrator', + 'super_admin' +) ON CONFLICT (email) DO NOTHING; + +-- Insert some sample invite codes for testing +INSERT INTO invite_codes (code, description, max_uses) VALUES + ('BETA2024', 'Beta testing invite', 100), + ('LAUNCH50', 'Launch promotion - 50% off', 50), + ('FRIENDS', 'Friends and family', NULL) +ON CONFLICT (code) DO NOTHING; + +-- Add invite_code_used column to users table to track which code was used +ALTER TABLE users ADD COLUMN IF NOT EXISTS invite_code_used VARCHAR(20); +ALTER TABLE users ADD COLUMN IF NOT EXISTS registered_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP; + +-- Create index for analytics +CREATE INDEX IF NOT EXISTS idx_users_invite_code ON users(invite_code_used); +CREATE INDEX IF NOT EXISTS idx_users_registered_at ON users(registered_at); \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.controller.ts b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.controller.ts new file mode 100644 index 0000000..27a35fc --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.controller.ts @@ -0,0 +1,80 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { InviteCodesService } from './invite-codes.service'; +import { CreateInviteCodeDto, UpdateInviteCodeDto } from './invite-codes.dto'; + +@Controller('api/v1/admin/invite-codes') +export class InviteCodesController { + constructor(private readonly inviteCodesService: InviteCodesService) {} + + @Post() + async create(@Body() dto: CreateInviteCodeDto, @Request() req: any) { + // For now, we'll pass undefined for createdBy until we implement admin auth + return this.inviteCodesService.create(dto, req.user?.id); + } + + @Get() + async findAll( + @Query('page') page?: string, + @Query('limit') limit?: string, + @Query('status') status?: string, + ) { + return this.inviteCodesService.findAll({ + page: page ? parseInt(page, 10) : undefined, + limit: limit ? parseInt(limit, 10) : undefined, + status, + }); + } + + @Get('generate') + async generateCode() { + const code = await this.inviteCodesService.generateRandomCode(); + return { code }; + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.inviteCodesService.findOne(id); + } + + @Get(':id/stats') + async getStats(@Param('id') id: string) { + return this.inviteCodesService.getUsageStats(id); + } + + @Put(':id') + async update(@Param('id') id: string, @Body() dto: UpdateInviteCodeDto) { + return this.inviteCodesService.update(id, dto); + } + + @Delete(':id') + async remove(@Param('id') id: string) { + await this.inviteCodesService.delete(id); + return { message: 'Invite code deleted successfully' }; + } +} + +// Public controller for validating invite codes during registration +@Controller('api/v1/invite-codes') +export class PublicInviteCodesController { + constructor(private readonly inviteCodesService: InviteCodesService) {} + + @Post('validate') + async validate(@Body('code') code: string) { + const result = await this.inviteCodesService.validateCode(code); + return { + isValid: result.isValid, + reason: result.reason, + }; + } +} \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.dto.ts b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.dto.ts new file mode 100644 index 0000000..28864cc --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.dto.ts @@ -0,0 +1,57 @@ +import { IsString, IsOptional, IsNumber, IsBoolean, IsDateString, IsJSON, Matches, Min } from 'class-validator'; + +export class CreateInviteCodeDto { + @IsString() + @Matches(/^[A-Z0-9\-]+$/, { message: 'Code must contain only uppercase letters, numbers, and hyphens' }) + code: string; + + @IsOptional() + @IsNumber() + @Min(1) + maxUses?: number; + + @IsOptional() + @IsDateString() + expiresAt?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsJSON() + metadata?: any; +} + +export class UpdateInviteCodeDto { + @IsOptional() + @IsString() + @Matches(/^[A-Z0-9\-]+$/, { message: 'Code must contain only uppercase letters, numbers, and hyphens' }) + code?: string; + + @IsOptional() + @IsNumber() + @Min(1) + maxUses?: number; + + @IsOptional() + @IsDateString() + expiresAt?: string; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsJSON() + metadata?: any; +} + +export class UseInviteCodeDto { + @IsString() + code: string; +} \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.entity.ts b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.entity.ts new file mode 100644 index 0000000..2b86659 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.entity.ts @@ -0,0 +1,113 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm'; +import { User } from '../../database/entities/user.entity'; + +@Entity('invite_codes') +export class InviteCode { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true, length: 20 }) + code: string; + + @Column({ name: 'created_by', nullable: true }) + createdBy: string; + + @ManyToOne(() => User, { nullable: true }) + creator?: User; + + @Column({ default: 0 }) + uses: number; + + @Column({ name: 'max_uses', nullable: true }) + maxUses: number | null; + + @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) + expiresAt: Date | null; + + @Column({ name: 'is_active', default: true }) + isActive: boolean; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ type: 'jsonb', default: {} }) + metadata: any; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; + + @OneToMany(() => InviteCodeUse, use => use.inviteCode) + usages: InviteCodeUse[]; +} + +@Entity('invite_code_uses') +export class InviteCodeUse { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'invite_code_id' }) + inviteCodeId: string; + + @ManyToOne(() => InviteCode, code => code.usages) + inviteCode: InviteCode; + + @Column({ name: 'used_by' }) + usedBy: string; + + @ManyToOne(() => User) + user: User; + + @Column({ name: 'user_email' }) + userEmail: string; + + @Column({ name: 'user_ip', nullable: true }) + userIp: string; + + @Column({ name: 'user_agent', type: 'text', nullable: true }) + userAgent: string; + + @CreateDateColumn({ name: 'used_at' }) + usedAt: Date; +} + +@Entity('admin_users') +export class AdminUser { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + email: string; + + @Column({ name: 'password_hash' }) + passwordHash: string; + + @Column({ nullable: true }) + name: string; + + @Column({ default: 'admin' }) + role: string; + + @Column({ name: 'is_active', default: true }) + isActive: boolean; + + @Column({ name: 'last_login_at', type: 'timestamptz', nullable: true }) + lastLoginAt: Date; + + @Column({ type: 'jsonb', default: [] }) + permissions: string[]; + + @Column({ name: 'two_factor_secret', nullable: true }) + twoFactorSecret: string; + + @Column({ name: 'two_factor_enabled', default: false }) + twoFactorEnabled: boolean; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.module.ts b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.module.ts new file mode 100644 index 0000000..f564caa --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { InviteCode, InviteCodeUse, AdminUser } from './invite-codes.entity'; +import { InviteCodesService } from './invite-codes.service'; +import { InviteCodesController, PublicInviteCodesController } from './invite-codes.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([InviteCode, InviteCodeUse, AdminUser])], + controllers: [InviteCodesController, PublicInviteCodesController], + providers: [InviteCodesService], + exports: [InviteCodesService], +}) +export class InviteCodesModule {} \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.service.ts b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.service.ts new file mode 100644 index 0000000..6580988 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/invite-codes/invite-codes.service.ts @@ -0,0 +1,204 @@ +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { InviteCode, InviteCodeUse } from './invite-codes.entity'; +import { CreateInviteCodeDto, UpdateInviteCodeDto } from './invite-codes.dto'; + +@Injectable() +export class InviteCodesService { + constructor( + @InjectRepository(InviteCode) + private inviteCodeRepository: Repository, + @InjectRepository(InviteCodeUse) + private inviteCodeUseRepository: Repository, + ) {} + + async create(dto: CreateInviteCodeDto, createdBy?: string): Promise { + // Validate code format + if (!/^[A-Z0-9\-]+$/.test(dto.code)) { + throw new BadRequestException('Code must contain only uppercase letters, numbers, and hyphens'); + } + + // Check if code already exists + const existing = await this.inviteCodeRepository.findOne({ where: { code: dto.code } }); + if (existing) { + throw new BadRequestException('Invite code already exists'); + } + + const inviteCode = this.inviteCodeRepository.create({ + ...dto, + createdBy, + }); + + return this.inviteCodeRepository.save(inviteCode); + } + + async findAll(params?: { + page?: number; + limit?: number; + status?: string + }): Promise<{ codes: InviteCode[]; total: number }> { + const query = this.inviteCodeRepository.createQueryBuilder('invite_code'); + + if (params?.status === 'active') { + query.where('invite_code.is_active = :isActive', { isActive: true }) + .andWhere('(invite_code.expires_at IS NULL OR invite_code.expires_at > NOW())') + .andWhere('(invite_code.max_uses IS NULL OR invite_code.uses < invite_code.max_uses)'); + } else if (params?.status === 'inactive') { + query.where('invite_code.is_active = :isActive', { isActive: false }); + } else if (params?.status === 'expired') { + query.where('invite_code.expires_at <= NOW()'); + } else if (params?.status === 'exhausted') { + query.where('invite_code.max_uses IS NOT NULL AND invite_code.uses >= invite_code.max_uses'); + } + + const page = params?.page || 1; + const limit = params?.limit || 10; + const skip = (page - 1) * limit; + + query.orderBy('invite_code.created_at', 'DESC') + .skip(skip) + .take(limit); + + const [codes, total] = await query.getManyAndCount(); + + return { codes, total }; + } + + async findOne(id: string): Promise { + const inviteCode = await this.inviteCodeRepository.findOne({ + where: { id }, + relations: ['usages'], + }); + + if (!inviteCode) { + throw new NotFoundException('Invite code not found'); + } + + return inviteCode; + } + + async findByCode(code: string): Promise { + const inviteCode = await this.inviteCodeRepository.findOne({ + where: { code }, + }); + + if (!inviteCode) { + throw new NotFoundException('Invite code not found'); + } + + return inviteCode; + } + + async update(id: string, dto: UpdateInviteCodeDto): Promise { + const inviteCode = await this.findOne(id); + + if (dto.code && dto.code !== inviteCode.code) { + // Check if new code already exists + const existing = await this.inviteCodeRepository.findOne({ where: { code: dto.code } }); + if (existing) { + throw new BadRequestException('Invite code already exists'); + } + } + + Object.assign(inviteCode, dto); + return this.inviteCodeRepository.save(inviteCode); + } + + async delete(id: string): Promise { + const inviteCode = await this.findOne(id); + await this.inviteCodeRepository.remove(inviteCode); + } + + async validateCode(code: string): Promise<{ + isValid: boolean; + reason?: string; + inviteCode?: InviteCode; + }> { + const inviteCode = await this.inviteCodeRepository.findOne({ + where: { code, isActive: true }, + }); + + if (!inviteCode) { + return { isValid: false, reason: 'Invalid invite code' }; + } + + // Check if expired + if (inviteCode.expiresAt && inviteCode.expiresAt < new Date()) { + return { isValid: false, reason: 'Invite code has expired' }; + } + + // Check if max uses reached + if (inviteCode.maxUses && inviteCode.uses >= inviteCode.maxUses) { + return { isValid: false, reason: 'Invite code has reached maximum uses' }; + } + + return { isValid: true, inviteCode }; + } + + async useCode(code: string, userId: string, userEmail: string, ipAddress?: string, userAgent?: string): Promise { + const validation = await this.validateCode(code); + + if (!validation.isValid || !validation.inviteCode) { + throw new BadRequestException(validation.reason || 'Invalid invite code'); + } + + // Check if user already used this code + const existingUse = await this.inviteCodeUseRepository.findOne({ + where: { + inviteCodeId: validation.inviteCode.id, + usedBy: userId, + }, + }); + + if (existingUse) { + throw new BadRequestException('You have already used this invite code'); + } + + // Record the use + const use = this.inviteCodeUseRepository.create({ + inviteCodeId: validation.inviteCode.id, + usedBy: userId, + userEmail, + userIp: ipAddress, + userAgent, + }); + + await this.inviteCodeUseRepository.save(use); + } + + async getUsageStats(inviteCodeId: string): Promise<{ + totalUses: number; + recentUses: InviteCodeUse[]; + }> { + const [recentUses, totalUses] = await Promise.all([ + this.inviteCodeUseRepository.find({ + where: { inviteCodeId }, + order: { usedAt: 'DESC' }, + take: 10, + relations: ['user'], + }), + this.inviteCodeUseRepository.count({ where: { inviteCodeId } }), + ]); + + return { totalUses, recentUses }; + } + + async generateRandomCode(): Promise { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + let code = ''; + + for (let i = 0; i < 8; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + if (i === 3) code += '-'; + } + + // Check if code exists and regenerate if needed + const existing = await this.inviteCodeRepository.findOne({ where: { code } }); + if (existing) { + return this.generateRandomCode(); + } + + return code; + } +} \ No newline at end of file diff --git a/migrate-production.sh b/migrate-production.sh new file mode 100755 index 0000000..64a3478 --- /dev/null +++ b/migrate-production.sh @@ -0,0 +1,278 @@ +#!/bin/bash + +# ParentFlow Production Database Migration Script +# Runs all database migrations for both main app and admin dashboard + +set -e + +# Configuration +DB_HOST="10.0.0.207" +DB_PORT="5432" +DB_USER="postgres" +DB_PASSWORD="a3ppq" +DB_NAME="parentflow" +DB_NAME_ADMIN="parentflowadmin" + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 + exit 1 +} + +success() { + echo -e "${GREEN}✓${NC} $1" +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# Header +echo "" +echo "==========================================" +echo " Database Migration for Production " +echo "==========================================" +echo "" + +# Check PostgreSQL client +if ! command -v psql &> /dev/null; then + error "PostgreSQL client not installed. Run: apt-get install postgresql-client" +fi + +# Test database connection +log "Testing database connection..." +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -c "SELECT version();" > /dev/null 2>&1 +if [ $? -ne 0 ]; then + error "Cannot connect to database at $DB_HOST:$DB_PORT" +fi +success "Database connection successful" + +# Create databases if they don't exist +log "Ensuring databases exist..." +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres << EOF +-- Create main database +SELECT 'CREATE DATABASE ${DB_NAME}' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}')\\gexec + +-- Create admin database +SELECT 'CREATE DATABASE ${DB_NAME_ADMIN}' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME_ADMIN}')\\gexec +EOF +success "Databases verified" + +# Enable extensions +log "Enabling required extensions..." +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "CREATE EXTENSION IF NOT EXISTS vector;" +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME_ADMIN -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" +success "Extensions enabled" + +# Find migration directory +MIGRATION_DIR="$(dirname "$0")/maternal-app/maternal-app-backend/src/database/migrations" +if [ ! -d "$MIGRATION_DIR" ]; then + error "Migration directory not found: $MIGRATION_DIR" +fi + +# Create migration tracking table +log "Creating migration tracking table..." +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME << 'EOF' +CREATE TABLE IF NOT EXISTS schema_migrations ( + id SERIAL PRIMARY KEY, + version VARCHAR(255) NOT NULL UNIQUE, + executed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + execution_time_ms INTEGER, + success BOOLEAN DEFAULT true, + error_message TEXT, + checksum VARCHAR(64) +); + +CREATE INDEX IF NOT EXISTS idx_schema_migrations_version ON schema_migrations(version); +CREATE INDEX IF NOT EXISTS idx_schema_migrations_executed ON schema_migrations(executed_at); +EOF +success "Migration tracking table ready" + +# Define all migrations in order +MIGRATIONS=( + "V001_create_core_auth.sql" + "V002_create_family_structure.sql" + "V003_create_child_entities.sql" + "V004_create_activity_tables.sql" + "V005_create_notification_system.sql" + "V006_create_audit_log.sql" + "V007_create_analytics_tables.sql" + "V008_add_verification_system.sql" + "V008_add_eula_fields.sql" + "V009_add_family_preferences.sql" + "V009_add_multi_child_preferences.sql" + "V010_enhance_notifications.sql" + "V010_add_ai_conversations.sql" + "V011_add_tracking_enhancements.sql" + "V011_create_photos_module.sql" + "V012_add_device_registry.sql" + "V013_add_mfa_support.sql" + "V014_add_refresh_token_rotation.sql" + "V015_add_audit_fields.sql" + "V016_add_webauthn_support.sql" + "V017_add_gdpr_compliance.sql" + "V018_add_ai_embeddings.sql" + "V019_add_voice_feedback.sql" + "V020_add_indexes_optimization.sql" + "V021_add_timezone_support.sql" + "V022_add_data_archiving.sql" + "V028_create_invite_codes.sql" +) + +# Function to calculate file checksum +calculate_checksum() { + local file=$1 + if command -v sha256sum &> /dev/null; then + sha256sum "$file" | cut -d' ' -f1 + else + echo "no-checksum" + fi +} + +# Run migrations +log "Running migrations for main database..." +TOTAL=${#MIGRATIONS[@]} +CURRENT=0 +SKIPPED=0 +EXECUTED=0 + +for migration in "${MIGRATIONS[@]}"; do + CURRENT=$((CURRENT + 1)) + MIGRATION_FILE="$MIGRATION_DIR/$migration" + + if [ ! -f "$MIGRATION_FILE" ]; then + warning "[$CURRENT/$TOTAL] Migration file not found: $migration" + continue + fi + + # Check if migration was already executed + VERSION=$(echo $migration | cut -d'_' -f1) + ALREADY_RUN=$(PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -tAc \ + "SELECT COUNT(*) FROM schema_migrations WHERE version = '$VERSION';") + + if [ "$ALREADY_RUN" = "1" ]; then + echo -e "${YELLOW}[$CURRENT/$TOTAL]${NC} Skipping $migration (already applied)" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo -e "${BLUE}[$CURRENT/$TOTAL]${NC} Applying $migration..." + + START_TIME=$(date +%s%3N) + CHECKSUM=$(calculate_checksum "$MIGRATION_FILE") + + # Run migration + if PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$MIGRATION_FILE" > /dev/null 2>&1; then + END_TIME=$(date +%s%3N) + EXEC_TIME=$((END_TIME - START_TIME)) + + # Record successful migration + PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME << EOF +INSERT INTO schema_migrations (version, execution_time_ms, checksum) +VALUES ('$VERSION', $EXEC_TIME, '$CHECKSUM'); +EOF + success "Applied $migration (${EXEC_TIME}ms)" + EXECUTED=$((EXECUTED + 1)) + else + error "Failed to apply migration: $migration" + fi +done + +log "Migration summary: $EXECUTED executed, $SKIPPED skipped, $TOTAL total" + +# Run admin-specific migrations if needed +log "Setting up admin database..." +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME_ADMIN << 'EOF' +-- Admin users table (if not exists from main DB) +CREATE TABLE IF NOT EXISTS admin_users ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(100), + role VARCHAR(50) DEFAULT 'admin', + is_active BOOLEAN DEFAULT true, + last_login_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + permissions JSONB DEFAULT '["users:read", "users:write", "invites:read", "invites:write", "analytics:read"]', + two_factor_secret VARCHAR(255), + two_factor_enabled BOOLEAN DEFAULT false +); + +-- Admin sessions +CREATE TABLE IF NOT EXISTS admin_sessions ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + admin_user_id VARCHAR(36) REFERENCES admin_users(id) ON DELETE CASCADE, + token_hash VARCHAR(255) NOT NULL UNIQUE, + ip_address VARCHAR(45), + user_agent TEXT, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Admin audit logs +CREATE TABLE IF NOT EXISTS admin_audit_logs ( + id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text, + admin_user_id VARCHAR(36) REFERENCES admin_users(id), + action VARCHAR(100) NOT NULL, + entity_type VARCHAR(50), + entity_id VARCHAR(36), + details JSONB DEFAULT '{}', + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_admin_users_email ON admin_users(email); +CREATE INDEX IF NOT EXISTS idx_admin_sessions_token ON admin_sessions(token_hash); +CREATE INDEX IF NOT EXISTS idx_admin_sessions_admin ON admin_sessions(admin_user_id); +CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_admin ON admin_audit_logs(admin_user_id); +CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created ON admin_audit_logs(created_at); + +-- Insert default admin user if not exists (password: admin123) +INSERT INTO admin_users (email, password_hash, name, role) +VALUES ( + 'admin@parentflowapp.com', + '$2b$10$H5hw3/iwkCichU5dpVIMqe5Me7WV9jz.qWRm0V4JyGF9smgxgFBxm', + 'System Administrator', + 'super_admin' +) ON CONFLICT (email) DO NOTHING; +EOF +success "Admin database configured" + +# Verify migration status +log "Verifying migration status..." +TOTAL_APPLIED=$(PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -tAc \ + "SELECT COUNT(*) FROM schema_migrations WHERE success = true;") + +echo "" +echo "==========================================" +echo -e "${GREEN} Database Migration Completed! ${NC}" +echo "==========================================" +echo "" +echo "Summary:" +echo " Total migrations: $TOTAL" +echo " Applied in this run: $EXECUTED" +echo " Previously applied: $SKIPPED" +echo " Total in database: $TOTAL_APPLIED" +echo "" +echo "Databases ready:" +echo " Main: $DB_NAME at $DB_HOST:$DB_PORT" +echo " Admin: $DB_NAME_ADMIN at $DB_HOST:$DB_PORT" +echo "" +success "All migrations completed successfully" \ No newline at end of file diff --git a/parentflow-admin/.gitignore b/parentflow-admin/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/parentflow-admin/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/parentflow-admin/README.md b/parentflow-admin/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/parentflow-admin/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/parentflow-admin/next.config.ts b/parentflow-admin/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/parentflow-admin/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/parentflow-admin/package-lock.json b/parentflow-admin/package-lock.json new file mode 100644 index 0000000..bf2ecf0 --- /dev/null +++ b/parentflow-admin/package-lock.json @@ -0,0 +1,3564 @@ +{ + "name": "parentflow-admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "parentflow-admin", + "version": "0.1.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.4", + "@mui/material": "^7.3.4", + "@mui/material-nextjs": "^7.3.3", + "@mui/x-charts": "^8.13.1", + "@mui/x-data-grid": "^8.13.1", + "@tanstack/react-query": "^5.90.2", + "axios": "^1.12.2", + "date-fns": "^4.1.0", + "next": "15.5.4", + "react": "19.1.0", + "react-dom": "19.1.0", + "recharts": "^3.2.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.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/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.4.tgz", + "integrity": "sha512-BIktMapG3r4iXwIhYNpvk97ZfYWTreBBQTWjQKbNbzI64+ULHfYavQEX2w99aSWHS58DvXESWIgbD9adKcUOBw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.4.tgz", + "integrity": "sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.4", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", + "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.4", + "@mui/system": "^7.3.3", + "@mui/types": "^7.4.7", + "@mui/utils": "^7.3.3", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material-nextjs": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-7.3.3.tgz", + "integrity": "sha512-SHqEh/GIa/HkHzBMMGvTRCVSLvXYTATZ7QN7/QxngimW5kBDI+pWzjieS8HmLDgo36rxD8g+GcLY6Bu+pilciw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.4", + "@emotion/server": "^11.11.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "next": "^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/cache": { + "optional": true + }, + "@emotion/server": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.3.tgz", + "integrity": "sha512-OJM+9nj5JIyPUvsZ5ZjaeC9PfktmK+W5YaVLToLR8L0lB/DGmv1gcKE43ssNLSvpoW71Hct0necfade6+kW3zQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.3.tgz", + "integrity": "sha512-CmFxvRJIBCEaWdilhXMw/5wFJ1+FT9f3xt+m2pPXhHPeVIbBg9MnMvNSJjdALvnQJMPw8jLhrUtXmN7QAZV2fw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz", + "integrity": "sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.3", + "@mui/styled-engine": "^7.3.3", + "@mui/types": "^7.4.7", + "@mui/utils": "^7.3.3", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.7.tgz", + "integrity": "sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.3.tgz", + "integrity": "sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.7", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.13.1.tgz", + "integrity": "sha512-JGlrdBuVplT0Xf8vZRByFJTLdLY/zf0w5sFIc0PWdm8Z/nvbcASUKU8wsBvIPZVjEAX4Z1XxNB16VAhdMboEZg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.2", + "@mui/x-charts-vendor": "8.12.0", + "@mui/x-internal-gestures": "0.3.2", + "@mui/x-internals": "8.13.1", + "bezier-easing": "^2.1.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.12.0.tgz", + "integrity": "sha512-QkJQNgbaZ/RX4qlXuDd3iQVqtDrBOB4CihJYB7SkFjh+rg4e3AwNDWsJpEX1IivE0OUmwU7aQBmvwHheKlzBLw==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-sankey": "^0.12.4", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.7", + "@types/d3-time": "^3.0.4", + "@types/d3-timer": "^3.0.2", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-sankey": "^0.12.3", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "d3-timer": "^3.0.1", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-data-grid": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.13.1.tgz", + "integrity": "sha512-64MlyukMoGEDLT3kqdm6tw+rocgMayChj+h+fdAwqD4+2NMQoD5wZElQE+xTNmU0/DPv710X4ENceBRt2hMuGw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.2", + "@mui/x-internals": "8.13.1", + "@mui/x-virtualizer": "0.2.2", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-internal-gestures": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.3.2.tgz", + "integrity": "sha512-c4DItm2b/HZVIZaiMgoaLPGHfhfdDnsNxt7MedEDBGlLTeDLFSRKkEMtS3Uob2Vwwjn482oXnEWnrxv9pm2hPA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + } + }, + "node_modules/@mui/x-internals": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.13.1.tgz", + "integrity": "sha512-OKQyCJ9uxtMpjBZCOEQGOR5MhgL1f9HjI4qZHuaLxxtDATK5rcBbVjBF67hI8FzXeF1wrcZP2wsjc4AgGpAo9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.2", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.2.tgz", + "integrity": "sha512-+ZcGYh/9ykoEofzcAWcJ3n6TBXzCc2ETvytho30wRkYv1ez+8yps0ezns/QvC4JqXBge/3y+e+QatIYjkTltdw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.2", + "@mui/x-internals": "8.13.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", + "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz", + "integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz", + "integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz", + "integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz", + "integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz", + "integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz", + "integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz", + "integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz", + "integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "postcss": "^8.4.41", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "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-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "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-sankey": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.12.4.tgz", + "integrity": "sha512-YTicQNwioitIlvuvlfW2GfO6sKxpohzg2cSQttlXAPjFwoBuN+XpGLhUN3kLutG/dI3GCLC+DUorqiJt7Naetw==", + "license": "MIT", + "dependencies": { + "@types/d3-shape": "^1" + } + }, + "node_modules/@types/d3-sankey/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "license": "MIT" + }, + "node_modules/@types/d3-sankey/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "^1" + } + }, + "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.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.1.tgz", + "integrity": "sha512-1U5NQWh/GylZQ50ZMnnPjkYHEaGhg6t5i/KI0LDDh3t4E3h3T3vzm+GLY2BRzMfIjSBwzm6tginoZl5z0O/qsA==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", + "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "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-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "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-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "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/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/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/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "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/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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": "15.5.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz", + "integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.4", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.4", + "@next/swc-darwin-x64": "15.5.4", + "@next/swc-linux-arm64-gnu": "15.5.4", + "@next/swc-linux-arm64-musl": "15.5.4", + "@next/swc-linux-x64-gnu": "15.5.4", + "@next/swc-linux-x64-musl": "15.5.4", + "@next/swc-win32-arm64-msvc": "15.5.4", + "@next/swc-win32-x64-msvc": "15.5.4", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/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/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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/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": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "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 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/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/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/parentflow-admin/package.json b/parentflow-admin/package.json new file mode 100644 index 0000000..6acb518 --- /dev/null +++ b/parentflow-admin/package.json @@ -0,0 +1,34 @@ +{ + "name": "parentflow-admin", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 3335 -H 0.0.0.0", + "build": "next build", + "start": "next start -p 3335" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.4", + "@mui/material": "^7.3.4", + "@mui/material-nextjs": "^7.3.3", + "@mui/x-charts": "^8.13.1", + "@mui/x-data-grid": "^8.13.1", + "@tanstack/react-query": "^5.90.2", + "axios": "^1.12.2", + "date-fns": "^4.1.0", + "next": "15.5.4", + "react": "19.1.0", + "react-dom": "19.1.0", + "recharts": "^3.2.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/parentflow-admin/postcss.config.mjs b/parentflow-admin/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/parentflow-admin/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/parentflow-admin/public/file.svg b/parentflow-admin/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/parentflow-admin/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/parentflow-admin/public/globe.svg b/parentflow-admin/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/parentflow-admin/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/parentflow-admin/public/next.svg b/parentflow-admin/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/parentflow-admin/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/parentflow-admin/public/vercel.svg b/parentflow-admin/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/parentflow-admin/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/parentflow-admin/public/window.svg b/parentflow-admin/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/parentflow-admin/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/parentflow-admin/src/app/favicon.ico b/parentflow-admin/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/parentflow-admin/src/app/globals.css b/parentflow-admin/src/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/parentflow-admin/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/parentflow-admin/src/app/invite-codes/page.tsx b/parentflow-admin/src/app/invite-codes/page.tsx new file mode 100644 index 0000000..0f17a43 --- /dev/null +++ b/parentflow-admin/src/app/invite-codes/page.tsx @@ -0,0 +1,393 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Box, + Paper, + Button, + Typography, + IconButton, + Chip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + FormControlLabel, + Switch, + Alert, + Tooltip, + Grid, +} from '@mui/material'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { + Add, + Edit, + Delete, + ContentCopy, + Refresh, +} from '@mui/icons-material'; +import { format } from 'date-fns'; +import AdminLayout from '@/components/AdminLayout'; +import apiClient from '@/lib/api-client'; + +interface InviteCode { + id: string; + code: string; + uses: number; + maxUses: number | null; + expiresAt: string | null; + createdAt: string; + createdBy: string; + isActive: boolean; + metadata?: any; +} + +export default function InviteCodesPage() { + const [inviteCodes, setInviteCodes] = useState([]); + const [loading, setLoading] = useState(true); + const [openDialog, setOpenDialog] = useState(false); + const [editingCode, setEditingCode] = useState(null); + const [copied, setCopied] = useState(null); + + // Form state + const [formData, setFormData] = useState({ + code: '', + maxUses: '', + hasExpiry: false, + expiresAt: '', + description: '', + }); + + const fetchInviteCodes = async () => { + setLoading(true); + try { + const data = await apiClient.getInviteCodes(); + setInviteCodes(data.codes || []); + } catch (error) { + console.error('Failed to fetch invite codes:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchInviteCodes(); + }, []); + + const generateRandomCode = () => { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + let code = ''; + for (let i = 0; i < 8; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + if (i === 3) code += '-'; + } + setFormData({ ...formData, code }); + }; + + const handleCopyCode = (code: string) => { + navigator.clipboard.writeText(code); + setCopied(code); + setTimeout(() => setCopied(null), 2000); + }; + + const handleSubmit = async () => { + try { + const payload = { + code: formData.code, + maxUses: formData.maxUses ? parseInt(formData.maxUses) : undefined, + expiresAt: formData.hasExpiry ? formData.expiresAt : undefined, + metadata: formData.description ? { description: formData.description } : undefined, + }; + + if (editingCode) { + await apiClient.updateInviteCode(editingCode.id, payload); + } else { + await apiClient.createInviteCode(payload); + } + + fetchInviteCodes(); + handleCloseDialog(); + } catch (error) { + console.error('Failed to save invite code:', error); + } + }; + + const handleDelete = async (id: string) => { + if (confirm('Are you sure you want to delete this invite code?')) { + try { + await apiClient.deleteInviteCode(id); + fetchInviteCodes(); + } catch (error) { + console.error('Failed to delete invite code:', error); + } + } + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + setEditingCode(null); + setFormData({ + code: '', + maxUses: '', + hasExpiry: false, + expiresAt: '', + description: '', + }); + }; + + const columns: GridColDef[] = [ + { + field: 'code', + headerName: 'Code', + width: 150, + renderCell: (params) => ( + + + {params.value} + + + handleCopyCode(params.value)}> + + + + + ), + }, + { + field: 'status', + headerName: 'Status', + width: 120, + renderCell: (params) => { + const isExpired = params.row.expiresAt && new Date(params.row.expiresAt) < new Date(); + const isMaxedOut = params.row.maxUses && params.row.uses >= params.row.maxUses; + + if (!params.row.isActive) { + return ; + } else if (isExpired) { + return ; + } else if (isMaxedOut) { + return ; + } + return ; + }, + }, + { + field: 'uses', + headerName: 'Uses', + width: 100, + renderCell: (params) => ( + + {params.value} / {params.row.maxUses || '∞'} + + ), + }, + { + field: 'expiresAt', + headerName: 'Expires', + width: 150, + renderCell: (params) => ( + + {params.value ? format(new Date(params.value), 'MMM dd, yyyy') : 'Never'} + + ), + }, + { + field: 'createdAt', + headerName: 'Created', + width: 150, + renderCell: (params) => ( + + {format(new Date(params.value), 'MMM dd, yyyy')} + + ), + }, + { + field: 'actions', + headerName: 'Actions', + width: 100, + sortable: false, + renderCell: (params) => ( + + { + setEditingCode(params.row); + setFormData({ + code: params.row.code, + maxUses: params.row.maxUses?.toString() || '', + hasExpiry: !!params.row.expiresAt, + expiresAt: params.row.expiresAt || '', + description: params.row.metadata?.description || '', + }); + setOpenDialog(true); + }}> + + + handleDelete(params.row.id)} color="error"> + + + + ), + }, + ]; + + return ( + + + + + Invite Codes Management + + + + + + + + + + + + Total Codes + + {inviteCodes.length} + + + + + + Active Codes + + + {inviteCodes.filter(c => c.isActive).length} + + + + + + + Total Uses + + + {inviteCodes.reduce((sum, c) => sum + c.uses, 0)} + + + + + + + Available + + + {inviteCodes.filter(c => { + const isExpired = c.expiresAt && new Date(c.expiresAt) < new Date(); + const isMaxedOut = c.maxUses && c.uses >= c.maxUses; + return c.isActive && !isExpired && !isMaxedOut; + }).length} + + + + + + + + + + + + {editingCode ? 'Edit Invite Code' : 'Create New Invite Code'} + + + + + setFormData({ ...formData, code: e.target.value })} + fullWidth + required + placeholder="ABCD-1234" + /> + + + + setFormData({ ...formData, maxUses: e.target.value })} + fullWidth + InputProps={{ inputProps: { min: 1 } }} + /> + + setFormData({ ...formData, hasExpiry: e.target.checked })} + /> + } + label="Set expiration date" + /> + + {formData.hasExpiry && ( + setFormData({ ...formData, expiresAt: e.target.value })} + fullWidth + InputLabelProps={{ shrink: true }} + /> + )} + + setFormData({ ...formData, description: e.target.value })} + fullWidth + multiline + rows={2} + placeholder="e.g., Beta testers group" + /> + + + Share this code with users to allow them to register. + The code can be used during the signup process. + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/parentflow-admin/src/app/layout.tsx b/parentflow-admin/src/app/layout.tsx new file mode 100644 index 0000000..55eedda --- /dev/null +++ b/parentflow-admin/src/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from 'next'; +import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; +import { ThemeProvider } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import theme from '@/lib/theme'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'ParentFlow Admin Dashboard', + description: 'Admin dashboard for managing ParentFlow application', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + + + ); +} \ No newline at end of file diff --git a/parentflow-admin/src/app/login/page.tsx b/parentflow-admin/src/app/login/page.tsx new file mode 100644 index 0000000..3be92b1 --- /dev/null +++ b/parentflow-admin/src/app/login/page.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { + Container, + Paper, + TextField, + Button, + Typography, + Box, + Alert, + CircularProgress, +} from '@mui/material'; +import { LockOutlined } from '@mui/icons-material'; +import apiClient from '@/lib/api-client'; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await apiClient.login(email, password); + router.push('/'); + } catch (err: any) { + setError(err.response?.data?.message || 'Login failed. Please check your credentials.'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + + + + Admin Login + + + + ParentFlow Admin Dashboard + + + {error && ( + + {error} + + )} + + + setEmail(e.target.value)} + disabled={loading} + /> + + setPassword(e.target.value)} + disabled={loading} + /> + + + + + + + ); +} \ No newline at end of file diff --git a/parentflow-admin/src/app/page.tsx b/parentflow-admin/src/app/page.tsx new file mode 100644 index 0000000..c7da420 --- /dev/null +++ b/parentflow-admin/src/app/page.tsx @@ -0,0 +1,393 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Box, + Grid, + Paper, + Typography, + Card, + CardContent, + LinearProgress, + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + Chip, + IconButton, +} from '@mui/material'; +import { + People, + FamilyRestroom, + ChildCare, + TrendingUp, + Warning, + CheckCircle, + Refresh, + AccessTime, +} from '@mui/icons-material'; +import { LineChart, Line, AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; +import { format, subDays, startOfDay } from 'date-fns'; +import AdminLayout from '@/components/AdminLayout'; +import apiClient from '@/lib/api-client'; + +interface DashboardStats { + totalUsers: number; + totalFamilies: number; + totalChildren: number; + activeUsers: number; + newUsersToday: number; + activitiesLogged: number; + aiQueriesTotal: number; + systemStatus: 'healthy' | 'warning' | 'error'; +} + +const COLORS = ['#FF8B7D', '#FFB5A0', '#FFD4CC', '#81C784', '#FFB74D']; + +export default function DashboardPage() { + const [stats, setStats] = useState({ + totalUsers: 0, + totalFamilies: 0, + totalChildren: 0, + activeUsers: 0, + newUsersToday: 0, + activitiesLogged: 0, + aiQueriesTotal: 0, + systemStatus: 'healthy', + }); + const [loading, setLoading] = useState(true); + const [userGrowthData, setUserGrowthData] = useState([]); + const [activityData, setActivityData] = useState([]); + const [recentUsers, setRecentUsers] = useState([]); + + const fetchDashboardData = async () => { + setLoading(true); + try { + // Simulate API calls - replace with actual calls + const analyticsData = await apiClient.getAnalytics(); + const growthData = await apiClient.getUserGrowth(); + const activityStats = await apiClient.getActivityStats(); + + // Mock data for now - replace with actual API response + setStats({ + totalUsers: 1250, + totalFamilies: 420, + totalChildren: 680, + activeUsers: 890, + newUsersToday: 23, + activitiesLogged: 15420, + aiQueriesTotal: 3250, + systemStatus: 'healthy', + }); + + // Mock user growth data + const mockGrowthData = Array.from({ length: 30 }, (_, i) => { + const date = subDays(new Date(), 29 - i); + return { + date: format(date, 'MMM dd'), + users: Math.floor(Math.random() * 50) + 20, + activities: Math.floor(Math.random() * 200) + 100, + }; + }); + setUserGrowthData(mockGrowthData); + + // Mock activity distribution + setActivityData([ + { name: 'Feeding', value: 4500, color: '#FF8B7D' }, + { name: 'Sleep', value: 3200, color: '#FFB5A0' }, + { name: 'Diapers', value: 2800, color: '#FFD4CC' }, + { name: 'Milestones', value: 1200, color: '#81C784' }, + { name: 'Other', value: 3720, color: '#FFB74D' }, + ]); + + // Mock recent users + setRecentUsers([ + { id: 1, name: 'Sarah Johnson', email: 'sarah@example.com', joinedAt: new Date() }, + { id: 2, name: 'Mike Chen', email: 'mike@example.com', joinedAt: new Date() }, + { id: 3, name: 'Emma Davis', email: 'emma@example.com', joinedAt: new Date() }, + ]); + } catch (error) { + console.error('Failed to fetch dashboard data:', error); + // Use mock data even on error for development + setStats({ + totalUsers: 1250, + totalFamilies: 420, + totalChildren: 680, + activeUsers: 890, + newUsersToday: 23, + activitiesLogged: 15420, + aiQueriesTotal: 3250, + systemStatus: 'healthy', + }); + + const mockGrowthData = Array.from({ length: 30 }, (_, i) => { + const date = subDays(new Date(), 29 - i); + return { + date: format(date, 'MMM dd'), + users: Math.floor(Math.random() * 50) + 20, + activities: Math.floor(Math.random() * 200) + 100, + }; + }); + setUserGrowthData(mockGrowthData); + + setActivityData([ + { name: 'Feeding', value: 4500, color: '#FF8B7D' }, + { name: 'Sleep', value: 3200, color: '#FFB5A0' }, + { name: 'Diapers', value: 2800, color: '#FFD4CC' }, + { name: 'Milestones', value: 1200, color: '#81C784' }, + { name: 'Other', value: 3720, color: '#FFB74D' }, + ]); + + setRecentUsers([ + { id: 1, name: 'Sarah Johnson', email: 'sarah@example.com', joinedAt: new Date() }, + { id: 2, name: 'Mike Chen', email: 'mike@example.com', joinedAt: new Date() }, + { id: 3, name: 'Emma Davis', email: 'emma@example.com', joinedAt: new Date() }, + ]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchDashboardData(); + }, []); + + const StatCard = ({ icon, title, value, change, color }: any) => ( + + + + + {icon} + + + + {title} + + + {value.toLocaleString()} + + {change && ( + 0 ? 'success.main' : 'error.main' }} + > + {change > 0 ? '+' : ''}{change}% from yesterday + + )} + + + + + ); + + if (loading) { + return ( + + + + ); + } + + return ( + + + + + + Dashboard Overview + + + Welcome back! Here's what's happening with ParentFlow today. + + + + + + + + {/* Stats Cards */} + + + } + title="Total Users" + value={stats.totalUsers} + change={5.2} + color="primary" + /> + + + } + title="Families" + value={stats.totalFamilies} + change={3.1} + color="secondary" + /> + + + } + title="Children" + value={stats.totalChildren} + change={4.5} + color="info" + /> + + + } + title="Activities Today" + value={stats.activitiesLogged} + change={12.3} + color="success" + /> + + + + {/* Charts Row */} + + + + + User Growth (Last 30 Days) + + + + + + + + + + + + + + + + Activity Distribution + + + + `${name} ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {activityData.map((entry, index) => ( + + ))} + + + + + + + + + {/* Recent Activity and System Status */} + + + + + Recent Users + + + {recentUsers.map((user) => ( + + + + {user.name.charAt(0)} + + + + } + label="New" + size="small" + color="success" + variant="outlined" + /> + + ))} + + + + + + + System Status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/parentflow-admin/src/components/AdminLayout.tsx b/parentflow-admin/src/components/AdminLayout.tsx new file mode 100644 index 0000000..72e0037 --- /dev/null +++ b/parentflow-admin/src/components/AdminLayout.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useState, ReactNode } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { + Box, + Drawer, + AppBar, + Toolbar, + List, + Typography, + IconButton, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Avatar, + Menu, + MenuItem, + Divider, + Chip, +} from '@mui/material'; +import { + Menu as MenuIcon, + Dashboard, + People, + ConfirmationNumber, + Analytics, + Settings, + Logout, + FamilyRestroom, + HealthAndSafety, +} from '@mui/icons-material'; +import apiClient from '@/lib/api-client'; + +const drawerWidth = 240; + +interface AdminLayoutProps { + children: ReactNode; +} + +export default function AdminLayout({ children }: AdminLayoutProps) { + const router = useRouter(); + const pathname = usePathname(); + const [mobileOpen, setMobileOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + + const handleDrawerToggle = () => { + setMobileOpen(!mobileOpen); + }; + + const handleLogout = async () => { + await apiClient.logout(); + router.push('/login'); + }; + + const menuItems = [ + { text: 'Dashboard', icon: , path: '/' }, + { text: 'Users', icon: , path: '/users' }, + { text: 'Families', icon: , path: '/families' }, + { text: 'Invite Codes', icon: , path: '/invite-codes' }, + { text: 'Analytics', icon: , path: '/analytics' }, + { text: 'System Health', icon: , path: '/health' }, + { text: 'Settings', icon: , path: '/settings' }, + ]; + + const drawer = ( + + + + ParentFlow Admin + + + + + {menuItems.map((item) => ( + + router.push(item.path)} + selected={pathname === item.path} + sx={{ + '&.Mui-selected': { + backgroundColor: 'primary.light', + '&:hover': { + backgroundColor: 'primary.light', + }, + }, + }} + > + + {item.icon} + + + + + ))} + + + ); + + return ( + + + + + + + + + + {menuItems.find(item => item.path === pathname)?.text || 'Dashboard'} + + + + + + setAnchorEl(e.currentTarget)}> + A + + + setAnchorEl(null)} + > + + + + + Logout + + + + + + + + {drawer} + + + {drawer} + + + + + {children} + + + ); +} \ No newline at end of file diff --git a/parentflow-admin/src/lib/api-client.ts b/parentflow-admin/src/lib/api-client.ts new file mode 100644 index 0000000..f537b06 --- /dev/null +++ b/parentflow-admin/src/lib/api-client.ts @@ -0,0 +1,183 @@ +import axios from 'axios'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020/api/v1'; + +class ApiClient { + private token: string | null = null; + private refreshToken: string | null = null; + + constructor() { + // Initialize tokens from localStorage if available + if (typeof window !== 'undefined') { + this.token = localStorage.getItem('admin_access_token'); + this.refreshToken = localStorage.getItem('admin_refresh_token'); + } + } + + setTokens(accessToken: string, refreshToken: string) { + this.token = accessToken; + this.refreshToken = refreshToken; + if (typeof window !== 'undefined') { + localStorage.setItem('admin_access_token', accessToken); + localStorage.setItem('admin_refresh_token', refreshToken); + } + } + + clearTokens() { + this.token = null; + this.refreshToken = null; + if (typeof window !== 'undefined') { + localStorage.removeItem('admin_access_token'); + localStorage.removeItem('admin_refresh_token'); + } + } + + private async request(method: string, endpoint: string, data?: any, options?: any) { + const config = { + method, + url: `${API_BASE_URL}${endpoint}`, + headers: { + 'Content-Type': 'application/json', + ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), + ...options?.headers, + }, + ...options, + }; + + if (data) { + config.data = data; + } + + try { + const response = await axios(config); + return response.data; + } catch (error: any) { + // Handle token refresh + if (error.response?.status === 401 && this.refreshToken) { + try { + const refreshResponse = await axios.post(`${API_BASE_URL}/auth/refresh`, { + refreshToken: this.refreshToken, + }); + + this.setTokens(refreshResponse.data.accessToken, refreshResponse.data.refreshToken); + + // Retry original request + config.headers.Authorization = `Bearer ${this.token}`; + const response = await axios(config); + return response.data; + } catch (refreshError) { + this.clearTokens(); + window.location.href = '/login'; + throw refreshError; + } + } + throw error; + } + } + + // Auth endpoints + async login(email: string, password: string) { + const response = await this.request('POST', '/admin/auth/login', { email, password }); + this.setTokens(response.accessToken, response.refreshToken); + return response; + } + + async logout() { + try { + await this.request('POST', '/admin/auth/logout'); + } finally { + this.clearTokens(); + } + } + + async getCurrentAdmin() { + return this.request('GET', '/admin/auth/me'); + } + + // User management endpoints + async getUsers(params?: { page?: number; limit?: number; search?: string }) { + const queryString = params ? '?' + new URLSearchParams(params as any).toString() : ''; + return this.request('GET', `/admin/users${queryString}`); + } + + async getUserById(id: string) { + return this.request('GET', `/admin/users/${id}`); + } + + async updateUser(id: string, data: any) { + return this.request('PATCH', `/admin/users/${id}`, data); + } + + async deleteUser(id: string) { + return this.request('DELETE', `/admin/users/${id}`); + } + + // Invite code endpoints + async getInviteCodes(params?: { page?: number; limit?: number; status?: string }) { + const queryString = params ? '?' + new URLSearchParams(params as any).toString() : ''; + return this.request('GET', `/admin/invite-codes${queryString}`); + } + + async createInviteCode(data: { + code: string; + maxUses?: number; + expiresAt?: string; + metadata?: any; + }) { + return this.request('POST', '/admin/invite-codes', data); + } + + async updateInviteCode(id: string, data: any) { + return this.request('PATCH', `/admin/invite-codes/${id}`, data); + } + + async deleteInviteCode(id: string) { + return this.request('DELETE', `/admin/invite-codes/${id}`); + } + + // Analytics endpoints + async getAnalytics(params?: { startDate?: string; endDate?: string }) { + const queryString = params ? '?' + new URLSearchParams(params as any).toString() : ''; + return this.request('GET', `/admin/analytics${queryString}`); + } + + async getUserGrowth() { + return this.request('GET', '/admin/analytics/user-growth'); + } + + async getActivityStats() { + return this.request('GET', '/admin/analytics/activity-stats'); + } + + async getSystemHealth() { + return this.request('GET', '/admin/analytics/system-health'); + } + + // Family management + async getFamilies(params?: { page?: number; limit?: number; search?: string }) { + const queryString = params ? '?' + new URLSearchParams(params as any).toString() : ''; + return this.request('GET', `/admin/families${queryString}`); + } + + async getFamilyById(id: string) { + return this.request('GET', `/admin/families/${id}`); + } + + // Activity logs + async getActivityLogs(params?: { page?: number; limit?: number; userId?: string }) { + const queryString = params ? '?' + new URLSearchParams(params as any).toString() : ''; + return this.request('GET', `/admin/logs${queryString}`); + } + + // System settings + async getSettings() { + return this.request('GET', '/admin/settings'); + } + + async updateSettings(data: any) { + return this.request('PATCH', '/admin/settings', data); + } +} + +export const apiClient = new ApiClient(); +export default apiClient; \ No newline at end of file diff --git a/parentflow-admin/src/lib/theme.ts b/parentflow-admin/src/lib/theme.ts new file mode 100644 index 0000000..469cf89 --- /dev/null +++ b/parentflow-admin/src/lib/theme.ts @@ -0,0 +1,106 @@ +import { createTheme } from '@mui/material/styles'; + +const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#FF8B7D', + light: '#FFB5A0', + dark: '#FF6B59', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#FFD4CC', + light: '#FFE8E4', + dark: '#FFB5A0', + contrastText: '#2C2C2C', + }, + success: { + main: '#81C784', + light: '#A5D6A7', + dark: '#66BB6A', + }, + warning: { + main: '#FFB74D', + light: '#FFCC80', + dark: '#FFA726', + }, + error: { + main: '#FF8A80', + light: '#FFAB91', + dark: '#FF7961', + }, + background: { + default: '#FFF8F5', + paper: '#FFFFFF', + }, + text: { + primary: '#2C2C2C', + secondary: '#5E5E5E', + }, + }, + typography: { + fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', + h1: { + fontSize: '2.5rem', + fontWeight: 600, + lineHeight: 1.2, + }, + h2: { + fontSize: '2rem', + fontWeight: 600, + lineHeight: 1.3, + }, + h3: { + fontSize: '1.75rem', + fontWeight: 500, + lineHeight: 1.4, + }, + h4: { + fontSize: '1.5rem', + fontWeight: 500, + lineHeight: 1.4, + }, + h5: { + fontSize: '1.25rem', + fontWeight: 500, + lineHeight: 1.5, + }, + h6: { + fontSize: '1rem', + fontWeight: 500, + lineHeight: 1.6, + }, + }, + shape: { + borderRadius: 12, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', + borderRadius: 8, + fontWeight: 500, + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: 12, + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + borderRadius: 12, + }, + }, + }, + }, +}); + +export default theme; \ No newline at end of file diff --git a/parentflow-admin/tsconfig.json b/parentflow-admin/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/parentflow-admin/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "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": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/start-production.sh b/start-production.sh index d07182a..fa57158 100755 --- a/start-production.sh +++ b/start-production.sh @@ -1,235 +1,214 @@ #!/bin/bash -# ParentFlow Production Startup Script -# Uses PM2 for frontend/backend, Docker for databases -# Ports: Backend 3020, Frontend 3030 -# Server: 10.0.0.240 (or localhost for local production testing) +# ParentFlow Production Start Script +# Starts all production services including backend, frontend, and admin dashboard -set -e # Exit on any error +set -e + +# Configuration +DEPLOY_DIR="/root/parentflow-production" +DB_HOST="10.0.0.207" +DB_PORT="5432" # Color codes RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +CYAN='\033[0;36m' +NC='\033[0m' +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 + exit 1 +} + +success() { + echo -e "${GREEN}✓${NC} $1" +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# Header +echo "" echo "==========================================" -echo "ParentFlow Production Startup" +echo " Starting ParentFlow Production " echo "==========================================" echo "" -# Check if PM2 is installed -if ! command -v pm2 &> /dev/null; then - echo -e "${RED}ERROR: PM2 is not installed!${NC}" - echo "Install PM2 globally with: npm install -g pm2" - exit 1 -fi - -# Check if Docker is installed -if ! command -v docker &> /dev/null; then - echo -e "${RED}ERROR: Docker is not installed!${NC}" - exit 1 -fi - -# Check if docker-compose is installed -if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then - echo -e "${RED}ERROR: Docker Compose is not installed!${NC}" - exit 1 -fi - -# Step 1: Start Docker services (databases) -echo -e "${BLUE}Step 1: Starting Docker services (databases)...${NC}" -if docker compose version &> /dev/null; then - docker compose -f docker-compose.production.yml up -d -else - docker-compose -f docker-compose.production.yml up -d +# Check if we're in the right directory +if [ "$PWD" != "$DEPLOY_DIR" ] && [ -d "$DEPLOY_DIR" ]; then + cd "$DEPLOY_DIR" fi +# Step 1: Check database connectivity +log "${CYAN}Step 1: Checking database connectivity...${NC}" +PGPASSWORD=a3ppq psql -h $DB_HOST -p $DB_PORT -U postgres -d parentflow \ + -c "SELECT version();" > /dev/null 2>&1 if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ Docker services started${NC}" + success "Database connection successful" else - echo -e "${RED}✗ Failed to start Docker services${NC}" - exit 1 + error "Cannot connect to database at $DB_HOST:$DB_PORT" fi -# Wait for databases to be ready -echo -e "${YELLOW}Waiting for databases to be healthy...${NC}" -sleep 10 - -# Check PostgreSQL connectivity (dedicated server) -echo -e "${BLUE}Checking PostgreSQL connectivity on 10.0.0.207...${NC}" -PGPASSWORD=a3ppq psql -h 10.0.0.207 -p 5432 -U postgres -d parentflow -c "SELECT version();" > /dev/null 2>&1 -if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ PostgreSQL connection successful${NC}" -else - echo -e "${RED}✗ Cannot connect to PostgreSQL on 10.0.0.207${NC}" - echo "Please ensure PostgreSQL is running and accessible" - exit 1 -fi - -# Check Docker services health -echo -e "${BLUE}Checking Docker services health...${NC}" -MAX_RETRIES=30 -RETRY_COUNT=0 - -while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - REDIS_HEALTHY=$(docker inspect parentflow-redis-prod --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") - MONGO_HEALTHY=$(docker inspect parentflow-mongodb-prod --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") - MINIO_HEALTHY=$(docker inspect parentflow-minio-prod --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") - - if [ "$REDIS_HEALTHY" = "healthy" ] && [ "$MONGO_HEALTHY" = "healthy" ] && [ "$MINIO_HEALTHY" = "healthy" ]; then - echo -e "${GREEN}✓ All Docker services are healthy${NC}" - break +# Step 2: Start Docker services +log "${CYAN}Step 2: Starting Docker services...${NC}" +if [ -f "docker-compose.production.yml" ]; then + if docker compose version &> /dev/null; then + docker compose -f docker-compose.production.yml up -d + else + docker-compose -f docker-compose.production.yml up -d fi - - echo -e "${YELLOW}Waiting for Docker services... ($RETRY_COUNT/$MAX_RETRIES)${NC}" - sleep 2 - ((RETRY_COUNT++)) -done - -if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then - echo -e "${RED}✗ Docker services failed to become healthy${NC}" - echo "Check Docker logs with: docker logs parentflow-redis-prod" - exit 1 -fi - -# Step 2: Run database migrations -echo "" -echo -e "${BLUE}Step 2: Running database migrations...${NC}" -cd /root/maternal-app/maternal-app/maternal-app-backend - -# Check if migration script exists -if [ -f "./scripts/master-migration.sh" ]; then - echo -e "${YELLOW}Running master migration script...${NC}" - DATABASE_HOST=10.0.0.207 \ - DATABASE_PORT=5432 \ - DATABASE_NAME=parentflow \ - DATABASE_USER=postgres \ - DATABASE_PASSWORD=a3ppq \ - ./scripts/master-migration.sh || { - echo -e "${YELLOW}Warning: Migrations may have partially failed. Continuing...${NC}" - } + sleep 5 + success "Docker services started (Redis, MongoDB, MinIO)" else - echo -e "${YELLOW}Warning: Migration script not found. Skipping migrations.${NC}" + warning "Docker compose file not found, skipping..." fi -# Step 3: Build backend (if needed) -echo "" -echo -e "${BLUE}Step 3: Building backend...${NC}" -cd /root/maternal-app/maternal-app/maternal-app-backend +# Step 3: Verify Docker services +log "${CYAN}Step 3: Verifying Docker services...${NC}" +docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "redis|mongo|minio" || warning "Some Docker services may not be running" -if [ ! -d "dist" ]; then - echo -e "${YELLOW}Building backend for the first time...${NC}" - npm run build - if [ $? -ne 0 ]; then - echo -e "${RED}✗ Backend build failed${NC}" - exit 1 - fi - echo -e "${GREEN}✓ Backend built successfully${NC}" -else - echo -e "${GREEN}✓ Backend dist directory exists${NC}" -fi +# Step 4: Start PM2 processes +log "${CYAN}Step 4: Starting PM2 application services...${NC}" -# Step 4: Build frontend (if needed) -echo "" -echo -e "${BLUE}Step 4: Building frontend...${NC}" -cd /root/maternal-app/maternal-web - -if [ ! -d ".next" ]; then - echo -e "${YELLOW}Building frontend for the first time...${NC}" - npm run build - if [ $? -ne 0 ]; then - echo -e "${RED}✗ Frontend build failed${NC}" - exit 1 - fi - echo -e "${GREEN}✓ Frontend built successfully${NC}" -else - echo -e "${GREEN}✓ Frontend .next directory exists${NC}" -fi - -# Step 5: Start PM2 processes -echo "" -echo -e "${BLUE}Step 5: Starting PM2 processes...${NC}" - -# Check if ecosystem.config.js exists -if [ ! -f "/root/maternal-app/ecosystem.config.js" ]; then - echo -e "${RED}✗ ecosystem.config.js not found${NC}" - exit 1 -fi - -# Stop any existing PM2 processes -echo -e "${YELLOW}Stopping any existing PM2 processes...${NC}" +# Delete any existing PM2 processes pm2 delete all 2>/dev/null || true -# Start PM2 with ecosystem config (production environment) -cd /root/maternal-app -pm2 start ecosystem.config.js --env production - -if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ PM2 processes started${NC}" +# Start using ecosystem file +if [ -f "ecosystem.config.js" ]; then + pm2 start ecosystem.config.js --env production + success "PM2 services started from ecosystem config" else - echo -e "${RED}✗ Failed to start PM2 processes${NC}" - exit 1 + warning "PM2 ecosystem config not found, starting services manually..." + + # Start Backend API + log "Starting Backend API..." + cd "$DEPLOY_DIR/maternal-app/maternal-app-backend" + pm2 start dist/main.js \ + --name "parentflow-api" \ + --instances 2 \ + --exec-mode cluster \ + --env production \ + --max-memory-restart 500M \ + --error /var/log/parentflow/api-error.log \ + --output /var/log/parentflow/api-out.log \ + --merge-logs \ + --time \ + -- --port 3020 + + # Start Frontend + log "Starting Frontend..." + cd "$DEPLOY_DIR/maternal-web" + pm2 start npm \ + --name "parentflow-frontend" \ + --instances 2 \ + --exec-mode cluster \ + --max-memory-restart 400M \ + --error /var/log/parentflow/frontend-error.log \ + --output /var/log/parentflow/frontend-out.log \ + --merge-logs \ + --time \ + -- run start + + # Start Admin Dashboard + log "Starting Admin Dashboard..." + cd "$DEPLOY_DIR/parentflow-admin" + pm2 start npm \ + --name "parentflow-admin" \ + --instances 1 \ + --max-memory-restart 300M \ + --error /var/log/parentflow/admin-error.log \ + --output /var/log/parentflow/admin-out.log \ + --merge-logs \ + --time \ + -- run start fi -# Save PM2 process list +# Save PM2 configuration pm2 save +pm2 startup systemd -u root --hp /root || true + +# Step 5: Wait for services to start +log "${CYAN}Step 5: Waiting for services to initialize...${NC}" +sleep 10 # Step 6: Verify services are running -echo "" -echo -e "${BLUE}Step 6: Verifying services...${NC}" -sleep 5 +log "${CYAN}Step 6: Verifying services...${NC}" -# Check PM2 status -echo -e "${YELLOW}PM2 Status:${NC}" +verify_service() { + local name=$1 + local port=$2 + + if lsof -i:$port > /dev/null 2>&1; then + success "$name is running on port $port" + return 0 + else + warning "$name is not detected on port $port" + return 1 + fi +} + +# Check each service +ALL_GOOD=true +verify_service "Backend API" 3020 || ALL_GOOD=false +verify_service "Frontend" 3030 || ALL_GOOD=false +verify_service "Admin Dashboard" 3335 || ALL_GOOD=false +verify_service "Redis" 6379 || ALL_GOOD=false +verify_service "MongoDB" 27017 || ALL_GOOD=false +verify_service "MinIO" 9000 || ALL_GOOD=false + +# Step 7: Show PM2 status +log "${CYAN}Step 7: PM2 Process Status${NC}" +echo "" pm2 list - -# Check if ports are listening echo "" -echo -e "${YELLOW}Port Status:${NC}" -if lsof -i:3020 > /dev/null 2>&1; then - echo -e "${GREEN}✓ Backend is running on port 3020${NC}" + +# Step 8: Test API health +log "${CYAN}Step 8: Testing API health endpoint...${NC}" +sleep 3 +if curl -s -o /dev/null -w "%{http_code}" http://localhost:3020/health | grep -q "200\|401"; then + success "API is responding" else - echo -e "${RED}✗ Backend not detected on port 3020${NC}" + warning "API health check failed - may still be starting" fi -if lsof -i:3030 > /dev/null 2>&1; then - echo -e "${GREEN}✓ Frontend is running on port 3030${NC}" +# Final summary +echo "" +echo "==========================================" +if [ "$ALL_GOOD" = true ]; then + echo -e "${GREEN} All Services Started Successfully! ${NC}" else - echo -e "${RED}✗ Frontend not detected on port 3030${NC}" + echo -e "${YELLOW} Services Started (Check Warnings) ${NC}" fi - -# Check Docker containers -echo "" -echo -e "${YELLOW}Docker Containers:${NC}" -docker ps --filter name=parentflow --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - -# Summary -echo "" -echo "==========================================" -echo -e "${GREEN}Production Environment Started!${NC}" echo "==========================================" echo "" -echo "Services:" -echo " Backend API: http://localhost:3020" -echo " Frontend: http://localhost:3030" -echo " PostgreSQL: localhost:5432" -echo " Redis: localhost:6379" -echo " MongoDB: localhost:27017" -echo " MinIO: localhost:9000" -echo " MinIO Console: localhost:9001" +echo "Service URLs:" +echo " Backend API: http://localhost:3020" +echo " Frontend: http://localhost:3030" +echo " Admin Dashboard: http://localhost:3335" +echo " MinIO Console: http://localhost:9001" echo "" echo "Management Commands:" -echo " PM2 status: pm2 status" -echo " PM2 logs: pm2 logs" -echo " PM2 restart: pm2 restart all" -echo " PM2 stop: pm2 stop all" -echo " Docker logs: docker logs parentflow-postgres-prod" -echo " Stop all: pm2 stop all && docker-compose -f docker-compose.production.yml down" +echo " View logs: pm2 logs" +echo " Monitor: pm2 monit" +echo " List processes: pm2 list" +echo " Restart all: pm2 restart all" +echo " Stop all: ./stop-production.sh" echo "" -echo "Domains (configure in Nginx/DNS):" -echo " Frontend: web.parentflowapp.com → localhost:3030" -echo " Backend: api.parentflowapp.com → localhost:3020" +echo "Log files:" +echo " /var/log/parentflow/api-*.log" +echo " /var/log/parentflow/frontend-*.log" +echo " /var/log/parentflow/admin-*.log" echo "" -echo -e "${GREEN}✓ Startup complete!${NC}" \ No newline at end of file + +# Create log directory if it doesn't exist +mkdir -p /var/log/parentflow + +log "Services started at $(date)" \ No newline at end of file diff --git a/stop-production.sh b/stop-production.sh index 745eb26..0d13c2d 100755 --- a/stop-production.sh +++ b/stop-production.sh @@ -1,7 +1,7 @@ #!/bin/bash # ParentFlow Production Stop Script -# Stops PM2 processes and Docker containers gracefully +# Stops all production services gracefully set -e @@ -10,68 +10,138 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +CYAN='\033[0;36m' +NC='\033[0m' +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +success() { + echo -e "${GREEN}✓${NC} $1" +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# Header +echo "" echo "==========================================" -echo "Stopping ParentFlow Production" +echo " Stopping ParentFlow Production " echo "==========================================" echo "" # Step 1: Stop PM2 processes -echo -e "${BLUE}Step 1: Stopping PM2 processes...${NC}" +log "${CYAN}Step 1: Stopping PM2 services...${NC}" if command -v pm2 &> /dev/null; then - pm2 stop all 2>/dev/null || true - echo -e "${GREEN}✓ PM2 processes stopped${NC}" + # Show current status + echo "Current PM2 processes:" + pm2 list + + # Stop all PM2 processes + pm2 stop all 2>/dev/null || warning "No PM2 processes were running" + + # Delete all PM2 processes + pm2 delete all 2>/dev/null || warning "No PM2 processes to delete" + + # Kill PM2 daemon + pm2 kill + + success "PM2 services stopped" else - echo -e "${YELLOW}Warning: PM2 not found${NC}" + warning "PM2 not found" fi -# Step 2: Stop Docker containers -echo "" -echo -e "${BLUE}Step 2: Stopping Docker containers...${NC}" -if docker compose version &> /dev/null; then - docker compose -f docker-compose.production.yml down -else - docker-compose -f docker-compose.production.yml down 2>/dev/null || true -fi +# Step 2: Stop Docker services +log "${CYAN}Step 2: Stopping Docker services...${NC}" +DEPLOY_DIR="/root/parentflow-production" -if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ Docker containers stopped${NC}" -else - echo -e "${YELLOW}Warning: Failed to stop some Docker containers${NC}" -fi - -# Verify everything is stopped -echo "" -echo -e "${BLUE}Verification:${NC}" - -# Check PM2 -if command -v pm2 &> /dev/null; then - RUNNING_PROCESSES=$(pm2 jlist 2>/dev/null | jq -r '.[] | select(.pm2_env.status == "online") | .name' | wc -l) - if [ "$RUNNING_PROCESSES" -eq 0 ]; then - echo -e "${GREEN}✓ No PM2 processes running${NC}" +# Try to find docker-compose file +if [ -f "$DEPLOY_DIR/docker-compose.production.yml" ]; then + cd "$DEPLOY_DIR" + if docker compose version &> /dev/null; then + docker compose -f docker-compose.production.yml down else - echo -e "${YELLOW}Warning: $RUNNING_PROCESSES PM2 processes still running${NC}" + docker-compose -f docker-compose.production.yml down fi -fi - -# Check Docker -RUNNING_CONTAINERS=$(docker ps --filter name=parentflow --format "{{.Names}}" | wc -l) -if [ "$RUNNING_CONTAINERS" -eq 0 ]; then - echo -e "${GREEN}✓ No ParentFlow Docker containers running${NC}" + success "Docker services stopped" +elif [ -f "./docker-compose.production.yml" ]; then + if docker compose version &> /dev/null; then + docker compose -f docker-compose.production.yml down + else + docker-compose -f docker-compose.production.yml down + fi + success "Docker services stopped" else - echo -e "${YELLOW}Warning: $RUNNING_CONTAINERS containers still running${NC}" - docker ps --filter name=parentflow + warning "Docker compose file not found, skipping..." fi +# Step 3: Kill any remaining Node processes on production ports +log "${CYAN}Step 3: Cleaning up remaining processes...${NC}" + +kill_port() { + local port=$1 + local name=$2 + + if lsof -i:$port > /dev/null 2>&1; then + log "Stopping $name on port $port..." + lsof -ti:$port | xargs -r kill -9 + success "$name stopped" + else + echo " $name not running on port $port" + fi +} + +kill_port 3020 "Backend API" +kill_port 3030 "Frontend" +kill_port 3335 "Admin Dashboard" + +# Step 4: Clean up temporary files +log "${CYAN}Step 4: Cleaning up temporary files...${NC}" +rm -rf /tmp/pm2-* 2>/dev/null || true +rm -rf /tmp/next-* 2>/dev/null || true +success "Temporary files cleaned" + +# Step 5: Verify all services are stopped +log "${CYAN}Step 5: Verifying all services are stopped...${NC}" + +check_port() { + local port=$1 + local name=$2 + + if lsof -i:$port > /dev/null 2>&1; then + warning "$name still running on port $port" + return 1 + else + success "$name stopped (port $port free)" + return 0 + fi +} + +ALL_STOPPED=true +check_port 3020 "Backend API" || ALL_STOPPED=false +check_port 3030 "Frontend" || ALL_STOPPED=false +check_port 3335 "Admin Dashboard" || ALL_STOPPED=false +check_port 6379 "Redis" || true # Redis might be used by other services +check_port 27017 "MongoDB" || true # MongoDB might be used by other services +check_port 9000 "MinIO" || true # MinIO might be used by other services + +# Final summary echo "" echo "==========================================" -echo -e "${GREEN}Production environment stopped${NC}" +if [ "$ALL_STOPPED" = true ]; then + echo -e "${GREEN} All Services Stopped Successfully! ${NC}" +else + echo -e "${YELLOW} Services Stopped (Check Warnings) ${NC}" +fi echo "==========================================" echo "" -echo "To completely remove Docker volumes (WARNING: deletes data):" -echo " docker-compose -f docker-compose.production.yml down -v" +echo "To restart services, run:" +echo " ./start-production.sh" echo "" -echo "To remove PM2 processes from startup:" -echo " pm2 delete all" -echo " pm2 save" \ No newline at end of file +log "Services stopped at $(date)" \ No newline at end of file