first commit
This commit is contained in:
commit
8b341a9f3b
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -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/
|
||||
121
.woodpecker.yaml
Normal file
121
.woodpecker.yaml
Normal file
@ -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
|
||||
154
README.md
Normal file
154
README.md
Normal file
@ -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
|
||||
16
backend/Dockerfile
Normal file
16
backend/Dockerfile
Normal file
@ -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"]
|
||||
26
backend/README.md
Normal file
26
backend/README.md
Normal file
@ -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
|
||||
264
backend/main.py
Normal file
264
backend/main.py
Normal file
@ -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)
|
||||
3
backend/requirements.txt
Normal file
3
backend/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@ -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
|
||||
30
frontend/Dockerfile
Normal file
30
frontend/Dockerfile
Normal file
@ -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;"]
|
||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>IP Subnet Calculator</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
14
frontend/nginx.conf
Normal file
14
frontend/nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
1813
frontend/package-lock.json
generated
Normal file
1813
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
515
frontend/src/App.css
Normal file
515
frontend/src/App.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
362
frontend/src/App.jsx
Normal file
362
frontend/src/App.jsx
Normal file
@ -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 }) => (
|
||||
<div className={`info-card ${highlight ? 'highlight' : ''}`}>
|
||||
<div className="info-label">{label}</div>
|
||||
<div className="info-value">{value}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const BinaryCard = ({ label, decimal, binary }) => (
|
||||
<div className="binary-card">
|
||||
<div className="binary-label">{label}</div>
|
||||
<div className="binary-decimal">{decimal}</div>
|
||||
<div className="binary-value">{binary}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// 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 (
|
||||
<div className="app">
|
||||
<div className="container">
|
||||
<h1 className="title">🌐 IP Subnet Calculator</h1>
|
||||
|
||||
<form onSubmit={calculateSubnet} className="form">
|
||||
<div className="input-group">
|
||||
<label htmlFor="ipAddress">IP Address</label>
|
||||
<input
|
||||
id="ipAddress"
|
||||
type="text"
|
||||
value={ipAddress}
|
||||
onChange={(e) => setIpAddress(e.target.value)}
|
||||
placeholder="e.g., 192.168.1.1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-type-selector">
|
||||
<button
|
||||
type="button"
|
||||
className={`type-btn ${inputType === 'cidr' ? 'active' : ''}`}
|
||||
onClick={() => setInputType('cidr')}
|
||||
>
|
||||
CIDR Notation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`type-btn ${inputType === 'mask' ? 'active' : ''}`}
|
||||
onClick={() => setInputType('mask')}
|
||||
>
|
||||
Subnet Mask
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{inputType === 'cidr' ? (
|
||||
<div className="input-group">
|
||||
<label htmlFor="cidr">CIDR (1-32)</label>
|
||||
<input
|
||||
id="cidr"
|
||||
type="number"
|
||||
min="1"
|
||||
max="32"
|
||||
value={cidr}
|
||||
onChange={(e) => setCidr(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="input-group">
|
||||
<label htmlFor="subnetMask">Subnet Mask</label>
|
||||
<input
|
||||
id="subnetMask"
|
||||
type="text"
|
||||
value={subnetMask}
|
||||
onChange={(e) => setSubnetMask(e.target.value)}
|
||||
placeholder="e.g., 255.255.255.0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button type="submit" className="calculate-btn" disabled={loading}>
|
||||
{loading ? 'Calculating...' : 'Calculate Subnet'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="results">
|
||||
<h2 className="results-title">Subnet Information</h2>
|
||||
|
||||
{/* Binary Representation Section */}
|
||||
<div className="binary-section">
|
||||
<h3 className="section-title">Binary Representation</h3>
|
||||
<div className="binary-grid">
|
||||
<BinaryCard
|
||||
label="Address"
|
||||
decimal={result.ip_address}
|
||||
binary={result.ip_address_binary}
|
||||
/>
|
||||
<BinaryCard
|
||||
label="Netmask"
|
||||
decimal={`${result.subnet_mask} = /${result.cidr}`}
|
||||
binary={result.subnet_mask_binary}
|
||||
/>
|
||||
<BinaryCard
|
||||
label="Wildcard"
|
||||
decimal={result.wildcard_mask}
|
||||
binary={result.wildcard_binary}
|
||||
/>
|
||||
<BinaryCard
|
||||
label="Network"
|
||||
decimal={`${result.network_id}/${result.cidr}`}
|
||||
binary={result.network_id_binary}
|
||||
/>
|
||||
<BinaryCard
|
||||
label="Broadcast"
|
||||
decimal={result.broadcast_address}
|
||||
binary={result.broadcast_binary}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Information Section */}
|
||||
<h3 className="section-title">Network Details</h3>
|
||||
<div className="results-grid">
|
||||
<InfoCard label="Network ID" value={`${result.network_id}/${result.cidr}`} highlight />
|
||||
<InfoCard label="Broadcast Address" value={result.broadcast_address} highlight />
|
||||
|
||||
<InfoCard label="First Usable IP" value={result.first_usable_ip} />
|
||||
<InfoCard label="Last Usable IP" value={result.last_usable_ip} />
|
||||
|
||||
<InfoCard label="Total Addresses" value={result.total_hosts.toLocaleString()} highlight />
|
||||
<InfoCard label="Usable Hosts" value={result.usable_hosts.toLocaleString()} highlight />
|
||||
|
||||
<InfoCard label="Subnet Mask" value={result.subnet_mask} />
|
||||
<InfoCard label="Wildcard Mask" value={result.wildcard_mask} />
|
||||
|
||||
<InfoCard label="Network Class" value={result.network_class} />
|
||||
<InfoCard label="IP Type" value={result.ip_type} />
|
||||
|
||||
<InfoCard
|
||||
label="Private IP"
|
||||
value={result.is_private ? '✅ Yes' : '❌ No'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Next Networks Section */}
|
||||
<h3 className="section-title">
|
||||
📊 All Next Sequential Networks ({calculateNetworkRanges(result).length.toLocaleString()} networks)
|
||||
</h3>
|
||||
<div className="ranges-table">
|
||||
<div className="table-header">
|
||||
<div className="table-cell">#</div>
|
||||
<div className="table-cell">Network</div>
|
||||
<div className="table-cell">Usable Range</div>
|
||||
<div className="table-cell">Broadcast</div>
|
||||
<div className="table-cell">Hosts</div>
|
||||
</div>
|
||||
<div className="table-body">
|
||||
{calculateNetworkRanges(result).map((network, index) => (
|
||||
<div key={index} className="table-row">
|
||||
<div className="table-cell range-id">{index + 1}</div>
|
||||
<div className="table-cell network-address">{network.networkAddress}</div>
|
||||
<div className="table-cell">{network.firstUsable} - {network.lastUsable}</div>
|
||||
<div className="table-cell">{network.broadcastAddress}</div>
|
||||
<div className="table-cell">{network.usableHosts.toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
22
frontend/src/index.css
Normal file
22
frontend/src/index.css
Normal file
@ -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;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
292
frontend/src/subnetCalculator.js
Normal file
292
frontend/src/subnetCalculator.js
Normal file
@ -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()
|
||||
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
})
|
||||
72
values.yaml
Normal file
72
values.yaml
Normal file
@ -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"
|
||||
Loading…
x
Reference in New Issue
Block a user