# 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((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 { // 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) ↓ Rendered to Leaflet map ``` **Implementation:** ```typescript // HeatmapTileRenderer.ts export class HeatmapTileRenderer { private tileCache = new Map(); 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 ``` --- ## πŸ”„ State Management ### Zustand Stores **Philosophy:** Separate stores for different concerns, minimal boilerplate ```typescript // src/store/sites.ts export const useSitesStore = create((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((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