Files
rfcp/docs/RFCP-ARCHITECTURE.md

1255 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RFCP Architecture Documentation
**Project:** RFCP (RF Coverage Planning) for Tactical Communications
**Developer:** Олег
**Version:** 1.0
**Last Updated:** January 30, 2025
**Status:** Frontend Complete, Backend In Planning
---
## 📋 Table of Contents
1. [Project Overview](#project-overview)
2. [System Architecture](#system-architecture)
3. [Frontend Architecture](#frontend-architecture)
4. [Backend Architecture](#backend-architecture-planned)
5. [Data Models](#data-models)
6. [RF Calculation Engine](#rf-calculation-engine)
7. [Geographic Rendering](#geographic-rendering)
8. [State Management](#state-management)
9. [Deployment Architecture](#deployment-architecture)
10. [Development History](#development-history)
11. [Future Roadmap](#future-roadmap)
---
## 🎯 Project Overview
### Vision
RFCP is a professional RF coverage planning tool designed for tactical military communications. It enables:
- Multi-site LTE network planning
- Realistic RF coverage visualization
- Geographic-scale heatmap rendering
- Multi-sector site configurations
### Key Principles
```
┌────────────────────────────────────────────────────────────┐
│ 🎯 Professional Grade - Production-ready code quality │
│ 🗺️ Geographic Accuracy - True-scale visualization │
│ ⚡ Performance First - Web Workers, tile caching │
│ 🔐 Security Focused - VPN-only access, no public │
│ 📐 Military Precision - Realistic RF calculations │
└────────────────────────────────────────────────────────────┘
```
### Technology Stack
**Frontend:**
- React 18.3+ (UI framework)
- TypeScript 5.x (type safety)
- Vite 5.x (build tool)
- Leaflet 1.9+ (mapping library)
- Zustand 4.x (state management)
**Backend (Planned):**
- FastAPI 0.109+ (Python web framework)
- MongoDB 7.x (document database)
- Motor (async MongoDB driver)
- NumPy/SciPy (scientific computing)
**Infrastructure:**
- VPS: Hetzner Germany (10.10.10.1)
- Reverse Proxy: Caddy 2.x
- VPN: WireGuard mesh (10.10.0.0/16)
- Systemd services
---
## 🏗️ System Architecture
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ RFCP System │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Frontend Layer │ │ Backend Layer │ │
│ │ (React + Leaflet) │◄────REST API────►│ (FastAPI + Mongo) │ │
│ │ Port: 443 (HTTPS) │ │ Port: 8888 (HTTP) │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
│ │ │ │
│ │ │ │
│ ┌──────────┴───────────┐ ┌──────────┴───────────┐ │
│ │ Web Workers │ │ External APIs │ │
│ │ - RF calculations │ │ - SRTM terrain │ │
│ │ - Parallel compute │ │ - OpenStreetMap │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Caddy Reverse Proxy │ │ WireGuard VPN Mesh │ │
│ │ - SSL termination │ │ - 10.10.0.0/16 │ │
│ │ - Path routing │ │ - Site isolation │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Network Topology
```
Internet (Public)
[Caddy Reverse Proxy]
├──> rfcp.eliah.one ──> Frontend (static files)
└──> (future: api.rfcp.eliah.one ──> Backend API)
WireGuard VPN (10.10.0.0/16)
├──> 10.10.10.1 (VPS A - Germany)
│ ├──> Frontend: dist/
│ ├──> Backend: :8888
│ └──> MongoDB: :27017
└──> 10.10.11.1 (VPS B - Finland) [future expansion]
```
### Access Control
```
Security Layer:
┌─────────────────────────────────────────────────────────────┐
│ Level 1: Public Internet │
│ ├─> Caddy (HTTPS only) │
│ └─> Frontend static files │
│ │
│ Level 2: WireGuard VPN (10.10.0.0/16) │
│ ├─> Backend API (internal only) │
│ ├─> MongoDB (internal only) │
│ └─> All admin interfaces │
│ │
│ Level 3: Future Auth (MAS OAuth) │
│ ├─> User authentication │
│ ├─> Role-based access │
│ └─> Project ownership │
└─────────────────────────────────────────────────────────────┘
```
---
## 🎨 Frontend Architecture
### Component Hierarchy
```
App.tsx (Root)
├── Map.tsx (Leaflet container)
│ ├── TileLayer (OpenStreetMap)
│ ├── SiteMarker[] (site locations)
│ ├── GeographicHeatmap (custom canvas overlay)
│ └── CoverageBoundary (polygon outline)
├── SitesPanel (left sidebar)
│ ├── SiteList
│ │ └── SiteCard[] (collapsible)
│ │ └── SectorList[] (Alpha, Beta, Gamma...)
│ ├── SiteModal (create/edit dialog)
│ └── BatchOperations (power, tilt, delete)
├── SettingsPanel (right sidebar)
│ ├── CoverageSettings
│ │ ├── NumberInput (radius, resolution)
│ │ └── ThresholdSliders (RSRP limits)
│ ├── MapSettings (base layer, opacity)
│ └── CalculateButton
├── CoverageStats (bottom panel)
│ ├── SignalQuality (excellent/good/fair/poor counts)
│ ├── CoverageArea (km²)
│ └── AverageRSRP
└── UIComponents (shared)
├── NumberInput (with slider)
├── ConfirmDialog (delete confirmation)
├── Toast (notifications)
└── LoadingSpinner
```
### Directory Structure
```
/opt/rfcp/frontend/
├── src/
│ ├── components/
│ │ ├── map/
│ │ │ ├── Map.tsx # Main map container
│ │ │ ├── SiteMarker.tsx # Site location markers
│ │ │ ├── GeographicHeatmap.tsx # Custom canvas renderer
│ │ │ ├── HeatmapTileRenderer.ts # Tile generation logic
│ │ │ └── CoverageBoundary.tsx # Coverage outline polygon
│ │ │
│ │ ├── panels/
│ │ │ ├── SitesPanel.tsx # Site management panel
│ │ │ ├── SettingsPanel.tsx # Coverage settings
│ │ │ ├── CoverageStats.tsx # Statistics display
│ │ │ └── ExportPanel.tsx # Data export
│ │ │
│ │ ├── ui/
│ │ │ ├── NumberInput.tsx # Validated numeric input
│ │ │ ├── ConfirmDialog.tsx # Confirmation modal
│ │ │ ├── Toast.tsx # Toast notifications
│ │ │ ├── Button.tsx # Styled button
│ │ │ └── Modal.tsx # Generic modal
│ │ │
│ │ └── modals/
│ │ └── SiteModal.tsx # Site create/edit dialog
│ │
│ ├── store/
│ │ ├── sites.ts # Site state (Zustand)
│ │ ├── coverage.ts # Coverage state
│ │ ├── ui.ts # UI state (modals, etc)
│ │ └── settings.ts # App settings
│ │
│ ├── lib/
│ │ ├── calculator.ts # Coverage grid generation
│ │ ├── pathLoss.ts # RF propagation models
│ │ ├── antennaPattern.ts # Directional patterns
│ │ └── geographic.ts # Lat/lon utilities
│ │
│ ├── utils/
│ │ ├── colorGradient.ts # RSRP color mapping
│ │ ├── validation.ts # Input validation
│ │ ├── logger.ts # Logging utility
│ │ └── format.ts # Number formatting
│ │
│ ├── constants/
│ │ ├── rsrp-thresholds.ts # Signal quality thresholds
│ │ ├── frequencies.ts # LTE frequency bands
│ │ └── defaults.ts # Default values
│ │
│ ├── hooks/
│ │ ├── useKeyboardShortcuts.ts # Keyboard handlers
│ │ ├── useCoverageCalculation.ts # Coverage computation
│ │ └── useMapBounds.ts # Map viewport tracking
│ │
│ ├── types/
│ │ ├── site.ts # Site & Sector types
│ │ ├── coverage.ts # Coverage types
│ │ └── map.ts # Map types
│ │
│ ├── workers/
│ │ └── rf-worker.ts # Web Worker for RF calc
│ │
│ ├── App.tsx # Root component
│ ├── main.tsx # Entry point
│ └── index.css # Global styles
├── public/
│ └── workers/
│ └── rf-worker.js # Built worker script
├── docs/
│ └── devlog/
│ └── front/
│ ├── RFCP-Iteration1-*.md # Development history
│ ├── RFCP-Iteration2-*.md
│ ├── ...
│ └── RFCP-Iteration10.6-*.md # Latest iteration
├── dist/ # Production build
├── node_modules/
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
```
### Key Design Patterns
**1. State Management (Zustand)**
```typescript
// Centralized state with actions
export const useSitesStore = create<SitesStore>((set, get) => ({
sites: [],
selectedSiteId: null,
// Actions
addSite: (site) => set((state) => ({
sites: [...state.sites, site]
})),
updateSite: (id, updates) => set((state) => ({
sites: state.sites.map(s => s.id === id ? { ...s, ...updates } : s)
})),
deleteSite: (id) => set((state) => ({
sites: state.sites.filter(s => s.id !== id),
selectedSiteId: state.selectedSiteId === id ? null : state.selectedSiteId
})),
selectSite: (id) => set({ selectedSiteId: id })
}));
```
**2. Custom Hooks**
```typescript
// Encapsulate complex logic
export function useCoverageCalculation() {
const sites = useSitesStore(s => s.sites);
const settings = useCoverageStore(s => s.settings);
const [isCalculating, setIsCalculating] = useState(false);
const calculate = useCallback(async () => {
setIsCalculating(true);
try {
const worker = new Worker('/workers/rf-worker.js');
const points = await calculateCoverageGrid(sites, settings, worker);
useCoverageStore.getState().setCoveragePoints(points);
} finally {
setIsCalculating(false);
}
}, [sites, settings]);
return { calculate, isCalculating };
}
```
**3. Keyboard Shortcuts**
```typescript
// Global keyboard handler with callbacks
export function useKeyboardShortcuts(callbacks: KeyboardCallbacks) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// N = New Site
if (e.key === 'n' || e.key === 'N') {
callbacks.onNewSite?.();
}
// Delete = Delete selected
if (e.key === 'Delete' && selectedSiteId) {
callbacks.onDeleteRequest?.(selectedSiteId, siteName);
}
// Ctrl+Z = Undo
if (e.ctrlKey && e.key === 'z') {
callbacks.onUndo?.();
}
// ... more shortcuts
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [callbacks]);
}
```
---
## 🖥️ Backend Architecture (Planned)
### Technology Stack
```
┌────────────────────────────────────────────────────┐
│ Backend Technology Stack │
├────────────────────────────────────────────────────┤
│ │
│ Web Framework: FastAPI 0.109+ │
│ Database: MongoDB 7.x │
│ DB Driver: Motor (async) │
│ Validation: Pydantic 2.x │
│ ASGI Server: Uvicorn │
│ │
│ RF Computing: NumPy, SciPy │
│ Terrain Data: SRTM (NASA elevation) │
│ Building Data: OpenStreetMap Overpass API │
│ │
└────────────────────────────────────────────────────┘
```
### Service Architecture
```
Backend Services:
┌─────────────────────────────────────────────────────────────┐
│ API Layer (FastAPI) │
│ ├── routes/ │
│ │ ├── projects.py - CRUD for projects │
│ │ ├── sites.py - Site management │
│ │ ├── coverage.py - Coverage calculation │
│ │ └── terrain.py - Terrain data endpoints │
│ │ │
│ ├── services/ │
│ │ ├── rf_calculator.py - Path loss, LOS, Fresnel │
│ │ ├── terrain_service.py - SRTM data handling │
│ │ ├── osm_service.py - Building obstacles │
│ │ └── cache_service.py - Coverage caching │
│ │ │
│ └── models/ │
│ ├── project.py - Pydantic models │
│ ├── site.py - Site/Sector schemas │
│ └── coverage.py - Coverage data schemas │
└─────────────────────────────────────────────────────────────┘
```
### API Endpoints (Planned)
```python
# Project Management
GET /api/projects/current # Get global project
PUT /api/projects/current # Update project
GET /api/projects/current/sites # Get all sites
PUT /api/projects/current/sites # Update sites
GET /api/projects/current/settings # Get settings
PUT /api/projects/current/settings # Update settings
# Coverage Calculation
POST /api/coverage/calculate # Trigger calculation
GET /api/coverage/cache # Get cached results
# Terrain Data (future)
GET /api/terrain/elevation # Get elevation at point
GET /api/terrain/profile # Get elevation profile
GET /api/terrain/buildings # Get buildings in area
# Export/Import (future)
GET /api/export/json # Export project as JSON
POST /api/import/json # Import project from JSON
```
### Data Persistence
```
MongoDB Collections:
┌─────────────────────────────────────────────────────────────┐
│ rfcp (database) │
│ ├── projects │
│ │ └── { name, sites[], settings, created_at, ... } │
│ │ │
│ ├── coverage_cache │
│ │ └── { project_name, calculated_at, points[], ... } │
│ │ │
│ └── terrain_cache (future) │
│ └── { tile_name, elevation_data, downloaded_at } │
└─────────────────────────────────────────────────────────────┘
```
---
## 📊 Data Models
### Core Types
```typescript
// Site with multiple sectors
interface Site {
id: string;
name: string;
lat: number;
lon: number;
sectors: Sector[];
}
// Individual sector (antenna)
interface Sector {
id: string;
name: string; // "Alpha", "Beta", "Gamma", ...
power: number; // dBm (10-50)
gain: number; // dBi (0-25)
height: number; // meters (1-500)
frequency: number; // MHz (700-3800)
azimuth: number; // degrees (0-359)
beamwidth: number; // degrees (10-360)
tilt: number; // degrees (-20 to +20)
antennaType: 'omnidirectional' | 'directional';
}
// Coverage calculation settings
interface CoverageSettings {
radius: number; // meters (1000-50000)
resolution: number; // meters (50-500)
minSignal: number; // dBm (-120 to -60)
useTerrain: boolean; // Enable terrain calculations
useBuildings: boolean; // Enable building obstacles
}
// Coverage point (single grid cell)
interface CoveragePoint {
lat: number;
lon: number;
rsrp: number; // dBm
siteId: string; // Which site provides coverage
sectorId: string; // Which sector
}
// Signal quality categories
type SignalQuality = 'excellent' | 'good' | 'fair' | 'poor' | 'none';
```
### RSRP Thresholds
```typescript
// LTE RSRP (Reference Signal Received Power) ranges
export const RSRP_THRESHOLDS = {
excellent: -80, // >-80 dBm - Full bars, HD video
good: -95, // -80 to -95 - Good voice, streaming
fair: -105, // -95 to -105 - Acceptable voice
poor: -115, // -105 to -115 - Marginal coverage
// Below -115 dBm = no service
} as const;
// Color mapping (purple → orange gradient)
export const SIGNAL_COLORS = {
excellent: '#ffb74d', // Light orange
good: '#ff9800', // Orange
fair: '#ff6f00', // Dark orange
poor: '#ab47bc', // Light purple
veryPoor: '#7b1fa2', // Purple
terrible: '#4a148c', // Dark purple
none: 'transparent'
} as const;
```
---
## 📡 RF Calculation Engine
### Path Loss Models
**1. Free Space Path Loss (FSPL)**
```typescript
function calculateFSPL(distanceKm: number, frequencyMHz: number): number {
// FSPL(dB) = 20log10(d) + 20log10(f) + 32.44
// where d is in km, f is in MHz
return 20 * Math.log10(distanceKm) +
20 * Math.log10(frequencyMHz) +
32.44;
}
```
**2. Okumura-Hata Model** (currently used)
```typescript
function calculateOkumuraHata(
distanceKm: number,
frequencyMHz: number,
txHeightM: number,
rxHeightM: number
): number {
// Urban/suburban propagation model for 150-1500 MHz
// L(dB) = 69.55 + 26.16log10(f) - 13.82log10(hb) - a(hm) +
// (44.9 - 6.55log10(hb))log10(d)
const a_hm = (1.1 * Math.log10(frequencyMHz) - 0.7) * rxHeightM -
(1.56 * Math.log10(frequencyMHz) - 0.8);
return 69.55 +
26.16 * Math.log10(frequencyMHz) -
13.82 * Math.log10(txHeightM) -
a_hm +
(44.9 - 6.55 * Math.log10(txHeightM)) * Math.log10(distanceKm);
}
```
**3. Terrain-Enhanced Model** (backend, planned)
```python
async def calculate_path_loss_with_terrain(
site_lat: float, site_lon: float, site_height: float,
point_lat: float, point_lon: float, point_height: float,
frequency_mhz: float,
terrain_service: TerrainService
) -> float:
"""
Enhanced path loss with:
- Terrain elevation profile
- Line-of-sight check
- Fresnel zone clearance
- Diffraction loss over obstacles
"""
# Base propagation model
base_loss = calculate_okumura_hata(...)
# Terrain factors
has_los, clearance = await check_line_of_sight(...)
if not has_los:
# Add obstruction loss
diffraction_loss = calculate_knife_edge_diffraction(clearance)
base_loss += diffraction_loss
# Fresnel zone clearance
fresnel_pct = await check_fresnel_clearance(...)
if fresnel_pct < 60:
# Partial obstruction
base_loss += (60 - fresnel_pct) / 60 * 10 # Up to 10 dB loss
return base_loss
```
### Antenna Pattern
**Directional Antenna Gain:**
```typescript
function calculateDirectionalGain(
azimuth: number, // Antenna pointing direction
beamwidth: number, // 3dB beamwidth
maxGain: number, // Peak gain in dBi
targetAzimuth: number // Direction to target point
): number {
// Calculate angle difference
let angleDiff = Math.abs(targetAzimuth - azimuth);
if (angleDiff > 180) angleDiff = 360 - angleDiff;
// Gaussian-like pattern
if (angleDiff <= beamwidth / 2) {
// Main lobe (inside 3dB beamwidth)
const attenuation = -3 * Math.pow(angleDiff / (beamwidth / 2), 2);
return maxGain + attenuation;
} else {
// Side lobes (outside beamwidth)
const sideLobeAttenuation = -12 - (angleDiff - beamwidth / 2) * 0.5;
return maxGain + Math.max(sideLobeAttenuation, -40); // Min -40 dB
}
}
```
### RSRP Calculation
```typescript
function calculateRSRP(
site: Site,
sector: Sector,
targetLat: number,
targetLon: number
): number {
// 1. Calculate distance
const distance = haversineDistance(
sector.lat, sector.lon,
targetLat, targetLon
);
// 2. Path loss
const pathLoss = calculateOkumuraHata(
distance / 1000, // Convert to km
sector.frequency,
sector.height,
1.5 // Assume 1.5m receiver height
);
// 3. Antenna gain (directional or omni)
let antennaGain = sector.gain;
if (sector.antennaType === 'directional') {
const targetAzimuth = calculateBearing(
sector.lat, sector.lon,
targetLat, targetLon
);
antennaGain = calculateDirectionalGain(
sector.azimuth,
sector.beamwidth,
sector.gain,
targetAzimuth
);
}
// 4. RSRP = Tx Power + Antenna Gain - Path Loss
return sector.power + antennaGain - pathLoss;
}
```
### Coverage Grid Generation
```typescript
async function calculateCoverageGrid(
sites: Site[],
settings: CoverageSettings
): Promise<CoveragePoint[]> {
// 1. Calculate bounding box
const bounds = calculateBounds(sites, settings.radius);
// 2. Generate grid points
const gridPoints = generateGrid(bounds, settings.resolution);
// 3. Use Web Workers for parallel calculation
const workers = Array(4).fill(null).map(() =>
new Worker('/workers/rf-worker.js')
);
// 4. Split work among workers
const chunks = chunkArray(gridPoints, Math.ceil(gridPoints.length / workers.length));
// 5. Calculate coverage for each chunk
const results = await Promise.all(
chunks.map((chunk, i) =>
calculateChunk(workers[i], sites, chunk, settings)
)
);
// 6. Combine results
const coveragePoints = results.flat();
// 7. Terminate workers
workers.forEach(w => w.terminate());
return coveragePoints;
}
```
---
## 🗺️ Geographic Rendering
### Custom Canvas Heatmap
**Problem:** Standard Leaflet heatmap plugins scale with zoom level (bad!)
**Solution:** Custom geographic-scale canvas overlay using Leaflet's `GridLayer`
**Architecture:**
```
GeographicHeatmap Component
HeatmapTileRenderer (singleton)
Creates 256×256 canvas tiles
Each tile = exact geographic coordinates
Tile cache (Map<tileKey, canvas>)
Rendered to Leaflet map
```
**Implementation:**
```typescript
// HeatmapTileRenderer.ts
export class HeatmapTileRenderer {
private tileCache = new Map<string, HTMLCanvasElement>();
private radiusMeters: number;
constructor(radiusMeters = 400) {
this.radiusMeters = radiusMeters;
}
createTile(coords: L.Coords, coveragePoints: CoveragePoint[]): HTMLCanvasElement {
const tileKey = `${coords.x}_${coords.y}_${coords.z}`;
// Check cache
if (this.tileCache.has(tileKey)) {
return this.tileCache.get(tileKey)!;
}
// Create 256×256 canvas
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d')!;
// Calculate tile bounds (lat/lon)
const tileBounds = this.getTileBounds(coords);
// Filter points in this tile
const relevantPoints = coveragePoints.filter(p =>
p.lat >= tileBounds.south && p.lat <= tileBounds.north &&
p.lon >= tileBounds.west && p.lon <= tileBounds.east
);
// Draw each point
relevantPoints.forEach(point => {
// Convert lat/lon to pixel coordinates within tile
const pixelX = this.latLonToTilePixel(point.lon, tileBounds.west, tileBounds.east);
const pixelY = this.latLonToTilePixel(point.lat, tileBounds.south, tileBounds.north);
// Calculate radius in pixels (constant 400m geographic radius)
const radiusPixels = this.metersToPixels(this.radiusMeters, point.lat, coords.z);
// Draw circle
ctx.fillStyle = this.rsrpToColor(point.rsrp);
ctx.beginPath();
ctx.arc(pixelX, pixelY, radiusPixels, 0, Math.PI * 2);
ctx.fill();
});
// Cache tile
this.tileCache.set(tileKey, canvas);
return canvas;
}
metersToPixels(meters: number, latitude: number, zoom: number): number {
// Earth circumference at equator: 40,075,017 meters
// At latitude φ: circumference * cos(φ)
const latCircumference = 40075017 * Math.cos(latitude * Math.PI / 180);
// Pixels per meter at this zoom level
const pixelsPerMeter = (256 * Math.pow(2, zoom)) / latCircumference;
return meters * pixelsPerMeter;
}
}
```
**Key Insight:** By calculating `metersToPixels` for each zoom level, we maintain **true 400m radius** regardless of zoom.
### Coverage Boundary
**Purpose:** Show outline polygon of coverage area
```typescript
// Calculate convex hull of coverage points
function calculateCoverageBoundary(points: CoveragePoint[]): [number, number][] {
// 1. Filter to edge points
const edgePoints = points.filter(isEdgePoint);
// 2. Graham scan algorithm for convex hull
const hull = grahamScan(edgePoints.map(p => [p.lat, p.lon]));
// 3. Smooth polygon (optional)
const smoothed = smoothPolygon(hull, smoothingFactor);
return smoothed;
}
// Render as Leaflet Polygon
<Polygon
positions={boundaryPoints}
pathOptions={{
color: '#ff6b35',
weight: 3,
opacity: 0.8,
fill: false,
dashArray: '10, 5'
}}
/>
```
---
## 🔄 State Management
### Zustand Stores
**Philosophy:** Separate stores for different concerns, minimal boilerplate
```typescript
// src/store/sites.ts
export const useSitesStore = create<SitesStore>((set, get) => ({
// State
sites: [],
selectedSiteId: null,
// Actions
addSite: (site) => set((state) => ({
sites: [...state.sites, site]
})),
updateSite: (id, updates) => set((state) => ({
sites: state.sites.map(s => s.id === id ? { ...s, ...updates } : s)
})),
deleteSite: (id) => {
const site = get().sites.find(s => s.id === id);
set((state) => ({
sites: state.sites.filter(s => s.id !== id),
selectedSiteId: state.selectedSiteId === id ? null : state.selectedSiteId
}));
// Show undo toast
toast.success('Site deleted', {
duration: 10000,
action: {
label: 'Undo',
onClick: () => get().addSite(site!)
}
});
},
// Sector operations
addSector: (siteId, sector) => set((state) => ({
sites: state.sites.map(s =>
s.id === siteId
? { ...s, sectors: [...s.sectors, sector] }
: s
)
})),
updateSector: (siteId, sectorId, updates) => set((state) => ({
sites: state.sites.map(s =>
s.id === siteId
? {
...s,
sectors: s.sectors.map(sec =>
sec.id === sectorId ? { ...sec, ...updates } : sec
)
}
: s
)
}))
}));
```
```typescript
// src/store/coverage.ts
export const useCoverageStore = create<CoverageStore>((set) => ({
// State
coveragePoints: [],
isCalculating: false,
lastCalculated: null,
// Settings
settings: {
radius: 10000,
resolution: 200,
minSignal: -105,
useTerrain: false,
useBuildings: false
},
// Actions
setCoveragePoints: (points) => set({
coveragePoints: points,
lastCalculated: new Date()
}),
updateSettings: (updates) => set((state) => ({
settings: { ...state.settings, ...updates }
})),
clearCoverage: () => set({
coveragePoints: [],
lastCalculated: null
})
}));
```
### localStorage Persistence
```typescript
// Auto-save to localStorage on changes
useSitesStore.subscribe((state) => {
localStorage.setItem('rfcp-sites', JSON.stringify(state.sites));
});
// Load from localStorage on app start
useEffect(() => {
const saved = localStorage.getItem('rfcp-sites');
if (saved) {
const sites = JSON.parse(saved);
useSitesStore.getState().setSites(sites);
}
}, []);
```
---
## 🚀 Deployment Architecture
### Current Deployment
```
VPS A (Hetzner Germany)
├── IP: 2.56.207.143 (public), 10.10.10.1 (VPN)
├── OS: Ubuntu 24.04 LTS
├── Services:
│ ├── Caddy (reverse proxy, port 443)
│ ├── WireGuard (VPN server, port 51820)
│ ├── RFCP Frontend (static files)
│ ├── RFCP Backend (systemd service, port 8888)
│ ├── MongoDB (port 27017, VPN-only)
│ ├── Open5GS (LTE core)
│ ├── Matrix Synapse (communications)
│ └── Various other UMTC services
```
### Caddy Configuration
```caddyfile
# /etc/caddy/Caddyfile
{
email oleg@eliah.one
default_bind 10.10.10.1 # VPN-only
}
rfcp.eliah.one {
bind 10.10.10.1
# Frontend (static files)
root * /opt/rfcp/frontend/dist
file_server
# SPA fallback (для React Router)
try_files {path} /index.html
# Headers
header {
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "no-referrer"
}
# Logs
log {
output file /var/log/caddy/rfcp.log
format json
}
}
# Future: Backend API
# api.rfcp.eliah.one {
# bind 10.10.10.1
# reverse_proxy localhost:8888
# }
```
### Backend Service
```ini
# /etc/systemd/system/rfcp-backend.service
[Unit]
Description=RFCP Backend API
After=network.target mongodb.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/rfcp/backend
ExecStart=/opt/rfcp/backend/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8888
Restart=always
RestartSec=10
# Environment
Environment="MONGODB_URL=mongodb://localhost:27017"
Environment="LOG_LEVEL=info"
[Install]
WantedBy=multi-user.target
```
### Build & Deploy Process
```bash
# On development machine (через VPN):
# 1. Build frontend
cd /opt/rfcp/frontend
npm run build
# 2. Deploy to VPS (if remote)
# scp -r dist/* root@10.10.10.1:/opt/rfcp/frontend/dist/
# 3. Reload Caddy (if config changed)
ssh root@10.10.10.1 "systemctl reload caddy"
# 4. Check services
ssh root@10.10.10.1 "systemctl status rfcp-backend"
ssh root@10.10.10.1 "systemctl status caddy"
# 5. View logs
ssh root@10.10.10.1 "journalctl -u rfcp-backend -f"
ssh root@10.10.10.1 "journalctl -u caddy -f"
```
---
## 📚 Development History
### Iteration Timeline
**Phase 1: Foundation (Iterations 1-3)**
- Basic React setup + Leaflet integration
- Simple site CRUD
- Free Space path loss model
- Initial heatmap (leaflet.heat plugin)
**Phase 2: Multi-Site Support (Iterations 4-5)**
- Multiple sites on map
- Site selection/editing UI
- Okumura-Hata propagation model
- Coverage aggregation (strongest signal wins)
**Phase 3: Antenna Patterns (Iteration 6)**
- Omnidirectional vs Directional antennas
- Azimuth and beamwidth controls
- Gaussian antenna pattern
- Directional gain calculation
**Phase 4: Geographic Heatmap (Iterations 7-8)** ⭐ Major Milestone
- **Problem:** Heatmap scaled with zoom (wrong!)
- **Solution:** Custom canvas renderer with true geographic scale
- 400m radius constant across all zoom levels
- Tile-based rendering with cache
- Replaced leaflet.heat dependency
**Phase 5: UX Polish (Iterations 9-9.1)**
- NumberInput components with sliders
- Delete confirmation dialogs
- Undo toast (10s duration)
- Keyboard shortcuts (N, Delete, Ctrl+Z, etc)
- Dark mode improvements
**Phase 6: Final Audit (Iteration 10)**
- TypeScript strict mode (0 errors)
- ESLint cleanup (0 errors)
- Code organization & dead code removal
- Logger utility (dev/prod separation)
- React.memo optimization
- Bundle size optimization (163KB gzipped)
**Phase 7: Bugfixes & Improvements (Iterations 10.1-10.6)**
- **10.1:** Stack overflow fix (spread operator), Delete confirmation, Gradient colors
- **10.2:** Purple → Orange gradient (better aesthetics)
- **10.3:** Coverage boundary outline + threshold filters
- **10.4:** Stadia Maps 401 fix (mobile compatibility)
- **10.5:** Input validation + Site/Sector hierarchy UI
- **10.6:** Site modal dialogs + Batch operations
### Key Learnings
**1. Geographic Calculations are Complex**
- Web Mercator projection has distortions
- Need precise lat/lon ↔ pixel conversions
- Tile-based rendering is powerful but tricky
- Constant geographic radius requires zoom-aware calculations
**2. Spread Operator Has Limits**
- JavaScript argument limit: ~65k-125k
- Never use `Math.min(...largeArray)`
- Use `reduce()` or `for...of` loops instead
**3. UX Matters for Professional Tools**
- Confirmation dialogs prevent accidents
- Undo functionality builds confidence
- Keyboard shortcuts increase productivity
- Visual feedback (loading, errors, empty states) essential
**4. TypeScript Strict Mode is Worth It**
- Catches bugs before runtime
- Forces better code design
- Minimal performance overhead
- Essential for team projects
---
## 🎯 Future Roadmap
### Phase 8: Backend MVP (2 months)
**Week 1-2: Foundation**
- FastAPI setup + MongoDB
- Basic CRUD endpoints
- Project persistence
- API documentation (Swagger)
**Week 3-4: Terrain Integration** ⭐ Priority
- SRTM elevation data service
- Line-of-sight calculation
- Fresnel zone clearance
- Realistic path loss with terrain
**Week 5-6: Building Obstacles**
- OpenStreetMap integration
- Building polygon extraction
- Urban area coverage reduction
**Week 7-8: Frontend Integration**
- API client (TypeScript)
- Replace localStorage with API
- Terrain toggle UI
- Coverage caching
### Phase 9: Advanced Features (3-6 months)
**Multi-User Support:**
- MAS OAuth integration
- User authentication
- Project ownership
- Sharing & collaboration
**Advanced RF:**
- Link budget calculator
- Interference modeling
- Multiple frequency bands
- Custom propagation models
**Visualization:**
- 3D terrain view
- Signal strength profiles
- Coverage comparison mode
- Time-based analysis
**Collaboration:**
- Real-time multi-user editing
- Comments & annotations
- Version history
- Export to KML/GeoJSON
---
## 📝 Appendix
### Build Commands
```bash
# Development
npm run dev # Start dev server (http://localhost:5173)
# Build
npm run build # Production build to dist/
npm run preview # Preview production build
# Quality
npm run type-check # TypeScript validation
npm run lint # ESLint check
npm run lint:fix # Auto-fix ESLint issues
```
### Environment Variables
```bash
# .env (if needed)
VITE_API_BASE_URL=http://10.10.10.1:8888
VITE_ENABLE_TERRAIN=false
```
### Performance Targets
- Coverage calculation: <5s (typical: 10 sites, 200m resolution)
- Tile rendering: <50ms per tile
- Initial load: <2s on 3G
- Bundle size: <500KB gzipped ✅ (achieved: 163KB)
### Browser Support
- Chrome/Edge: 120+ ✅
- Firefox: 115+ ✅
- Safari: 16+ ✅ (limited testing)
- Mobile: Chrome/Safari ✅
---
**Document Status:** ✅ COMPLETE
**Last Updated:** January 30, 2025
**Maintained By:** Олег
**Next Review:** After Backend MVP completion