From 8b341a9f3b747f0264ed989b7bf6dc8056186bad Mon Sep 17 00:00:00 2001
From: dvirlabs <114520947+dvirlabs@users.noreply.github.com>
Date: Tue, 23 Dec 2025 21:05:31 +0200
Subject: [PATCH] first commit
---
.gitignore | 30 +
.woodpecker.yaml | 121 ++
README.md | 154 +++
backend/Dockerfile | 16 +
backend/README.md | 26 +
backend/main.py | 264 +++++
backend/requirements.txt | 3 +
docker-compose.yml | 32 +
frontend/Dockerfile | 30 +
frontend/index.html | 12 +
frontend/nginx.conf | 14 +
frontend/package-lock.json | 1813 ++++++++++++++++++++++++++++++
frontend/package.json | 22 +
frontend/src/App.css | 515 +++++++++
frontend/src/App.jsx | 362 ++++++
frontend/src/index.css | 22 +
frontend/src/main.jsx | 10 +
frontend/src/subnetCalculator.js | 292 +++++
frontend/vite.config.js | 9 +
values.yaml | 72 ++
20 files changed, 3819 insertions(+)
create mode 100644 .gitignore
create mode 100644 .woodpecker.yaml
create mode 100644 README.md
create mode 100644 backend/Dockerfile
create mode 100644 backend/README.md
create mode 100644 backend/main.py
create mode 100644 backend/requirements.txt
create mode 100644 docker-compose.yml
create mode 100644 frontend/Dockerfile
create mode 100644 frontend/index.html
create mode 100644 frontend/nginx.conf
create mode 100644 frontend/package-lock.json
create mode 100644 frontend/package.json
create mode 100644 frontend/src/App.css
create mode 100644 frontend/src/App.jsx
create mode 100644 frontend/src/index.css
create mode 100644 frontend/src/main.jsx
create mode 100644 frontend/src/subnetCalculator.js
create mode 100644 frontend/vite.config.js
create mode 100644 values.yaml
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 }) => (
+
+ )
+
+ 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
+
+
+
+ {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"