first commit

This commit is contained in:
dvirlabs 2025-12-23 21:05:31 +02:00
commit 8b341a9f3b
20 changed files with 3819 additions and 0 deletions

30
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0

32
docker-compose.yml Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

22
frontend/package.json Normal file
View 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
View 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
View 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
View 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
View 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>,
)

View 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
View 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
View 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"