commit 8b341a9f3b747f0264ed989b7bf6dc8056186bad Author: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Tue Dec 23 21:05:31 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56a655a --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Frontend +node_modules/ +dist/ +.env +.env.local + +# Backend +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +ENV/ +env/ +*.egg-info/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +my-recipes-chart/\ +helm/ \ No newline at end of file diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..3d68271 --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,121 @@ +steps: + build-frontend: + name: Build & Push Frontend + image: woodpeckerci/plugin-kaniko + when: + branch: [ master, develop ] + event: [ push, pull_request, tag ] + path: + include: [ frontend/** ] + settings: + registry: harbor.dvirlabs.com + repo: my-apps/${CI_REPO_NAME}-frontend + dockerfile: frontend/Dockerfile + context: frontend + tags: + - latest + - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}} + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + + build-backend: + name: Build & Push Backend + image: woodpeckerci/plugin-kaniko + when: + branch: [ master, develop ] + event: [ push, pull_request, tag ] + path: + include: [ backend/** ] + settings: + registry: harbor-core.dev-tools.svc.cluster.local + repo: my-apps/${CI_REPO_NAME}-backend + dockerfile: backend/Dockerfile + context: backend + tags: + - latest + - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}} + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + + update-values-frontend: + name: Update frontend tag in values.yaml + image: alpine:3.19 + when: + branch: [ master, develop ] + event: [ push ] + path: + include: [ frontend/** ] + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GIT_TOKEN: + from_secret: GIT_TOKEN + commands: + - apk add --no-cache git yq + - git config --global user.name "woodpecker-bot" + - git config --global user.email "ci@dvirlabs.com" + - git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git" + - cd my-apps + - | + TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" + echo "💡 Setting frontend tag to: $TAG" + yq -i ".frontend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + git add manifests/${CI_REPO_NAME}/values.yaml + git commit -m "frontend: update tag to $TAG" || echo "No changes" + git push origin HEAD + + update-values-backend: + name: Update backend tag in values.yaml + image: alpine:3.19 + when: + branch: [ master, develop ] + event: [ push ] + path: + include: [ backend/** ] + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GIT_TOKEN: + from_secret: GIT_TOKEN + commands: + - apk add --no-cache git yq + - git config --global user.name "woodpecker-bot" + - git config --global user.email "ci@dvirlabs.com" + - | + if [ ! -d "my-apps" ]; then + git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git" + fi + - cd my-apps + - | + TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" + echo "💡 Setting backend tag to: $TAG" + yq -i ".backend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + git add manifests/${CI_REPO_NAME}/values.yaml + git commit -m "backend: update tag to $TAG" || echo "No changes" + git push origin HEAD + + + trigger-gitops-via-push: + when: + branch: [ master, develop ] + event: [ push ] + name: Trigger apps-gitops via Git push + image: alpine/git + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GIT_TOKEN: + from_secret: GIT_TOKEN + commands: | + git config --global user.name "woodpecker-bot" + git config --global user.email "ci@dvirlabs.com" + git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/apps-gitops.git" + cd apps-gitops + echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger + git add .trigger + git commit -m "ci: trigger apps-gitops build" || echo "no changes" + git push origin HEAD \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7851db0 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# IP Subnet Calculator + +A full-stack IP subnet calculator application with React + Vite frontend and FastAPI backend. + +## Features + +- ✅ Calculate Network ID +- ✅ Calculate Broadcast Address +- ✅ Show First and Last Usable IP addresses +- ✅ Display Total Hosts and Usable Hosts +- ✅ Show Subnet Mask and Wildcard Mask +- ✅ Identify Network Class (A, B, C, D, E) +- ✅ Detect IP Type (Unicast, Multicast, Loopback, etc.) +- ✅ Check if IP is Private or Public +- ✅ Support both CIDR notation and Subnet Mask input + +## Project Structure + +``` +ip-calculator/ +├── backend/ # FastAPI backend +│ ├── main.py +│ ├── requirements.txt +│ └── README.md +└── frontend/ # React + Vite frontend + ├── src/ + │ ├── App.jsx + │ ├── App.css + │ ├── main.jsx + │ └── index.css + ├── index.html + ├── package.json + └── vite.config.js +``` + +## Setup Instructions + +### Backend Setup + +1. Navigate to the backend directory: +```bash +cd backend +``` + +2. Create and activate a virtual environment: +```bash +# Windows +python -m venv venv +venv\Scripts\activate + +# Linux/Mac +python -m venv venv +source venv/bin/activate +``` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +4. Run the FastAPI server: +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +The API will be available at `http://localhost:8000` +API documentation at `http://localhost:8000/docs` + +### Frontend Setup + +1. Navigate to the frontend directory: +```bash +cd frontend +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Run the development server: +```bash +npm run dev +``` + +The application will be available at `http://localhost:3000` + +## Usage + +1. Start the backend server (port 8000) +2. Start the frontend server (port 3000) +3. Open your browser and go to `http://localhost:3000` +4. Enter an IP address (e.g., 192.168.1.1) +5. Choose input method: + - **CIDR Notation**: Enter a value between 1-32 (e.g., 24) + - **Subnet Mask**: Enter a subnet mask (e.g., 255.255.255.0) +6. Click "Calculate Subnet" to see all subnet information + +## API Endpoints + +### POST /calculate +Calculate subnet information for a given IP address. + +**Request Body:** +```json +{ + "ip_address": "192.168.1.1", + "cidr": 24 +} +``` +or +```json +{ + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0" +} +``` + +**Response:** +```json +{ + "ip_address": "192.168.1.1", + "cidr": 24, + "subnet_mask": "255.255.255.0", + "network_id": "192.168.1.0", + "broadcast_address": "192.168.1.255", + "first_usable_ip": "192.168.1.1", + "last_usable_ip": "192.168.1.254", + "total_hosts": 256, + "usable_hosts": 254, + "wildcard_mask": "0.0.0.255", + "network_class": "C", + "is_private": true, + "ip_type": "Unicast" +} +``` + +## Technologies Used + +### Backend +- **FastAPI**: Modern, fast web framework for building APIs +- **Uvicorn**: ASGI server for running FastAPI +- **Pydantic**: Data validation using Python type annotations +- **ipaddress**: Python standard library for IP address manipulation + +### Frontend +- **React**: UI library for building interactive interfaces +- **Vite**: Fast build tool and development server +- **Axios**: HTTP client for API requests +- **CSS3**: Modern styling with gradients and animations + +## License + +MIT diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..c306081 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY main.py . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..d3d2a20 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,26 @@ +# IP Subnet Calculator - Backend + +## Setup + +1. Create a virtual environment: +```bash +python -m venv venv +``` + +2. Activate the virtual environment: +- Windows: `venv\Scripts\activate` +- Linux/Mac: `source venv/bin/activate` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +## Run the server + +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +API will be available at http://localhost:8000 +API documentation at http://localhost:8000/docs diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..7266721 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,264 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, validator +import ipaddress +import re +from typing import Optional + +app = FastAPI(title="IP Subnet Calculator API") + +# CORS configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +class SubnetInput(BaseModel): + ip_address: str + cidr: Optional[int] = None + subnet_mask: Optional[str] = None + + @validator('ip_address') + def validate_ip_address(cls, v): + if not v or not v.strip(): + raise ValueError("IP address cannot be empty") + + # Check basic format + ip_pattern = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' + if not re.match(ip_pattern, v): + raise ValueError("Invalid IP address format. Expected format: xxx.xxx.xxx.xxx") + + # Check each octet is valid (0-255) + octets = v.split('.') + for i, octet in enumerate(octets): + try: + num = int(octet) + if num < 0 or num > 255: + raise ValueError(f"Octet {i+1} must be between 0 and 255. Got: {num}") + except ValueError: + raise ValueError(f"Invalid octet value: {octet}") + + # Validate with ipaddress library + try: + ipaddress.IPv4Address(v) + except Exception as e: + raise ValueError(f"Invalid IP address: {str(e)}") + + return v + + @validator('cidr') + def validate_cidr(cls, v): + if v is not None: + if not isinstance(v, int): + raise ValueError("CIDR must be a number") + if v < 0 or v > 32: + raise ValueError("CIDR must be between 0 and 32") + return v + + @validator('subnet_mask') + def validate_subnet_mask(cls, v): + if v is not None: + if not v.strip(): + raise ValueError("Subnet mask cannot be empty") + + # Check format + parts = v.split('.') + if len(parts) != 4: + raise ValueError("Subnet mask must have 4 octets (e.g., 255.255.255.0)") + + # Validate with ipaddress + try: + mask = ipaddress.IPv4Address(v) + # Check if it's a valid subnet mask (contiguous 1s followed by 0s) + mask_int = int(mask) + # Valid masks have all 1s on the left and all 0s on the right + # Check by converting to binary and verifying pattern + binary = bin(mask_int)[2:].zfill(32) + if '01' in binary: # If there's a 0 followed by 1, it's invalid + raise ValueError("Invalid subnet mask: bits must be contiguous (all 1s followed by all 0s)") + except Exception as e: + raise ValueError(f"Invalid subnet mask: {str(e)}") + + return v + +class SubnetInfo(BaseModel): + ip_address: str + ip_address_binary: str + cidr: int + subnet_mask: str + subnet_mask_binary: str + network_id: str + network_id_binary: str + broadcast_address: str + broadcast_binary: str + first_usable_ip: str + last_usable_ip: str + total_hosts: int + usable_hosts: int + wildcard_mask: str + wildcard_binary: str + network_class: str + is_private: bool + ip_type: str + +@app.get("/") +def read_root(): + return {"message": "IP Subnet Calculator API", "version": "1.0.0"} + +@app.post("/calculate", response_model=SubnetInfo) +def calculate_subnet(subnet_input: SubnetInput): + try: + # Validate that at least one of CIDR or subnet mask is provided + if subnet_input.cidr is None and subnet_input.subnet_mask is None: + raise HTTPException( + status_code=400, + detail="You must provide either CIDR notation (e.g., 24) or subnet mask (e.g., 255.255.255.0)" + ) + + # Determine CIDR notation + if subnet_input.cidr is not None: + cidr = subnet_input.cidr + ip_with_cidr = f"{subnet_input.ip_address}/{cidr}" + elif subnet_input.subnet_mask is not None: + # Convert subnet mask to CIDR + try: + subnet_mask = ipaddress.IPv4Address(subnet_input.subnet_mask) + cidr = sum(bin(int(x)).count('1') for x in str(subnet_mask).split('.')) + + # Validate CIDR is reasonable + if cidr == 0: + raise HTTPException( + status_code=400, + detail="Invalid subnet mask: 0.0.0.0 is not a valid subnet mask" + ) + + ip_with_cidr = f"{subnet_input.ip_address}/{cidr}" + except Exception as e: + raise HTTPException( + status_code=400, + detail=f"Invalid subnet mask format: {str(e)}" + ) + + # Validate the IP address + try: + ip_obj = ipaddress.IPv4Address(subnet_input.ip_address) + except Exception as e: + raise HTTPException( + status_code=400, + detail=f"Invalid IP address format: {str(e)}" + ) + + # Create network object + try: + network = ipaddress.IPv4Network(ip_with_cidr, strict=False) + except Exception as e: + raise HTTPException( + status_code=400, + detail=f"Cannot create network from IP {subnet_input.ip_address} with CIDR /{cidr}: {str(e)}" + ) + + # Calculate network class + first_octet = int(subnet_input.ip_address.split('.')[0]) + if first_octet < 128: + network_class = "A" + elif first_octet < 192: + network_class = "B" + elif first_octet < 224: + network_class = "C" + elif first_octet < 240: + network_class = "D (Multicast)" + else: + network_class = "E (Reserved)" + + # Calculate wildcard mask + subnet_mask_int = int(network.netmask) + wildcard_int = (2**32 - 1) ^ subnet_mask_int + wildcard_mask = str(ipaddress.IPv4Address(wildcard_int)) + + # Get first and last usable IPs - Calculate mathematically to avoid memory issues + total_hosts = network.num_addresses + + if cidr == 32: + # Single host + first_usable = str(network.network_address) + last_usable = str(network.network_address) + usable_hosts = 1 + elif cidr == 31: + # Point-to-point network (RFC 3021) + first_usable = str(network.network_address) + last_usable = str(network.broadcast_address) + usable_hosts = 2 + else: + # Calculate first and last usable IPs mathematically + # First usable = network address + 1 + # Last usable = broadcast address - 1 + network_int = int(network.network_address) + broadcast_int = int(network.broadcast_address) + + first_usable = str(ipaddress.IPv4Address(network_int + 1)) + last_usable = str(ipaddress.IPv4Address(broadcast_int - 1)) + usable_hosts = total_hosts - 2 # Exclude network and broadcast addresses + + # Helper function to convert IP to binary representation + def ip_to_binary(ip_str): + ip = ipaddress.IPv4Address(ip_str) + # Convert to 32-bit binary and format with dots every 8 bits + binary = bin(int(ip))[2:].zfill(32) + return f"{binary[0:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}" + + # Determine IP type + if ip_obj.is_loopback: + ip_type = "Loopback" + elif ip_obj.is_multicast: + ip_type = "Multicast" + elif ip_obj.is_link_local: + ip_type = "Link-Local" + elif ip_obj.is_reserved: + ip_type = "Reserved" + else: + ip_type = "Unicast" + + return SubnetInfo( + ip_address=subnet_input.ip_address, + ip_address_binary=ip_to_binary(subnet_input.ip_address), + cidr=cidr, + subnet_mask=str(network.netmask), + subnet_mask_binary=ip_to_binary(str(network.netmask)), + network_id=str(network.network_address), + network_id_binary=ip_to_binary(str(network.network_address)), + broadcast_address=str(network.broadcast_address), + broadcast_binary=ip_to_binary(str(network.broadcast_address)), + first_usable_ip=first_usable, + last_usable_ip=last_usable, + total_hosts=network.num_addresses, + usable_hosts=usable_hosts, + wildcard_mask=wildcard_mask, + wildcard_binary=ip_to_binary(wildcard_mask), + network_class=network_class, + is_private=ip_obj.is_private, + ip_type=ip_type + ) + + except HTTPException: + # Re-raise HTTP exceptions as-is + raise + except ValueError as e: + # Pydantic validation errors + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + # Unexpected errors + raise HTTPException( + status_code=500, + detail=f"Unexpected error during calculation: {str(e)}" + ) + +@app.get("/health") +def health_check(): + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..5313262 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..302bec9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: ip-calculator-backend + ports: + - "8000:8000" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: ip-calculator-frontend + ports: + - "3000:80" + depends_on: + - backend + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..f4b0b8c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +# Build stage +FROM node:18-alpine as build + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy application code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built files from build stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..90e22ff --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + IP Subnet Calculator + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..127a806 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..60ae34b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1813 @@ +{ + "name": "ip-calculator-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ip-calculator-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8" + } + }, + "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==", + "dev": true, + "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/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@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-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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==", + "dev": true, + "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.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "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, + "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==", + "dev": true, + "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==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "dev": true, + "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" + } + ] + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true + }, + "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==", + "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==", + "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==", + "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==", + "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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "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==", + "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==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3659d9f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "ip-calculator-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.2" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8" + } +} diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..9b9c880 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,515 @@ +.app { + width: 100%; +} + +.container { + background: white; + border-radius: 20px; + padding: 40px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +.title { + font-size: 2.5rem; + color: #667eea; + text-align: center; + margin-bottom: 30px; + font-weight: 700; +} + +.mode-toggle { + display: flex; + gap: 15px; + max-width: 600px; + margin: 0 auto 30px; + padding: 8px; + background: #f8f9fa; + border-radius: 12px; +} + +.mode-btn { + flex: 1; + padding: 14px; + border: 2px solid transparent; + background: white; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + color: #666; +} + +.mode-btn:hover { + background: #f0f0f0; +} + +.mode-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.form { + max-width: 600px; + margin: 0 auto 40px; +} + +.input-group { + margin-bottom: 20px; +} + +.input-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #333; + font-size: 0.95rem; +} + +.input-group input { + width: 100%; + padding: 12px 16px; + font-size: 1rem; + border: 2px solid #e0e0e0; + border-radius: 8px; + transition: all 0.3s ease; + font-family: 'Courier New', monospace; +} + +.input-group input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.input-hint { + display: block; + margin-top: 6px; + font-size: 0.85rem; + color: #666; + font-style: italic; +} + +.highlight-box { + background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%); + border: 2px dashed #667eea; + border-radius: 12px; + padding: 20px; +} + +.highlight-box label { + color: #667eea; + font-size: 1.05rem; +} + +.highlight-box .input-hint { + background: white; + padding: 10px; + border-radius: 6px; + border-left: 3px solid #667eea; + font-size: 0.9rem; + color: #555; +} + +.input-type-selector { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.type-btn { + flex: 1; + padding: 12px; + border: 2px solid #e0e0e0; + background: white; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.type-btn:hover { + border-color: #667eea; +} + +.type-btn.active { + background: #667eea; + color: white; + border-color: #667eea; +} + +.calculate-btn { + width: 100%; + padding: 14px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.calculate-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); +} + +.calculate-btn:active:not(:disabled) { + transform: translateY(0); +} + +.calculate-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.error-message { + background: #fee; + border: 2px solid #fcc; + color: #c33; + padding: 16px; + border-radius: 8px; + margin-bottom: 20px; + text-align: center; + font-weight: 600; +} + +.detailed-error { + text-align: left; + line-height: 1.6; +} + +.error-title { + font-size: 1.1rem; + font-weight: 700; + margin-bottom: 12px; + color: #c33; +} + +.error-reason { + background: #fff; + padding: 10px; + border-radius: 6px; + margin-bottom: 12px; + border-left: 4px solid #c33; +} + +.error-info { + background: #fff; + padding: 12px; + border-radius: 6px; + margin-bottom: 12px; + font-size: 0.95rem; +} + +.error-info > div { + padding: 4px 0; +} + +.error-suggestions { + background: #e8f4fd; + padding: 12px; + border-radius: 6px; + border-left: 4px solid #2196F3; + color: #333; +} + +.error-suggestions strong { + color: #1976D2; + display: block; + margin-bottom: 8px; +} + +.error-suggestions ul { + margin: 0; + padding-left: 20px; +} + +.error-suggestions li { + padding: 4px 0; + color: #555; +} + +.warning-message { + background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); + border: 2px solid #ffc107; + border-radius: 12px; + padding: 16px; + margin-bottom: 25px; + display: flex; + gap: 15px; + align-items: flex-start; + box-shadow: 0 4px 12px rgba(255, 193, 7, 0.2); +} + +.warning-icon { + font-size: 2rem; + flex-shrink: 0; +} + +.warning-content { + flex: 1; +} + +.warning-content strong { + display: block; + color: #856404; + font-size: 1.05rem; + margin-bottom: 8px; +} + +.warning-text { + color: #664d03; + line-height: 1.6; + padding: 6px 0; + font-size: 0.95rem; +} + +.results { + margin-top: 40px; + animation: slideIn 0.4s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.results-title { + font-size: 1.8rem; + color: #333; + margin-bottom: 25px; + text-align: center; + font-weight: 700; +} + +.section-title { + font-size: 1.3rem; + color: #667eea; + margin: 30px 0 15px 0; + font-weight: 700; + text-align: center; +} + +.binary-section { + margin-bottom: 30px; +} + +.binary-grid { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; +} + +.binary-card { + background: #f0f4ff; + border: 2px solid #667eea; + border-radius: 8px; + padding: 12px 16px; + transition: all 0.3s ease; +} + +.binary-card:hover { + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2); +} + +.binary-label { + font-size: 0.75rem; + color: #667eea; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; +} + +.binary-decimal { + font-size: 0.95rem; + color: #333; + font-weight: 600; + font-family: 'Courier New', monospace; + margin-bottom: 6px; +} + +.binary-value { + font-size: 0.9rem; + color: #666; + font-family: 'Courier New', monospace; + letter-spacing: 1px; + word-break: break-all; +} + +.results-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.info-card { + background: #f8f9fa; + border: 2px solid #e9ecef; + border-radius: 12px; + padding: 20px; + transition: all 0.3s ease; +} + +.info-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); +} + +.info-card.highlight { + background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + border-color: #667eea; +} + +.info-label { + font-size: 0.85rem; + color: #666; + margin-bottom: 8px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.info-value { + font-size: 1.2rem; + color: #333; + font-weight: 700; + font-family: 'Courier New', monospace; + word-break: break-all; +} + +.division-summary { + margin-bottom: 30px; +} + +.subnets-table, +.ranges-table { + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; +} + +.ranges-table .table-header { + grid-template-columns: 60px 2fr 3fr 2fr 1fr; +} + +.ranges-table .table-body { + max-height: 500px; + overflow-y: auto; + overflow-x: hidden; +} + +.ranges-table .table-row { + grid-template-columns: 60px 2fr 3fr 2fr 1fr; +} + +.network-address { + font-weight: 700; + color: #667eea; +} + +.range-id { + font-weight: 700; + color: #667eea; + text-align: center; + font-size: 1rem; +} + +.table-header { + display: grid; + grid-template-columns: 80px 2fr 2.5fr 1.5fr 1fr; + gap: 10px; + padding: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-weight: 700; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + position: sticky; + top: 0; + z-index: 10; +} + +.table-row { + display: grid; + grid-template-columns: 80px 2fr 2.5fr 1.5fr 1fr; + gap: 10px; + padding: 14px 16px; + border-bottom: 1px solid #e9ecef; + transition: background 0.2s ease; + font-size: 0.9rem; +} + +.table-row:hover { + background: #f8f9fa; +} + +.table-row:last-child { + border-bottom: none; +} + +.table-cell { + font-family: 'Courier New', monospace; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.subnet-id { + font-weight: 700; + color: #667eea; + text-align: center; + font-size: 1.1rem; +} + +.subnet-network { + font-weight: 700; + color: #667eea; +} + +@media (max-width: 768px) { + .container { + padding: 20px; + } + + .title { + font-size: 1.8rem; + } + + .results-grid { + grid-template-columns: 1fr; + } + + .table-header, + .table-row { + grid-template-columns: 1fr; + gap: 5px; + } + + .table-cell { + white-space: normal; + word-break: break-all; + } + + .mode-toggle { + flex-direction: column; + gap: 10px; + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..b9c84e3 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,362 @@ +import { useState } from 'react' +import axios from 'axios' +import './App.css' + +const API_URL = 'http://localhost:8000' + +function App() { + const [ipAddress, setIpAddress] = useState('') + const [cidr, setCidr] = useState('24') + const [subnetMask, setSubnetMask] = useState('') + const [inputType, setInputType] = useState('cidr') // 'cidr' or 'mask' + const [result, setResult] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + // Validate IP address format + const validateIpAddress = (ip) => { + if (!ip || !ip.trim()) { + return 'IP address is required' + } + + const ipPattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ + if (!ipPattern.test(ip)) { + return 'Invalid IP format. Expected: xxx.xxx.xxx.xxx' + } + + const octets = ip.split('.') + for (let i = 0; i < octets.length; i++) { + const num = parseInt(octets[i]) + if (isNaN(num) || num < 0 || num > 255) { + return `Invalid octet ${i + 1}: must be between 0-255` + } + } + + return null + } + + // Validate subnet mask format + const validateSubnetMask = (mask) => { + if (!mask || !mask.trim()) { + return 'Subnet mask is required' + } + + const parts = mask.split('.') + if (parts.length !== 4) { + return 'Subnet mask must have 4 octets' + } + + // Check each octet is 0-255 + for (let part of parts) { + const num = parseInt(part) + if (isNaN(num) || num < 0 || num > 255) { + return 'Each octet must be between 0-255' + } + } + + // Common valid subnet masks + const validMasks = [ + '255.255.255.255', '255.255.255.254', '255.255.255.252', '255.255.255.248', + '255.255.255.240', '255.255.255.224', '255.255.255.192', '255.255.255.128', + '255.255.255.0', '255.255.254.0', '255.255.252.0', '255.255.248.0', + '255.255.240.0', '255.255.224.0', '255.255.192.0', '255.255.128.0', + '255.255.0.0', '255.254.0.0', '255.252.0.0', '255.248.0.0', + '255.240.0.0', '255.224.0.0', '255.192.0.0', '255.128.0.0', + '255.0.0.0', '254.0.0.0', '252.0.0.0', '248.0.0.0', + '240.0.0.0', '224.0.0.0', '192.0.0.0', '128.0.0.0', '0.0.0.0' + ] + + if (!validMasks.includes(mask)) { + return 'Invalid subnet mask (bits must be contiguous)' + } + + return null + } + + const calculateSubnet = async (e) => { + e.preventDefault() + setError('') + setResult(null) + + // Frontend validation + const ipError = validateIpAddress(ipAddress) + if (ipError) { + setError(ipError) + return + } + + if (inputType === 'cidr') { + const cidrNum = parseInt(cidr) + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + setError('CIDR must be between 0 and 32') + return + } + } else { + const maskError = validateSubnetMask(subnetMask) + if (maskError) { + setError(maskError) + return + } + } + + setLoading(true) + + try { + const payload = { + ip_address: ipAddress, + } + + if (inputType === 'cidr') { + payload.cidr = parseInt(cidr) + } else { + payload.subnet_mask = subnetMask + } + + const response = await axios.post(`${API_URL}/calculate`, payload) + setResult(response.data) + } catch (err) { + const errorMessage = err.response?.data?.detail || err.message || 'Failed to calculate subnet' + setError(errorMessage) + } finally { + setLoading(false) + } + } + + const InfoCard = ({ label, value, highlight }) => ( +
+
{label}
+
{value}
+
+ ) + + const BinaryCard = ({ label, decimal, binary }) => ( +
+
{label}
+
{decimal}
+
{binary}
+
+ ) + + // Calculate ALL next sequential networks based on the current network + const calculateNetworkRanges = (result) => { + const networkId = result.network_id + const cidr = result.cidr + const totalAddresses = result.total_hosts + + // Parse network ID to get starting IP + const parts = networkId.split('.').map(Number) + let networkNum = (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3] + + const networks = [] + + // Calculate all possible networks until end of IPv4 space + // For performance, limit to 10000 networks max + const maxNetworks = 10000 + let count = 0 + + // Generate next sequential networks + while (count < maxNetworks) { + const currentNetworkNum = networkNum + (count * totalAddresses) + + // Check if we're still within valid IPv4 range + const broadcastNum = currentNetworkNum + totalAddresses - 1 + if (currentNetworkNum > 0xFFFFFFFF || broadcastNum > 0xFFFFFFFF) break + + // Convert number back to IP address + const networkAddress = `${(currentNetworkNum >>> 24) & 0xFF}.${(currentNetworkNum >>> 16) & 0xFF}.${(currentNetworkNum >>> 8) & 0xFF}.${currentNetworkNum & 0xFF}` + const broadcastAddress = `${(broadcastNum >>> 24) & 0xFF}.${(broadcastNum >>> 16) & 0xFF}.${(broadcastNum >>> 8) & 0xFF}.${broadcastNum & 0xFF}` + + // Calculate first and last usable IPs + let firstUsable, lastUsable + if (cidr === 32) { + firstUsable = networkAddress + lastUsable = networkAddress + } else if (cidr === 31) { + firstUsable = networkAddress + lastUsable = broadcastAddress + } else { + const firstUsableNum = currentNetworkNum + 1 + const lastUsableNum = broadcastNum - 1 + firstUsable = `${(firstUsableNum >>> 24) & 0xFF}.${(firstUsableNum >>> 16) & 0xFF}.${(firstUsableNum >>> 8) & 0xFF}.${firstUsableNum & 0xFF}` + lastUsable = `${(lastUsableNum >>> 24) & 0xFF}.${(lastUsableNum >>> 16) & 0xFF}.${(lastUsableNum >>> 8) & 0xFF}.${lastUsableNum & 0xFF}` + } + + networks.push({ + networkAddress: `${networkAddress}/${cidr}`, + firstUsable, + lastUsable, + broadcastAddress, + usableHosts: result.usable_hosts + }) + + count++ + } + + return networks + } + + return ( +
+
+

🌐 IP Subnet Calculator

+ +
+
+ + setIpAddress(e.target.value)} + placeholder="e.g., 192.168.1.1" + required + /> +
+ +
+ + +
+ + {inputType === 'cidr' ? ( +
+ + setCidr(e.target.value)} + required + /> +
+ ) : ( +
+ + setSubnetMask(e.target.value)} + placeholder="e.g., 255.255.255.0" + required + /> +
+ )} + + +
+ + {error && ( +
+ ⚠️ {error} +
+ )} + + {result && ( +
+

Subnet Information

+ + {/* Binary Representation Section */} +
+

Binary Representation

+
+ + + + + +
+
+ + {/* Main Information Section */} +

Network Details

+
+ + + + + + + + + + + + + + + + +
+ + {/* Next Networks Section */} +

+ 📊 All Next Sequential Networks ({calculateNetworkRanges(result).length.toLocaleString()} networks) +

+
+
+
#
+
Network
+
Usable Range
+
Broadcast
+
Hosts
+
+
+ {calculateNetworkRanges(result).map((network, index) => ( +
+
{index + 1}
+
{network.networkAddress}
+
{network.firstUsable} - {network.lastUsable}
+
{network.broadcastAddress}
+
{network.usableHosts.toLocaleString()}
+
+ ))} +
+
+
+ )} +
+
+ ) +} + +export default App diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..13910e1 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,22 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +#root { + width: 100%; + max-width: 1200px; + margin: 0 auto; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..5cc5991 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/src/subnetCalculator.js b/frontend/src/subnetCalculator.js new file mode 100644 index 0000000..a39f161 --- /dev/null +++ b/frontend/src/subnetCalculator.js @@ -0,0 +1,292 @@ +/** + * Pure subnet calculation utilities for IPv4 + * No DOM/UI dependencies - pure mathematical functions + */ + +/** + * Convert IPv4 string to 32-bit unsigned integer + * @param {string} ipStr - IPv4 address (e.g., "192.168.10.0") + * @returns {number} 32-bit unsigned integer + * @throws {Error} if invalid IPv4 format + * + * Example: ipToInt("192.168.10.0") => 3232238080 + */ +export function ipToInt(ipStr) { + if (!ipStr || typeof ipStr !== 'string') { + throw new Error('IP address must be a non-empty string') + } + + const octets = ipStr.split('.') + if (octets.length !== 4) { + throw new Error(`Invalid IPv4 format: ${ipStr}`) + } + + let result = 0 + for (let i = 0; i < 4; i++) { + const octet = parseInt(octets[i], 10) + if (isNaN(octet) || octet < 0 || octet > 255) { + throw new Error(`Invalid octet in IP ${ipStr}: ${octets[i]}`) + } + result = (result << 8) | octet + } + + return result >>> 0 // Ensure unsigned 32-bit +} + +/** + * Convert 32-bit unsigned integer to IPv4 string + * @param {number} ipInt - 32-bit unsigned integer + * @returns {string} IPv4 address + * + * Example: intToIp(3232238080) => "192.168.10.0" + */ +export function intToIp(ipInt) { + const uint = ipInt >>> 0 // Ensure unsigned + const octet1 = (uint >>> 24) & 0xFF + const octet2 = (uint >>> 16) & 0xFF + const octet3 = (uint >>> 8) & 0xFF + const octet4 = uint & 0xFF + return `${octet1}.${octet2}.${octet3}.${octet4}` +} + +/** + * Calculate subnet mask as 32-bit integer from prefix length + * @param {number} prefixLength - CIDR prefix (0-32) + * @returns {number} 32-bit subnet mask + * + * Example: maskFromPrefix(24) => 4294967040 (255.255.255.0) + */ +export function maskFromPrefix(prefixLength) { + if (prefixLength < 0 || prefixLength > 32) { + throw new Error(`Prefix length must be between 0 and 32, got: ${prefixLength}`) + } + + if (prefixLength === 0) { + return 0 + } + + return (0xFFFFFFFF << (32 - prefixLength)) >>> 0 +} + +/** + * Calculate the smallest prefix length needed for desired usable hosts + * For IPv4: usable hosts = 2^h - 2 (excluding network and broadcast) + * + * @param {number} desiredUsableHosts - Number of usable hosts needed + * @returns {number} CIDR prefix length (0-30) + * @throws {Error} if invalid input + * + * Examples: + * prefixFromHosts(50) => 26 (2^6 - 2 = 62 usable hosts) + * prefixFromHosts(10) => 28 (2^4 - 2 = 14 usable hosts) + * prefixFromHosts(1) => 30 (2^2 - 2 = 2 usable hosts) + * prefixFromHosts(254) => 24 (2^8 - 2 = 254 usable hosts) + */ +export function prefixFromHosts(desiredUsableHosts) { + if (!Number.isInteger(desiredUsableHosts) || desiredUsableHosts < 1) { + throw new Error(`Desired usable hosts must be a positive integer, got: ${desiredUsableHosts}`) + } + + // Maximum usable hosts in IPv4 is 2^30 - 2 (prefix /2) + if (desiredUsableHosts > 1073741822) { + throw new Error(`Desired hosts ${desiredUsableHosts} exceeds IPv4 maximum (1,073,741,822)`) + } + + // Find smallest h where 2^h - 2 >= desiredUsableHosts + // This means 2^h >= desiredUsableHosts + 2 + const requiredTotal = desiredUsableHosts + 2 + + // Find h: smallest integer where 2^h >= requiredTotal + let h = 0 + while ((1 << h) < requiredTotal) { + h++ + } + + // Prefix length = 32 - h + const prefix = 32 - h + + // Ensure we don't return /31 or /32 (no usable hosts in standard model) + if (prefix > 30) { + return 30 // /30 gives 2 usable hosts + } + + return prefix +} + +/** + * Align an IP address to the network boundary for a given prefix + * @param {number} ipInt - IP address as 32-bit integer + * @param {number} prefixLength - CIDR prefix length + * @returns {number} Aligned network address + * + * Example: alignToNetwork(ipToInt("192.168.10.5"), 24) => ipToInt("192.168.10.0") + */ +export function alignToNetwork(ipInt, prefixLength) { + const mask = maskFromPrefix(prefixLength) + return (ipInt & mask) >>> 0 +} + +/** + * Calculate subnets based on base network, desired hosts per subnet, and subnet count + * + * @param {string} baseNetwork - Base IPv4 address (e.g., "192.168.10.0") + * @param {number} desiredUsableHosts - Number of usable hosts needed per subnet + * @param {number} subnetCount - Number of subnets to generate + * @returns {Object} Subnet calculation results + * + * Example: + * calculateSubnets("192.168.10.0", 50, 4) + * => { + * prefixLength: 26, + * subnetMask: "255.255.255.192", + * totalAddressesPerSubnet: 64, + * usableHostsPerSubnet: 62, + * alignedBaseNetwork: "192.168.10.0", + * warnings: [], + * subnets: [ + * { index: 0, network: "192.168.10.0/26", firstUsable: "192.168.10.1", + * lastUsable: "192.168.10.62", broadcast: "192.168.10.63" }, + * { index: 1, network: "192.168.10.64/26", firstUsable: "192.168.10.65", + * lastUsable: "192.168.10.126", broadcast: "192.168.10.127" }, + * ... + * ] + * } + */ +export function calculateSubnets(baseNetwork, desiredUsableHosts, subnetCount) { + const warnings = [] + + // Validate inputs + if (!baseNetwork || typeof baseNetwork !== 'string') { + throw new Error('Base network must be a valid IPv4 string') + } + + if (!Number.isInteger(desiredUsableHosts) || desiredUsableHosts < 1) { + throw new Error('Desired usable hosts must be a positive integer') + } + + if (!Number.isInteger(subnetCount) || subnetCount < 1) { + throw new Error('Subnet count must be a positive integer') + } + + // Convert base network to integer + let baseInt + try { + baseInt = ipToInt(baseNetwork) + } catch (err) { + throw new Error(`Invalid base network: ${err.message}`) + } + + // Calculate required prefix length + const prefixLength = prefixFromHosts(desiredUsableHosts) + + if (prefixLength < 0 || prefixLength > 30) { + throw new Error(`Calculated prefix length ${prefixLength} is out of valid range (0-30)`) + } + + // Calculate subnet properties + const hostBits = 32 - prefixLength + const totalAddressesPerSubnet = Math.pow(2, hostBits) + const usableHostsPerSubnet = totalAddressesPerSubnet - 2 // Exclude network and broadcast + const subnetMaskInt = maskFromPrefix(prefixLength) + const subnetMask = intToIp(subnetMaskInt) + + // Align base network to proper boundary + const alignedBaseInt = alignToNetwork(baseInt, prefixLength) + const alignedBaseNetwork = intToIp(alignedBaseInt) + + if (alignedBaseInt !== baseInt) { + warnings.push( + `Base network ${baseNetwork} is not aligned to /${prefixLength} boundary. ` + + `Adjusted to ${alignedBaseNetwork}` + ) + } + + // Generate subnets + const subnets = [] + const maxIpInt = 0xFFFFFFFF // Maximum IPv4 address + + for (let i = 0; i < subnetCount; i++) { + const networkInt = alignedBaseInt + (i * totalAddressesPerSubnet) + + // Check for overflow + if (networkInt > maxIpInt) { + warnings.push( + `Subnet generation stopped at index ${i} due to IPv4 address space overflow. ` + + `Requested ${subnetCount} subnets but only ${i} can fit.` + ) + break + } + + const broadcastInt = networkInt + totalAddressesPerSubnet - 1 + + // Check if broadcast would overflow + if (broadcastInt > maxIpInt) { + warnings.push( + `Subnet ${i} would exceed IPv4 address space. ` + + `Generated ${i} subnets out of ${subnetCount} requested.` + ) + break + } + + const firstUsableInt = networkInt + 1 + const lastUsableInt = broadcastInt - 1 + + subnets.push({ + index: i, + network: `${intToIp(networkInt)}/${prefixLength}`, + networkAddress: intToIp(networkInt), + firstUsable: intToIp(firstUsableInt), + lastUsable: intToIp(lastUsableInt), + broadcast: intToIp(broadcastInt) + }) + } + + return { + prefixLength, + subnetMask, + totalAddressesPerSubnet, + usableHostsPerSubnet, + alignedBaseNetwork, + warnings, + subnets + } +} + +/** + * Test examples (can be run in console or test suite) + */ +export const testExamples = { + // Example 1: 50 hosts => /26 + example1: () => { + console.log('Example 1: 50 hosts per subnet') + const result = calculateSubnets('192.168.10.0', 50, 4) + console.log('Prefix:', result.prefixLength) // 26 + console.log('Subnet Mask:', result.subnetMask) // 255.255.255.192 + console.log('Usable Hosts:', result.usableHostsPerSubnet) // 62 + console.log('First subnet:', result.subnets[0]) + return result + }, + + // Example 2: Unaligned base network + example2: () => { + console.log('Example 2: Unaligned base network') + const result = calculateSubnets('192.168.10.5', 10, 2) + console.log('Aligned to:', result.alignedBaseNetwork) // 192.168.10.0 + console.log('Warnings:', result.warnings) + return result + }, + + // Example 3: Large subnet + example3: () => { + console.log('Example 3: 1000 hosts per subnet') + const result = calculateSubnets('10.0.0.0', 1000, 3) + console.log('Prefix:', result.prefixLength) // 22 + console.log('Usable Hosts:', result.usableHostsPerSubnet) // 1022 + return result + } +} + +// Uncomment to run examples: +// testExamples.example1() +// testExamples.example2() +// testExamples.example3() diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..40707c4 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + }, +}) diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..efa59fb --- /dev/null +++ b/values.yaml @@ -0,0 +1,72 @@ +# Basic configuration values for IP Calculator application + +# Application metadata +app: + name: ip-calculator + version: "1.0.0" + description: "IP Subnet Calculator with React + Vite frontend and FastAPI backend" + +# Backend configuration +backend: + port: 8000 + host: "0.0.0.0" + apiUrl: "http://localhost:8000" + cors: + enabled: true + origins: + - "http://localhost:3000" + - "http://localhost:5173" + +# Frontend configuration +frontend: + port: 3000 + apiUrl: "http://localhost:8000" + +# Development settings +development: + hotReload: true + debug: true + +# Production settings +production: + debug: false + minify: true + +# Docker configuration +docker: + backend: + image: "ip-calculator-backend" + tag: "latest" + frontend: + image: "ip-calculator-frontend" + tag: "latest" + +# Kubernetes/Helm configuration (for helm chart) +kubernetes: + namespace: "default" + replicas: + backend: 1 + frontend: 1 + resources: + backend: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "250m" + memory: "256Mi" + frontend: + limits: + cpu: "200m" + memory: "256Mi" + requests: + cpu: "100m" + memory: "128Mi" + +# Ingress configuration +ingress: + enabled: false + host: "ip-calculator.example.com" + tls: + enabled: false + secretName: "ip-calculator-tls"