diff --git a/RFCP-TechSpec-v3.0.md b/RFCP-TechSpec-v3.0.md new file mode 100644 index 0000000..3f49378 --- /dev/null +++ b/RFCP-TechSpec-v3.0.md @@ -0,0 +1,2373 @@ +# RFCP - RF Coverage Planning Tool +## Technical Specification v3.0 (Final) + +**Project:** RF Coverage Planner +**Version:** 3.0 (PWA/Offline-First/Manual Input) +**Target URL:** https://rfcp.eliah.one +**Repository:** https://git.eliah.one/mytec/rfcp.git +**Created:** 2025-01-30 +**Author:** Claude + ะžะปะตะณ (mytec) +**Status:** ๐Ÿš€ Ready for Implementation + +--- + +## Executive Summary + +RFCP - Progressive Web Application ะดะปั ะฟะปะฐะฝัƒะฒะฐะฝะฝั ั‚ะฐ ะฒั–ะทัƒะฐะปั–ะทะฐั†ั–ั— ะฟะพะบั€ะธั‚ั‚ั wireless ะผะตั€ะตะถ (LTE, UHF/VHF) ะท ะฟะพะฒะฝะพัŽ ะฟั–ะดั‚ั€ะธะผะบะพัŽ offline ั€ะตะถะธะผัƒ. ะ†ะฝัั‚ั€ัƒะผะตะฝั‚ ะฝะฐะดะฐั” ะฒั–ะนััŒะบะพะฒะธะผ ะทะฒ'ัะทะบั–ะฒั†ัะผ ะณะฝัƒั‡ะบะธะน, ั‚ะพั‡ะฝะธะน ะทะฐัั–ะฑ ะฟะปะฐะฝัƒะฒะฐะฝะฝั ั€ะพะทะณะพั€ั‚ะฐะฝะฝั ั€ะฐะดั–ะพัะธัั‚ะตะผ ะฝะฐ ะผั–ัั†ะตะฒะพัั‚ั– ะท ัƒั€ะฐั…ัƒะฒะฐะฝะฝัะผ ั€ะตะปัŒั”ั„ัƒ. + +### Core Philosophy +**"Flexibility over Presets"** - ะšะพั€ะธัั‚ัƒะฒะฐั‡ ะฒะฒะพะดะธั‚ัŒ ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฒั€ัƒั‡ะฝัƒ, ั€ะพะทัƒะผั–ั” ั‰ะพ ั€ะพะฑะธั‚ัŒ, ะตะบัะฟะตั€ะธะผะตะฝั‚ัƒั”. ะั–ัะบะธั… ะถะพั€ัั‚ะบะธั… ะพะฑะผะตะถะตะฝัŒ ะฝะฐ ะพะฑะปะฐะดะฝะฐะฝะฝั. + +### Key Features +- ๐Ÿ“ด **100% Offline Capable** - ะฟั€ะฐั†ัŽั” ะฑะตะท ั–ะฝั‚ะตั€ะฝะตั‚ัƒ ะฟั–ัะปั ะฟะตั€ัˆะพะณะพ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั +- ๐ŸŽ›๏ธ **Manual Input First** - ะฟะพะฒะฝะฐ ะณะฝัƒั‡ะบั–ัั‚ัŒ ะฟะฐั€ะฐะผะตั‚ั€ั–ะฒ, ะฑะตะท ะฟั€ะธะฒ'ัะทะบะธ ะดะพ ะบะพะฝะบั€ะตั‚ะฝะพะณะพ ะพะฑะปะฐะดะฝะฐะฝะฝั +- ๐Ÿ“ฑ **Mobile-First** - ะพะฟั‚ะธะผั–ะทะพะฒะฐะฝะพ ะดะปั ะฟะปะฐะฝัˆะตั‚ั–ะฒ ั‚ะฐ ั‚ะตะปะตั„ะพะฝั–ะฒ +- ๐Ÿ—บ๏ธ **High-Res Terrain** - 30m SRTM elevation data ะดะปั ั‚ะพั‡ะฝะธั… ั€ะพะทั€ะฐั…ัƒะฝะบั–ะฒ +- ๐Ÿ” **Optional Auth** - MAS OAuth ะดะปั ัะธะฝั…ั€ะพะฝั–ะทะฐั†ั–ั— ะผั–ะถ ะฟั€ะธัั‚ั€ะพัะผะธ +- ๐Ÿ“ฆ **Smart Download** - terrain on-demand ะดะปั 2-3 ะพะฑะปะฐัั‚ะตะน (~800MB-1.2GB) +- โšก **Fast** - ะฒัั– RF ั€ะพะทั€ะฐั…ัƒะฝะบะธ ะฒ ะฑั€ะฐัƒะทะตั€ั– (Web Workers) +- ๐ŸŒ **Universal** - LTE (MVP) โ†’ UHF/VHF (v1.1+) + +### Target Users +- ะ’ั–ะนััŒะบะพะฒั– ะทะฒ'ัะทะบั–ะฒั†ั– (ะฟะปะฐะฝัƒะฒะฐะฝะฝั tactical LTE/radio networks) +- ะขะตะปะตะบะพะผ ั–ะฝะถะตะฝะตั€ะธ (proof-of-concept deployments) +- UMTC infrastructure ะฟะปะฐะฝัƒะฒะฐะปัŒะฝะธะบะธ +- Amateur radio operators (ะผะฐะนะฑัƒั‚ะฝั”) + +--- + +## Architecture Overview + +### High-Level Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Browser / Mobile Device โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ RFCP PWA Application โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ UI Layer โ”‚ โ”‚ Service Worker โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (React) โ”‚โ—„โ”€โ”€โ”€โ”€โ–บโ”‚ (Offline Cache) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Manual โ”‚ โ”‚ - Map tiles โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Input UI โ”‚ โ”‚ - App assets โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Dynamic โ”‚ โ”‚ - Terrain data โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Bands โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ RF Calculation Engine โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (TypeScript + Web Workers) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Universal FSPL (any frequency) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Terrain loss (30m SRTM) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Sector antenna patterns โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Multi-site aggregation โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Offline Storage โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - IndexedDB (Terrain + Configs) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - LocalStorage (Settings) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ (Optional Network Connection) + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Backend API (FastAPI) - Optional โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ MAS OAuth โ”‚ โ”‚ Terrain Data CDN โ”‚ โ”‚ +โ”‚ โ”‚ Integration โ”‚ โ”‚ (30m SRTM files) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Config Sync (for multi-device users) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## Technology Stack + +### Frontend (PWA Core) + +```yaml +Core Framework: + - React: 18.x (with TypeScript) + - TypeScript: 5.x + - Vite: 5.x (build tool) + - Tailwind CSS: 3.x + +Mapping Libraries: + - Leaflet: 1.9.x (map rendering) + - React-Leaflet: 4.x + - Leaflet.heat: 0.2.x (heatmap visualization) + +Offline Capabilities: + - Workbox: 7.x (Service Worker management) + - Dexie.js: 4.x (IndexedDB wrapper) + - localforage: 1.x (fallback storage) + +State Management: + - Zustand: 4.x (lightweight state) + - React Query: 5.x (server state, optional) + +RF Calculations: + - Pure TypeScript (no dependencies) + - Web Workers for parallel processing + - Custom SRTM parser (30m resolution) + +UI Components: + - Headless UI (accessible components) + - Heroicons (icons) + - React Hook Form (forms) + - React Range (sliders) +``` + +### Backend (Optional/Supplementary) + +```yaml +API Framework: + - FastAPI: 0.110.x + - Python: 3.11+ + - Uvicorn: ASGI server + +Authentication: + - matrix-auth-server OAuth client + - JWT token validation + +Data Storage: + - SQLite (config backups) + - File system (30m SRTM tiles) + +CDN/Static Files: + - Caddy reverse proxy + - Pre-processed SRTM tiles +``` + +### Infrastructure + +```yaml +Hosting: + - VPS-A (Germany) or dedicated container + - Caddy 2.x (reverse proxy + TLS) + - Docker + Docker Compose + +Domain: + - rfcp.eliah.one + +Monitoring: + - Uptime Kuma integration +``` + +--- + +## Data Models + +### TypeScript Types (Frontend) + +```typescript +// types/site.ts +export interface Site { + // Identity + id: string; // UUID + name: string; // User-defined name + + // Location + lat: number; // Latitude + lon: number; // Longitude + height: number; // meters above ground (1-100) + + // RF Parameters (all manually editable) + power: number; // dBm (10-50) + gain: number; // dBi (0-25) + frequency: number; // MHz (400-6000, user can enter any value) + + // Antenna Configuration + antennaType: 'omni' | 'sector'; + azimuth?: number; // degrees (0-360), for sector only + beamwidth?: number; // degrees (30-120), for sector only + + // UI/Display + color: string; // hex color for map marker + visible: boolean; // show/hide on map + + // User Metadata (optional, free-text) + notes?: string; // e.g., "ZTE B8200 + Huawei APX4518R0" + equipment?: string; // free-form equipment description + + // Timestamps + createdAt: Date; + updatedAt: Date; +} + +// types/coverage.ts +export interface CoveragePoint { + lat: number; + lon: number; + rsrp: number; // dBm (calculated signal strength) + siteId: string; // which site provides this coverage +} + +export interface CoverageResult { + points: CoveragePoint[]; + calculationTime: number; // milliseconds + totalPoints: number; + bounds: LatLngBounds; + settings: CoverageSettings; +} + +export interface CoverageSettings { + radius: number; // km (calculation radius) + resolution: number; // meters (grid resolution) + rsrpThreshold: number; // dBm (minimum signal to display) +} + +// types/terrain.ts +export interface TerrainTile { + region: string; // e.g., "ukraine-east" + bounds: { + north: number; + south: number; + east: number; + west: number; + }; + resolution: 30; // arc-seconds (30m SRTM) + data: Int16Array; // elevation data + width: number; // tile width in pixels + height: number; // tile height in pixels + downloadedAt: number; // timestamp + sizeBytes: number; // compressed size +} + +// types/frequency.ts +export interface FrequencyBand { + value: number; // MHz + name: string; // e.g., "Band 3" + range: string; // e.g., "1710-1880 MHz" + type: 'LTE' | 'UHF' | 'VHF' | '5G' | 'Custom'; + characteristics: { + range: 'short' | 'medium' | 'long'; + penetration: 'poor' | 'fair' | 'good' | 'excellent'; + typical: string; // use case description + }; +} + +// types/project.ts +export interface ProjectConfig { + id: string; + name: string; + description?: string; + sites: Site[]; + mapCenter: [number, number]; + mapZoom: number; + coverageSettings: CoverageSettings; + createdAt: Date; + updatedAt: Date; + syncedAt?: Date; // for cloud sync +} + +// types/settings.ts +export interface AppSettings { + // UI + theme: 'light' | 'dark' | 'auto'; + language: 'uk' | 'en'; + + // Units + units: 'metric'; // only metric for now + + // Defaults + defaultFrequency: number; // MHz + defaultPower: number; // dBm + defaultHeight: number; // meters + defaultGain: number; // dBi + + // Features + autoDownloadTerrain: boolean; + enableCloudSync: boolean; + + // Map + mapTileServer: string; + defaultMapZoom: number; +} +``` + +### IndexedDB Schema (Dexie) + +```typescript +// db/schema.ts +import Dexie, { Table } from 'dexie'; + +export interface DBTerrainTile { + region: string; // primary key + bounds: { + north: number; + south: number; + east: number; + west: number; + }; + resolution: 30; + data: ArrayBuffer; // compressed elevation data + width: number; + height: number; + downloadedAt: number; + sizeBytes: number; +} + +export interface DBProjectConfig { + id: string; // primary key (UUID) + name: string; + data: string; // JSON serialized ProjectConfig + createdAt: number; + updatedAt: number; + syncedAt?: number; +} + +export class RFCPDatabase extends Dexie { + terrain!: Table; + projects!: Table; + + constructor() { + super('rfcp-db'); + this.version(1).stores({ + terrain: 'region, downloadedAt', + projects: 'id, updatedAt, syncedAt' + }); + } +} + +export const db = new RFCPDatabase(); +``` + +--- + +## Frequency Bands & Configuration + +### Common Frequency Bands (Reference Data) + +```typescript +// constants/frequencies.ts +export const COMMON_FREQUENCIES: FrequencyBand[] = [ + // LTE Bands + { + value: 800, + name: 'Band 20', + range: '791-862 MHz', + type: 'LTE', + characteristics: { + range: 'long', + penetration: 'excellent', + typical: 'Rural coverage, deep building penetration' + } + }, + { + value: 1800, + name: 'Band 3', + range: '1710-1880 MHz', + type: 'LTE', + characteristics: { + range: 'medium', + penetration: 'good', + typical: 'Urban/suburban, most common in Ukraine' + } + }, + { + value: 1900, + name: 'Band 2', + range: '1850-1990 MHz', + type: 'LTE', + characteristics: { + range: 'medium', + penetration: 'good', + typical: 'North America, some military equipment' + } + }, + { + value: 2600, + name: 'Band 7', + range: '2500-2690 MHz', + type: 'LTE', + characteristics: { + range: 'short', + penetration: 'fair', + typical: 'High capacity urban, shorter range' + } + }, + + // Future: UHF/VHF (v1.1) + { + value: 150, + name: 'VHF High', + range: '136-174 MHz', + type: 'VHF', + characteristics: { + range: 'long', + penetration: 'excellent', + typical: 'Tactical radio, emergency services' + } + }, + { + value: 450, + name: 'UHF', + range: '400-470 MHz', + type: 'UHF', + characteristics: { + range: 'medium', + penetration: 'good', + typical: 'Military tactical radio, PMR446' + } + }, + + // Future: 5G NR (v1.2) + { + value: 3500, + name: 'Band 42/43 (n78)', + range: '3400-3800 MHz', + type: '5G', + characteristics: { + range: 'short', + penetration: 'poor', + typical: '5G NR, high bandwidth' + } + } +]; + +export function getFrequencyInfo(frequency: number): FrequencyBand | null { + // Find closest match + return COMMON_FREQUENCIES.find(band => + Math.abs(band.value - frequency) < 50 + ) || null; +} + +export function getWavelength(frequencyMHz: number): string { + const wavelengthMeters = 300 / frequencyMHz; + if (wavelengthMeters >= 1) { + return `${wavelengthMeters.toFixed(2)} m`; + } else { + return `${(wavelengthMeters * 100).toFixed(1)} cm`; + } +} +``` + +--- + +## RF Propagation Models + +### Free Space Path Loss (FSPL) - Universal + +```typescript +/** + * Calculate Free Space Path Loss + * Works with ANY frequency (LTE, UHF, VHF, 5G) + * + * @param distanceKm - Distance in kilometers + * @param frequencyMHz - Frequency in megahertz + * @returns Path loss in dB + */ +export function calculateFSPL(distanceKm: number, frequencyMHz: number): number { + if (distanceKm <= 0) return 0; + + // FSPL(dB) = 20ร—logโ‚โ‚€(d) + 20ร—logโ‚โ‚€(f) + 32.45 + // This formula is universal for all RF frequencies + return 20 * Math.log10(distanceKm) + + 20 * Math.log10(frequencyMHz) + + 32.45; +} + +/** + * Calculate RSRP at receiver location + * Universal calculator - works with any user-provided parameters + * + * @param site - Transmitter site configuration (user inputs) + * @param rxLat - Receiver latitude + * @param rxLon - Receiver longitude + * @param terrainLoss - Additional terrain loss in dB + * @returns RSRP in dBm + */ +export function calculateRSRP( + site: Site, + rxLat: number, + rxLon: number, + terrainLoss: number = 0 +): number { + const distanceKm = haversineDistance( + site.lat, site.lon, + rxLat, rxLon + ); + + // Free space path loss (universal formula) + const fspl = calculateFSPL(distanceKm, site.frequency); + + // Link budget calculation + // RSRP = P_tx + G_tx - FSPL - TerrainLoss - SectorLoss + let rsrp = site.power + site.gain - fspl - terrainLoss; + + // Apply antenna pattern loss (for sector antennas) + if (site.antennaType === 'sector' && site.azimuth !== undefined) { + const bearing = calculateBearing(site.lat, site.lon, rxLat, rxLon); + const relativeAngle = Math.abs(bearing - site.azimuth); + const normalizedAngle = relativeAngle > 180 ? 360 - relativeAngle : relativeAngle; + + const patternLoss = calculateSectorPatternLoss( + normalizedAngle, + site.beamwidth || 65 + ); + rsrp -= patternLoss; + } + + return rsrp; +} +``` + +### Terrain Loss Model (Enhanced for 30m SRTM) + +```typescript +/** + * Calculate additional path loss due to terrain + * Uses 30m resolution SRTM data for accurate results + * Implements simplified knife-edge diffraction + */ +export function calculateTerrainLoss( + txLat: number, + txLon: number, + txHeight: number, + rxLat: number, + rxLon: number, + rxHeight: number = 1.5, + terrain: TerrainManager +): number { + // Get elevations from 30m SRTM data + const txElevation = terrain.getElevation(txLat, txLon); + const rxElevation = terrain.getElevation(rxLat, rxLon); + const distance = haversineDistance(txLat, txLon, rxLat, rxLon); + + // Effective antenna heights above sea level + const h1 = txHeight + txElevation; + const h2 = rxHeight + rxElevation; + + // Sample terrain profile (5 points between TX and RX) + const profilePoints = 5; + let maxObstruction = 0; + + for (let i = 1; i < profilePoints; i++) { + const ratio = i / profilePoints; + const midLat = txLat + (rxLat - txLat) * ratio; + const midLon = txLon + (rxLon - txLon) * ratio; + const midElevation = terrain.getElevation(midLat, midLon); + + // Line-of-sight height at this point + const losHeight = h1 + (h2 - h1) * ratio; + + // Obstruction height + const obstruction = midElevation - losHeight; + if (obstruction > maxObstruction) { + maxObstruction = obstruction; + } + } + + if (maxObstruction > 0) { + // Knife-edge diffraction loss (simplified) + // More accurate with 30m resolution + const v = maxObstruction * Math.sqrt(2 * distance / 0.3); // wavelength ~0.3m for 1GHz + const diffraction = 6.9 + 20 * Math.log10(Math.sqrt((v - 0.1) * (v - 0.1) + 1) + v - 0.1); + return Math.min(diffraction, 40); // Cap at 40 dB + } + + return 0; // Clear line-of-sight +} +``` + +### Sector Antenna Pattern + +```typescript +/** + * Calculate antenna pattern loss for sector antennas + * Based on 3GPP antenna models + * + * @param angleOffBoresight - Angle from antenna boresight in degrees (0-180) + * @param beamwidth - 3dB beamwidth in degrees + * @returns Pattern loss in dB + */ +export function calculateSectorPatternLoss( + angleOffBoresight: number, + beamwidth: number = 65 +): number { + // 3GPP antenna pattern model + const theta3dB = beamwidth / 2; + const sideLobeLevel = 20; // dB + + // Main lobe + if (angleOffBoresight <= theta3dB) { + return 12 * Math.pow(angleOffBoresight / theta3dB, 2); + } + + // Side/back lobes + return Math.min( + 12 * Math.pow(angleOffBoresight / theta3dB, 2), + sideLobeLevel + ); +} +``` + +### Signal Quality Thresholds + +```typescript +// constants/rsrp-thresholds.ts +export const RSRP_THRESHOLDS = { + EXCELLENT: -70, // > -70 dBm (very strong) + GOOD: -85, // -70 to -85 dBm (strong) + FAIR: -100, // -85 to -100 dBm (acceptable) + POOR: -110, // -100 to -110 dBm (marginal) + WEAK: -120, // -110 to -120 dBm (very weak) + NO_SERVICE: -120 // < -120 dBm (no service) +} as const; + +export function getSignalQuality(rsrp: number): string { + if (rsrp >= RSRP_THRESHOLDS.EXCELLENT) return 'excellent'; + if (rsrp >= RSRP_THRESHOLDS.GOOD) return 'good'; + if (rsrp >= RSRP_THRESHOLDS.FAIR) return 'fair'; + if (rsrp >= RSRP_THRESHOLDS.POOR) return 'poor'; + if (rsrp >= RSRP_THRESHOLDS.WEAK) return 'weak'; + return 'no-service'; +} + +export const SIGNAL_COLORS = { + excellent: '#22c55e', // green-600 + good: '#84cc16', // lime-500 + fair: '#eab308', // yellow-500 + poor: '#f97316', // orange-500 + weak: '#ef4444', // red-500 +} as const; + +export function getRSRPColor(rsrp: number): string { + return SIGNAL_COLORS[getSignalQuality(rsrp)]; +} +``` + +--- + +## User Interface Design + +### Main Application Layout + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ RFCP - RF Coverage Planner ๐ŸŒ UA ๐Ÿ‘ค ะžะปะตะณ โš™๏ธ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ๐Ÿ—บ๏ธ Interactive Map โ”‚ โ”‚ +โ”‚ โ”‚ (Leaflet + OSM) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ Click to place base station โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [Heatmap overlay when calculated] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ Sites (3) [+ Add]โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ”ด Station-1 1800 MHz 43 dBm [Edit] [ร—] โ”‚ โ”‚ +โ”‚ โ”‚ ๐Ÿ”ต Station-2 2600 MHz 30 dBm [Edit] [ร—] โ”‚ โ”‚ +โ”‚ โ”‚ ๐ŸŸข Station-3 800 MHz 46 dBm [Edit] [ร—] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ [๐Ÿงฎ ะ ะพะทั€ะฐั…ัƒะฒะฐั‚ะธ ะฟะพะบั€ะธั‚ั‚ั] [๐Ÿ“ฅ ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ terrain] โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Site Configuration Panel (Manual Input) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ“ก ะšะพะฝั„ั–ะณัƒั€ะฐั†ั–ั ะฑะฐะทะพะฒะพั— ัั‚ะฐะฝั†ั–ั— โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ ะะฐะทะฒะฐ ัั‚ะฐะฝั†ั–ั— โ”‚ +โ”‚ [Station-1 ] โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ ะšะพะพั€ะดะธะฝะฐั‚ะธ โ”‚ +โ”‚ Lat: [48.4647] Lon: [35.0462] [๐Ÿ“ ะ—ะผั–ะฝะธั‚ะธ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ะ ะฐะดั–ะพะฟะฐั€ะฐะผะตั‚ั€ะธ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ +โ”‚ โ”‚ +โ”‚ โšก ะŸะพั‚ัƒะถะฝั–ัั‚ัŒ ะฟะตั€ะตะดะฐะฒะฐั‡ะฐ (dBm) โ”‚ +โ”‚ [โ•โ•โ•โ•โ•โ•โ•โ•โ—โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•] 43 โ”‚ +โ”‚ 10 50 โ”‚ +โ”‚ ๐Ÿ’ก ะขะธะฟะพะฒั–: LimeSDR 20, BBU 43, RRU 46 โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ถ ะšะพะตั„ั–ั†ั–ั”ะฝั‚ ะฟั–ะดัะธะปะตะฝะฝั ะฐะฝั‚ะตะฝะธ (dBi) โ”‚ +โ”‚ [โ•โ•โ•โ•โ•โ•โ—โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•] 8 โ”‚ +โ”‚ 0 25 โ”‚ +โ”‚ ๐Ÿ’ก Omni 2-8, Sector 15-18, Parabolic 20-25 โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ป ะ ะพะฑะพั‡ะฐ ั‡ะฐัั‚ะพั‚ะฐ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [800] [1800] [1900] [2600] ะฐะฑะพ: โ”‚ โ”‚ +โ”‚ โ”‚ [1800] MHz โ”‚ โ”‚ +โ”‚ โ”‚ โ„น๏ธ Band 3 (1710-1880 MHz) โ”‚ โ”‚ +โ”‚ โ”‚ ฮป = 16.7 cm โ€ข Medium range โ€ข Good penetr. โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ะคั–ะทะธั‡ะฝั– ะฟะฐั€ะฐะผะตั‚ั€ะธ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ ะ’ะธัะพั‚ะฐ ะฟั–ะดะฒั–ััƒ ะฐะฝั‚ะตะฝะธ (ะผะตั‚ั€ั–ะฒ) โ”‚ +โ”‚ [โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ—โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•] 30 โ”‚ +โ”‚ 1 100 โ”‚ +โ”‚ ๐Ÿ’ก ะ’ะธัะพั‚ะฐ ะฒั–ะด ะทะตะผะปั– ะดะพ ั†ะตะฝั‚ั€ัƒ ะฐะฝั‚ะตะฝะธ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ก ะขะธะฟ ะฐะฝั‚ะตะฝะธ โ”‚ +โ”‚ โ—‰ ะ’ัะตะฝะฐะฟั€ะฐะฒะปะตะฝะฐ (Omni) โ”‚ +โ”‚ โ—‹ ะกะตะบั‚ะพั€ะฝะฐ (Sector) โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€ [Sector parameters (collapsed)] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ“ ะะทะธะผัƒั‚ (ะณั€ะฐะดัƒัะธ) [0] โ”‚ โ”‚ +โ”‚ โ”‚ ๐Ÿ“Š ะจะธั€ะธะฝะฐ ะฟั€ะพะผะตะฝั (ะณั€ะฐะดัƒัะธ) [65] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ะŸั€ะธะผั–ั‚ะบะธ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ ะžะฑะปะฐะดะฝะฐะฝะฝั / ะŸั€ะธะผั–ั‚ะบะธ (ะฝะตะพะฑะพะฒ'ัะทะบะพะฒะพ) โ”‚ +โ”‚ [ZTE B8200 BBU + custom omni antenna ] โ”‚ +โ”‚ [Test deployment, field validation needed ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ’พ Quick Templates (optional) โ”‚ +โ”‚ [๐Ÿš€ LimeSDR] [๐Ÿ“ก Low BBU] [๐Ÿ“ก High BBU] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ +โ”‚ โ”‚ +โ”‚ [๐Ÿ’พ ะ—ะฑะตั€ะตะณั‚ะธ ะทะผั–ะฝะธ] [๐Ÿ—‘๏ธ ะ’ะธะดะฐะปะธั‚ะธ ัั‚ะฐะฝั†ั–ัŽ] โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Key UI Principles:** +- โœ… **Sliders with values** - ะฒะธะทัƒะฐะปัŒะฝะธะน feedback + ั‚ะพั‡ะฝะธะน ะบะพะฝั‚ั€ะพะปัŒ +- โœ… **Helpful hints** - ั‚ะธะฟะพะฒั– ะทะฝะฐั‡ะตะฝะฝั ะฟั–ะด ะฟะพะปัะผะธ +- โœ… **Contextual info** - band info, wavelength automatically shown +- โœ… **Progressive disclosure** - sector params hidden if omni selected +- โœ… **Optional templates** - ัˆะฒะธะดะบะธะน ัั‚ะฐั€ั‚, ะฐะปะต ะฝะต ะพะฑะพะฒ'ัะทะบะพะฒะพ +- โœ… **Free-text notes** - ะบะพั€ะธัั‚ัƒะฒะฐั‡ ะพะฟะธััƒั” ัะฒั–ะน setup + +--- + +## Terrain Data Management (30m SRTM) + +### Ukraine Terrain Regions (Updated for 30m) + +```typescript +// terrain/regions.ts +export const UKRAINE_REGIONS = { + 'ukraine-west': { + name: 'ะ—ะฐั…ั–ะดะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', + nameEn: 'Western Ukraine', + bounds: { + north: 52.5, + south: 47.9, + west: 22.1, + east: 27.0 + }, + resolution: 30, // meters (1 arc-second) + size: '~400MB', // compressed + includes: ['ะ›ัŒะฒั–ะฒ', 'ะ†ะฒะฐะฝะพ-ะคั€ะฐะฝะบั–ะฒััŒะบ', 'ะงะตั€ะฝั–ะฒั†ั–', 'ะขะตั€ะฝะพะฟั–ะปัŒ', 'ะ›ัƒั†ัŒะบ'] + }, + 'ukraine-center': { + name: 'ะฆะตะฝั‚ั€ะฐะปัŒะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', + nameEn: 'Central Ukraine', + bounds: { + north: 52.5, + south: 48.0, + west: 27.0, + east: 32.0 + }, + resolution: 30, + size: '~400MB', + includes: ['ะšะธั—ะฒ', 'ะ’ั–ะฝะฝะธั†ั', 'ะ–ะธั‚ะพะผะธั€', 'ะงะตั€ะบะฐัะธ', 'ะŸะพะปั‚ะฐะฒะฐ'] + }, + 'ukraine-east': { + name: 'ะกั…ั–ะดะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', + nameEn: 'Eastern Ukraine', + bounds: { + north: 50.5, + south: 47.0, + west: 32.0, + east: 40.3 + }, + resolution: 30, + size: '~400MB', + includes: ['ะฅะฐั€ะบั–ะฒ', 'ะ”ะพะฝะตั†ัŒะบ', 'ะ›ัƒะณะฐะฝััŒะบ', 'ะ”ะฝั–ะฟั€ะพ', 'ะšั€ะธะฒะธะน ะ ั–ะณ'] + }, + 'ukraine-south': { + name: 'ะŸั–ะฒะดะตะฝะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', + nameEn: 'Southern Ukraine', + bounds: { + north: 48.0, + south: 44.4, + west: 27.0, + east: 38.0 + }, + resolution: 30, + size: '~400MB', + includes: ['ะžะดะตัะฐ', 'ะœะธะบะพะปะฐั—ะฒ', 'ะฅะตั€ัะพะฝ', 'ะ—ะฐะฟะพั€ั–ะถะถั', 'ะœะตะปั–ั‚ะพะฟะพะปัŒ'] + }, + 'ukraine-north': { + name: 'ะŸั–ะฒะฝั–ั‡ะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', + nameEn: 'Northern Ukraine', + bounds: { + north: 52.4, + south: 50.4, + west: 27.0, + east: 35.0 + }, + resolution: 30, + size: '~300MB', + includes: ['ะงะตั€ะฝั–ะณั–ะฒ', 'ะกัƒะผะธ', 'ะ ั–ะฒะฝะต', 'ะ–ะผะตั€ะธะฝะบะฐ'] + } +} as const; + +export type UkraineRegion = keyof typeof UKRAINE_REGIONS; + +/** + * Determine which region contains the given coordinates + */ +export function latLonToRegion(lat: number, lon: number): UkraineRegion | null { + for (const [region, data] of Object.entries(UKRAINE_REGIONS)) { + const { north, south, west, east } = data.bounds; + if (lat >= south && lat <= north && lon >= west && lon <= east) { + return region as UkraineRegion; + } + } + return null; +} + +/** + * Get total size for typical 2-3 region deployment + */ +export function getTypicalDeploymentSize(): string { + // Most users will download 2-3 regions + // ukraine-east (400MB) + ukraine-south (400MB) + ukraine-center (400MB) + return '~800MB - 1.2GB for 2-3 regions'; +} +``` + +### Enhanced Terrain Manager (30m resolution) + +```typescript +// terrain/manager.ts +export class TerrainManager { + private cache = new Map(); + private downloading = new Map>(); + + constructor(private db: RFCPDatabase) {} + + /** + * Get elevation at specific coordinates + * Uses 30m SRTM data with bilinear interpolation + * Auto-downloads terrain if needed and online + */ + async getElevation(lat: number, lon: number): Promise { + const region = latLonToRegion(lat, lon); + + if (!region) { + console.warn('Coordinates outside Ukraine bounds'); + return 0; + } + + // Check memory cache + if (this.cache.has(region)) { + return this.interpolateElevation(this.cache.get(region)!, lat, lon); + } + + // Check IndexedDB cache + const cached = await this.db.terrain.get(region); + if (cached) { + const tile = this.deserializeTile(cached); + this.cache.set(region, tile); + return this.interpolateElevation(tile, lat, lon); + } + + // Download if online + if (navigator.onLine) { + console.log(`Auto-downloading terrain for ${region}`); + await this.downloadRegion(region); + return this.getElevation(lat, lon); // Recursive call after download + } + + console.warn(`Terrain data for ${region} not available offline`); + return 0; + } + + /** + * Download 30m SRTM terrain data for region + * ~400MB per region + */ + async downloadRegion(region: UkraineRegion): Promise { + // Prevent duplicate downloads + if (this.downloading.has(region)) { + await this.downloading.get(region); + return; + } + + const promise = this._downloadRegion(region); + this.downloading.set(region, promise); + + try { + await promise; + } finally { + this.downloading.delete(region); + } + } + + private async _downloadRegion(region: UkraineRegion): Promise { + const regionData = UKRAINE_REGIONS[region]; + console.log(`Downloading 30m terrain: ${regionData.name} (~${regionData.size})`); + + const startTime = Date.now(); + + const response = await fetch(`/api/terrain/${region}`); + if (!response.ok) { + throw new Error(`Failed to download terrain: ${response.statusText}`); + } + + const buffer = await response.arrayBuffer(); + const tile = this.parseSRTM30m(buffer, region); + + const downloadTime = Date.now() - startTime; + console.log( + `Terrain downloaded: ${regionData.name} ` + + `(${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB in ${(downloadTime / 1000).toFixed(1)}s)` + ); + + // Save to IndexedDB + await this.db.terrain.put({ + region, + bounds: regionData.bounds, + resolution: 30, + data: buffer, + width: tile.width, + height: tile.height, + downloadedAt: Date.now(), + sizeBytes: buffer.byteLength + }); + + // Cache in memory + this.cache.set(region, tile); + + return tile; + } + + /** + * Parse 30m SRTM binary data (1 arc-second) + */ + private parseSRTM30m(buffer: ArrayBuffer, region: UkraineRegion): TerrainTile { + const bounds = UKRAINE_REGIONS[region].bounds; + const resolution = 1; // 1 arc-second (~30m) + + // Calculate tile dimensions + // 1 degree = 3600 arc-seconds + // With 1 arc-second resolution, we get 3600 pixels per degree + const width = Math.ceil((bounds.east - bounds.west) * 3600 / resolution); + const height = Math.ceil((bounds.north - bounds.south) * 3600 / resolution); + + // Parse as 16-bit signed integers (big-endian) + // SRTM format: 2 bytes per sample + const data = new Int16Array(buffer); + + // Handle byte order (SRTM is big-endian) + if (!this.isBigEndian()) { + for (let i = 0; i < data.length; i++) { + data[i] = ((data[i] & 0xFF) << 8) | ((data[i] >> 8) & 0xFF); + } + } + + return { + region, + bounds, + resolution: 30, // meters + data, + width, + height, + downloadedAt: Date.now(), + sizeBytes: buffer.byteLength + }; + } + + /** + * Bilinear interpolation for smooth elevation lookup + * Essential for 30m data to get accurate intermediate values + */ + private interpolateElevation( + tile: TerrainTile, + lat: number, + lon: number + ): number { + const { bounds, width, height, data } = tile; + + // Convert lat/lon to pixel coordinates + const x = ((lon - bounds.west) / (bounds.east - bounds.west)) * (width - 1); + const y = ((bounds.north - lat) / (bounds.north - bounds.south)) * (height - 1); + + // Get surrounding pixel indices + const x0 = Math.floor(x); + const x1 = Math.min(x0 + 1, width - 1); + const y0 = Math.floor(y); + const y1 = Math.min(y0 + 1, height - 1); + + // Interpolation weights + const dx = x - x0; + const dy = y - y0; + + // Get elevation at 4 corners + const e00 = data[y0 * width + x0]; + const e10 = data[y0 * width + x1]; + const e01 = data[y1 * width + x0]; + const e11 = data[y1 * width + x1]; + + // Handle void data (SRTM uses -32768 for no data) + if (e00 === -32768 || e10 === -32768 || e01 === -32768 || e11 === -32768) { + return 0; // Default to sea level for void areas + } + + // Bilinear interpolation + const e0 = e00 * (1 - dx) + e10 * dx; + const e1 = e01 * (1 - dx) + e11 * dx; + + return e0 * (1 - dy) + e1 * dy; + } + + /** + * Get terrain profile between two points + * Useful for line-of-sight analysis + */ + async getTerrainProfile( + lat1: number, lon1: number, + lat2: number, lon2: number, + numSamples: number = 50 + ): Promise { + const profile: number[] = []; + + for (let i = 0; i <= numSamples; i++) { + const ratio = i / numSamples; + const lat = lat1 + (lat2 - lat1) * ratio; + const lon = lon1 + (lon2 - lon1) * ratio; + const elevation = await this.getElevation(lat, lon); + profile.push(elevation); + } + + return profile; + } + + /** + * Get download status for all regions + */ + async getDownloadStatus(): Promise> { + const status: any = {}; + + for (const region of Object.keys(UKRAINE_REGIONS) as UkraineRegion[]) { + const cached = await this.db.terrain.get(region); + status[region] = !!cached; + } + + return status; + } + + /** + * Get total storage used by terrain data + */ + async getStorageUsed(): Promise { + const tiles = await this.db.terrain.toArray(); + return tiles.reduce((sum, tile) => sum + tile.sizeBytes, 0); + } + + /** + * Clear terrain cache (free up space) + */ + async clearCache(region?: UkraineRegion): Promise { + if (region) { + await this.db.terrain.delete(region); + this.cache.delete(region); + } else { + await this.db.terrain.clear(); + this.cache.clear(); + } + } + + private isBigEndian(): boolean { + const buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 256, true); + return new Int16Array(buffer)[0] !== 256; + } +} +``` + +--- + +## RF Calculation Engine (Universal) + +### Core Calculator Class + +```typescript +// rf/calculator.ts +export class RFCalculator { + private terrain: TerrainManager; + + constructor(terrain: TerrainManager) { + this.terrain = terrain; + } + + /** + * Calculate coverage for multiple sites + * Works with any user-provided parameters (LTE, UHF, VHF, etc.) + * Uses Web Workers for parallel processing + */ + async calculateCoverage( + sites: Site[], + bounds: LatLngBounds, + settings: CoverageSettings + ): Promise { + const startTime = performance.now(); + + // Generate grid of points to calculate + const gridPoints = this.generateGrid(bounds, settings.resolution); + + console.log( + `Calculating coverage for ${sites.length} sites, ` + + `${gridPoints.length} points (${settings.resolution}m resolution)` + ); + + // Split work across multiple workers (4 parallel threads) + const numWorkers = Math.min(4, navigator.hardwareConcurrency || 4); + const workers = this.createWorkers(numWorkers); + const chunks = this.chunkArray(gridPoints, numWorkers); + + // Calculate coverage in parallel + const results = await Promise.all( + chunks.map((chunk, i) => + this.calculateChunk( + workers[i], + sites, + chunk, + settings.rsrpThreshold + ) + ) + ); + + // Merge results from all workers + const allPoints = results.flat(); + + // Cleanup workers + workers.forEach(w => w.terminate()); + + const calculationTime = performance.now() - startTime; + + console.log( + `Coverage calculated: ${allPoints.length} points in ` + + `${(calculationTime / 1000).toFixed(2)}s` + ); + + return { + points: allPoints, + calculationTime, + totalPoints: allPoints.length, + bounds, + settings + }; + } + + /** + * Generate grid of lat/lon points + */ + private generateGrid( + bounds: LatLngBounds, + resolution: number // meters + ): GridPoint[] { + const points: GridPoint[] = []; + + // Convert resolution to degrees (approximate) + const latStep = resolution / 111000; // ~111km per degree latitude + const center = bounds.getCenter(); + const lonStep = resolution / (111000 * Math.cos(center.lat * Math.PI / 180)); + + let lat = bounds.getSouth(); + while (lat <= bounds.getNorth()) { + let lon = bounds.getWest(); + while (lon <= bounds.getEast()) { + points.push({ lat, lon }); + lon += lonStep; + } + lat += latStep; + } + + return points; + } + + /** + * Create Web Workers for parallel calculation + */ + private createWorkers(count: number): Worker[] { + const workers: Worker[] = []; + for (let i = 0; i < count; i++) { + workers.push(new Worker('/workers/rf-worker.js')); + } + return workers; + } + + /** + * Split array into chunks for parallel processing + */ + private chunkArray(array: T[], numChunks: number): T[][] { + const chunks: T[][] = []; + const chunkSize = Math.ceil(array.length / numChunks); + + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + + return chunks; + } + + /** + * Calculate coverage for a chunk of points (worker task) + */ + private async calculateChunk( + worker: Worker, + sites: Site[], + points: GridPoint[], + rsrpThreshold: number + ): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Worker timeout')); + }, 30000); // 30 second timeout + + worker.postMessage({ + type: 'calculate', + sites, + points, + rsrpThreshold + }); + + worker.onmessage = (e) => { + clearTimeout(timeout); + if (e.data.type === 'complete') { + resolve(e.data.results); + } else if (e.data.type === 'error') { + reject(new Error(e.data.message)); + } + }; + + worker.onerror = (err) => { + clearTimeout(timeout); + reject(err); + }; + }); + } +} +``` + +### Web Worker Implementation + +```typescript +// workers/rf-worker.ts +// This runs in a separate thread for parallel processing + +self.onmessage = async (e) => { + const { type, sites, points, rsrpThreshold } = e.data; + + if (type === 'calculate') { + try { + const results: CoveragePoint[] = []; + + // Process each point in this chunk + for (const point of points) { + let bestRSRP = -Infinity; + let bestSiteId = ''; + + // Find best serving site for this point + for (const site of sites) { + const rsrp = calculatePointRSRP(site, point); + + if (rsrp > bestRSRP) { + bestRSRP = rsrp; + bestSiteId = site.id; + } + } + + // Only include points above threshold + if (bestRSRP >= rsrpThreshold) { + results.push({ + lat: point.lat, + lon: point.lon, + rsrp: bestRSRP, + siteId: bestSiteId + }); + } + } + + self.postMessage({ type: 'complete', results }); + } catch (error) { + self.postMessage({ + type: 'error', + message: error.message + }); + } + } +}; + +/** + * Calculate RSRP at a specific point + * Universal formula - works with any frequency + */ +function calculatePointRSRP(site: any, point: any): number { + const distance = haversineDistance( + site.lat, site.lon, + point.lat, point.lon + ); + + // Free space path loss (universal) + const fspl = 20 * Math.log10(distance) + + 20 * Math.log10(site.frequency) + + 32.45; + + // Link budget + let rsrp = site.power + site.gain - fspl; + + // TODO: Add terrain loss (requires terrain data in worker) + // For MVP, terrain loss will be added in Phase 4 + + // Apply sector antenna pattern + if (site.antennaType === 'sector' && site.azimuth !== undefined) { + const bearing = calculateBearing( + site.lat, site.lon, + point.lat, point.lon + ); + const relativeAngle = Math.abs(bearing - site.azimuth); + const normalizedAngle = relativeAngle > 180 ? 360 - relativeAngle : relativeAngle; + + const patternLoss = calculateSectorPatternLoss( + normalizedAngle, + site.beamwidth || 65 + ); + rsrp -= patternLoss; + } + + return rsrp; +} + +/** + * Haversine distance formula + */ +function haversineDistance( + lat1: number, lon1: number, + lat2: number, lon2: number +): number { + const R = 6371; // Earth radius in km + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1 * Math.PI / 180) * + Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon/2) * Math.sin(dLon/2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; +} + +/** + * Calculate bearing from point A to point B + */ +function calculateBearing( + lat1: number, lon1: number, + lat2: number, lon2: number +): number { + const dLon = (lon2 - lon1) * Math.PI / 180; + const y = Math.sin(dLon) * Math.cos(lat2 * Math.PI / 180); + const x = Math.cos(lat1 * Math.PI / 180) * Math.sin(lat2 * Math.PI / 180) - + Math.sin(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.cos(dLon); + const bearing = Math.atan2(y, x) * 180 / Math.PI; + return (bearing + 360) % 360; +} + +/** + * Sector antenna pattern loss + */ +function calculateSectorPatternLoss( + angleOffBoresight: number, + beamwidth: number +): number { + const theta3dB = beamwidth / 2; + const sideLobeLevel = 20; + + if (angleOffBoresight <= theta3dB) { + return 12 * Math.pow(angleOffBoresight / theta3dB, 2); + } + + return Math.min( + 12 * Math.pow(angleOffBoresight / theta3dB, 2), + sideLobeLevel + ); +} +``` + +--- + +## Development Roadmap + +### Phase 1: Core UI & Manual Input (Days 1-2) + +**Goal:** Working PWA with map and flexible input UI + +**Tasks:** +- [ ] Initialize React + TypeScript + Vite project +- [ ] Configure Tailwind CSS +- [ ] Setup Workbox for Service Worker +- [ ] Create PWA manifest.json +- [ ] Implement IndexedDB schema (Dexie) +- [ ] Setup Leaflet map component +- [ ] Add OpenStreetMap tile layer +- [ ] Implement click-to-place site marker +- [ ] Create manual input form with: + - [ ] Power slider (10-50 dBm) + - [ ] Gain slider (0-25 dBi) + - [ ] Dynamic frequency input (quick buttons + custom) + - [ ] Height slider (1-100m) + - [ ] Antenna type selector (omni/sector) + - [ ] Sector parameters (azimuth, beamwidth) + - [ ] Free-text notes field +- [ ] Implement frequency band info display +- [ ] Add wavelength calculator +- [ ] Create optional quick templates +- [ ] Setup Zustand state management +- [ ] Add site list panel (view/edit/delete) + +**Deliverables:** +- โœ… Working PWA that can be installed +- โœ… Map with OSM tiles (cached offline) +- โœ… Can place multiple site markers +- โœ… Fully flexible manual input form +- โœ… Contextual frequency information + +**Testing:** +```bash +npm run dev +# Open http://localhost:5173 +# Click on map โ†’ marker appears +# Configure site parameters manually +# Add multiple sites +# Verify all inputs work +``` + +--- + +### Phase 2: RF Calculation Engine (Days 3-4) + +**Goal:** Working RF calculations in browser (universal) + +**Tasks:** +- [ ] Implement universal FSPL calculator +- [ ] Create haversine distance function +- [ ] Implement bearing calculator +- [ ] Create grid generator +- [ ] Implement sector antenna pattern calculator +- [ ] Create Web Worker template +- [ ] Implement RFCalculator class +- [ ] Add multi-site coverage aggregation +- [ ] Create signal quality color mapping +- [ ] Add calculation progress indicator +- [ ] Unit tests for RF functions +- [ ] Performance optimization (target < 3s for 3 sites) + +**Deliverables:** +- โœ… Can calculate coverage for single site (any frequency) +- โœ… Can calculate composite coverage (multiple sites) +- โœ… Calculations run in Web Workers (non-blocking UI) +- โœ… Results ready for visualization +- โœ… Works with user's custom parameters + +**Testing:** +```typescript +// Test universal FSPL +const fspl1800 = calculateFSPL(1.0, 1800); +const fspl150 = calculateFSPL(1.0, 150); // VHF +console.assert(fspl150 < fspl1800, 'Lower freq = less path loss'); + +// Test coverage calculation +const calculator = new RFCalculator(terrain); +const result = await calculator.calculateCoverage( + [customSite], // User's parameters + bounds, + { radius: 5, resolution: 100, rsrpThreshold: -120 } +); +console.log(`Calculated ${result.points.length} points`); +``` + +--- + +### Phase 3: Heatmap Visualization (Days 5-6) + +**Goal:** Beautiful coverage heatmap on map + +**Tasks:** +- [ ] Integrate Leaflet.heat plugin +- [ ] Convert coverage points to heatmap format +- [ ] Implement color gradient mapping (RSRP โ†’ color) +- [ ] Add heatmap layer to map +- [ ] Create legend component with RSRP ranges +- [ ] Add toggle heatmap visibility +- [ ] Implement zoom-dependent detail levels +- [ ] Add loading spinner during calculation +- [ ] Optimize rendering for 5000+ points +- [ ] Add "Download as PNG" feature + +**Deliverables:** +- โœ… Colorful heatmap overlay on map +- โœ… Legend showing RSRP ranges and colors +- โœ… Smooth performance with large datasets +- โœ… Toggle to show/hide heatmap +- โœ… Visual feedback during calculation + +**Testing:** +```typescript +// Visual test +// 1. Place site with power=43 dBm, freq=1800 MHz +// 2. Calculate coverage +// 3. Should see green circle near site (~2km radius) +// 4. Yellow/orange further away (~3-5km) +// 5. Red at edges (~5-7km) +// 6. Toggle heatmap on/off smoothly +// 7. Performance: rendering < 2s for 3 sites +``` + +--- + +### Phase 4: Terrain Integration (Days 7-9) + +**Goal:** Real 30m terrain data in calculations + +**Tasks:** +- [ ] Implement enhanced TerrainManager class +- [ ] Create 30m SRTM parser (1 arc-second) +- [ ] Add byte-order handling (big-endian) +- [ ] Implement bilinear interpolation +- [ ] Add region detection (lat/lon โ†’ region) +- [ ] Implement terrain loss calculation +- [ ] Add terrain profile generation +- [ ] Create terrain download UI +- [ ] Implement auto-download on map pan +- [ ] Add download progress indicator (with size display) +- [ ] Store terrain in IndexedDB +- [ ] Handle offline terrain access +- [ ] Add storage management UI +- [ ] Test with real Ukraine terrain + +**Deliverables:** +- โœ… Can download 30m terrain regions (~400MB each) +- โœ… Terrain data persists offline +- โœ… Elevation lookup works with bilinear interpolation +- โœ… Coverage calculations consider terrain (more accurate) +- โœ… Storage management (view usage, clear cache) + +**Testing:** +```typescript +// Download terrain +const terrain = new TerrainManager(db); +await terrain.downloadRegion('ukraine-east'); // ~400MB, 30-60s + +// Check elevation accuracy +const dnipro = await terrain.getElevation(48.4647, 35.0462); +console.log(`Dnipro elevation: ${dnipro}m`); // Should be ~50-70m + +// Check storage +const used = await terrain.getStorageUsed(); +console.log(`Storage used: ${(used / 1024 / 1024).toFixed(0)} MB`); + +// Test offline +// Go offline +// Pan to Dnipro +// Should still get elevation data from IndexedDB +``` + +--- + +### Phase 5: Multi-Site & UX Polish (Days 10-11) + +**Goal:** Production-ready MVP + +**Tasks:** +- [ ] Support multiple sites on map (tested with 5+ sites) +- [ ] Add site list panel with thumbnails +- [ ] Implement site color coding +- [ ] Add calculation settings panel (radius, resolution) +- [ ] Implement site drag-to-reposition +- [ ] Add keyboard shortcuts (Delete, Esc, etc.) +- [ ] Create user guide overlay (first-time experience) +- [ ] Mobile responsive design +- [ ] Touch gestures for mobile (pinch-zoom, pan) +- [ ] Add error handling & toast notifications +- [ ] Implement undo/redo for site changes +- [ ] Add project save/load functionality +- [ ] Export coverage as PNG/GeoJSON +- [ ] Performance optimization +- [ ] Internationalization (Ukrainian/English) + +**Deliverables:** +- โœ… Can manage 5+ sites easily +- โœ… Intuitive UI, works on mobile/tablet +- โœ… Polished user experience +- โœ… Helpful onboarding +- โœ… Bilingual support + +--- + +### Phase 6: Backend & Auth (Days 12-13) + +**Goal:** Optional cloud sync + +**Tasks:** +- [ ] Create FastAPI backend project +- [ ] Implement terrain CDN endpoints +- [ ] Setup 30m SRTM data preprocessing +- [ ] Add MAS OAuth integration +- [ ] Create config sync API +- [ ] Implement JWT auth middleware +- [ ] Setup SQLite for config storage +- [ ] Create docker-compose.yml +- [ ] Configure Caddy reverse proxy +- [ ] Add background sync in Service Worker +- [ ] Test multi-device sync + +**Deliverables:** +- โœ… Backend API running +- โœ… Can login with MAS +- โœ… Configs sync across devices +- โœ… Terrain downloads work via CDN + +--- + +### Phase 7: Deployment (Days 14-15) + +**Goal:** Live at rfcp.eliah.one + +**Tasks:** +- [ ] Setup domain DNS (rfcp.eliah.one) +- [ ] Deploy backend to VPS-A +- [ ] Deploy frontend with Caddy +- [ ] Configure SSL/TLS +- [ ] Upload 30m SRTM terrain data +- [ ] Test from external network +- [ ] Test on mobile devices (iOS/Android) +- [ ] Create offline installer ZIP +- [ ] Write user documentation +- [ ] Setup monitoring (Uptime Kuma) +- [ ] Create backup procedures +- [ ] Performance testing +- [ ] Security audit + +**Deliverables:** +- โœ… Live at https://rfcp.eliah.one +- โœ… Works on desktop/mobile/tablet +- โœ… Offline installer available (~200MB base + terrain) +- โœ… Documentation complete (Ukrainian/English) +- โœ… Monitoring active + +--- + +## Post-MVP Roadmap + +### v1.1 - UHF/VHF Support (Week 4-5) ๐ŸŽฏ + +**Goal:** Add tactical radio frequency planning + +**New Features:** +- [ ] VHF frequencies (30-300 MHz) + - [ ] VHF Low (30-50 MHz) - long range + - [ ] VHF High (136-174 MHz) - tactical radio + - [ ] FM broadcast (88-108 MHz) +- [ ] UHF frequencies (300-3000 MHz) + - [ ] UHF (400-470 MHz) - military tactical + - [ ] PMR446 (446 MHz) - personal radio + - [ ] Amateur radio bands +- [ ] Enhanced propagation models: + - [ ] Longley-Rice (VHF/UHF specific) + - [ ] ITU-R P.1546 (broadcasting) +- [ ] Frequency-specific characteristics: + - [ ] Ground wave propagation (VHF low) + - [ ] Ionospheric reflection (HF) + - [ ] Tropospheric scatter +- [ ] New UI elements: + - [ ] "Radio Type" selector (LTE/VHF/UHF/Amateur) + - [ ] Band-specific presets + - [ ] Propagation mode selector + +**Technical Implementation:** +```typescript +// Extended frequency support +export const RADIO_TYPES = { + 'lte': { + name: 'LTE (4G)', + freqRange: [700, 2700], + propagationModel: 'FSPL + terrain' + }, + 'vhf': { + name: 'VHF Radio', + freqRange: [30, 300], + propagationModel: 'Longley-Rice' + }, + 'uhf': { + name: 'UHF Radio', + freqRange: [300, 3000], + propagationModel: 'Longley-Rice' + }, + 'amateur': { + name: 'Amateur Radio', + freqRange: [1.8, 30000], // HF to SHF + propagationModel: 'Mixed' + } +}; +``` + +**Use Cases:** +- โœ… Planning tactical radio networks (VHF 150 MHz) +- โœ… UHF repeater placement (450 MHz) +- โœ… Amateur radio link planning +- โœ… Emergency services coordination + +--- + +### v1.2 - Advanced Features (Week 6-7) + +**Features:** +- [ ] Okumura-Hata propagation model (urban areas) +- [ ] COST 231 extension (dense urban) +- [ ] 3D sector antenna visualization +- [ ] Interference calculation between sites +- [ ] Best server boundary display +- [ ] Handover zone visualization +- [ ] Link budget calculator +- [ ] Export to KML/KMZ (Google Earth) +- [ ] PDF report generation +- [ ] CSV import/export +- [ ] Real RF measurements overlay + +--- + +### v2.0 - Advanced Platform (Month 3+) + +**Features:** +- [ ] 3D terrain visualization (Three.js) +- [ ] Drive test integration +- [ ] Network optimization algorithms +- [ ] Machine learning coverage prediction +- [ ] Multi-technology support (5G NR) +- [ ] Collaborative planning (WebRTC) +- [ ] Cost estimation +- [ ] Weather effects modeling +- [ ] Real-time spectrum monitoring + +--- + +## File Structure + +``` +rfcp/ +โ”œโ”€โ”€ .github/ +โ”‚ โ””โ”€โ”€ workflows/ +โ”‚ โ””โ”€โ”€ deploy.yml +โ”‚ +โ”œโ”€โ”€ frontend/ +โ”‚ โ”œโ”€โ”€ public/ +โ”‚ โ”‚ โ”œโ”€โ”€ icons/ # PWA icons +โ”‚ โ”‚ โ”œโ”€โ”€ workers/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ rf-worker.js # RF calculation worker +โ”‚ โ”‚ โ”œโ”€โ”€ manifest.json # PWA manifest +โ”‚ โ”‚ โ”œโ”€โ”€ offline.html # Offline fallback +โ”‚ โ”‚ โ””โ”€โ”€ robots.txt +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ”œโ”€โ”€ components/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ map/ +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Map.tsx # Main map component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SiteMarker.tsx # Site marker +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Heatmap.tsx # Heatmap layer +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Legend.tsx # Signal legend +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ panels/ +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SiteForm.tsx # Manual input form +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SiteList.tsx # Sites list +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Settings.tsx # Settings panel +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TerrainPanel.tsx # Terrain management +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ FrequencySelector.tsx # Dynamic freq input +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ui/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Button.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Input.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Slider.tsx +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Select.tsx +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Toast.tsx +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ rf/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ calculator.ts # Main RF calculator +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ fspl.ts # Universal FSPL +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain-loss.ts # Terrain calculations +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ antenna-pattern.ts # Sector patterns +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils.ts # Haversine, bearing, etc. +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ terrain/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ manager.ts # TerrainManager (30m) +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ regions.ts # Ukraine regions +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ parser.ts # SRTM parser +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ interpolation.ts # Bilinear interpolation +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ store/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sites.ts # Sites state (Zustand) +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ settings.ts # Settings state +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ coverage.ts # Coverage state +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.ts # Auth state +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ db/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schema.ts # IndexedDB schema +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ migrations.ts # DB migrations +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ api/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ client.ts # API client +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.ts # Terrain endpoints +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sync.ts # Sync endpoints +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.ts # Auth endpoints +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ types/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ site.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ coverage.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ frequency.ts +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ constants/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ frequencies.ts # Common bands +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ rsrp-thresholds.ts +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ colors.ts +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ utils/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ geo.ts # Geographic utilities +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ colors.ts # Color utilities +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ format.ts # Formatters +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ validation.ts # Input validation +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ useRFCalculator.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ useTerrain.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ useOffline.ts +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ useFrequencyInfo.ts +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ i18n/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ uk.json # Ukrainian +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ en.json # English +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ App.tsx +โ”‚ โ”‚ โ”œโ”€โ”€ main.tsx +โ”‚ โ”‚ โ”œโ”€โ”€ service-worker.ts # Service Worker +โ”‚ โ”‚ โ””โ”€โ”€ vite-env.d.ts +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ tests/ +โ”‚ โ”‚ โ”œโ”€โ”€ unit/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ fspl.test.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ haversine.test.ts +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.test.ts +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ antenna-pattern.test.ts +โ”‚ โ”‚ โ””โ”€โ”€ integration/ +โ”‚ โ”‚ โ”œโ”€โ”€ calculator.test.ts +โ”‚ โ”‚ โ””โ”€โ”€ terrain-manager.test.ts +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ .gitignore +โ”‚ โ”œโ”€โ”€ package.json +โ”‚ โ”œโ”€โ”€ tsconfig.json +โ”‚ โ”œโ”€โ”€ vite.config.ts +โ”‚ โ”œโ”€โ”€ tailwind.config.js +โ”‚ โ””โ”€โ”€ README.md +โ”‚ +โ”œโ”€โ”€ backend/ +โ”‚ โ”œโ”€โ”€ app/ +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ main.py # FastAPI app +โ”‚ โ”‚ โ”œโ”€โ”€ config.py # Configuration +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ api/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.py # Terrain CDN endpoints +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sync.py # Sync endpoints +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.py # MAS OAuth +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ services/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.py # Terrain service +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sync.py # Sync service +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.py # Auth service +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€ models/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ config.py # Pydantic models +โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€ db/ +โ”‚ โ”‚ โ””โ”€โ”€ database.py # SQLite setup +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ data/ +โ”‚ โ”‚ โ””โ”€โ”€ terrain/ # 30m SRTM files +โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-west.hgt +โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-center.hgt +โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-east.hgt +โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-south.hgt +โ”‚ โ”‚ โ””โ”€โ”€ ukraine-north.hgt +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ scripts/ +โ”‚ โ”‚ โ”œโ”€โ”€ download-srtm.py # Download SRTM data +โ”‚ โ”‚ โ””โ”€โ”€ preprocess-terrain.py # Preprocess for web +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ tests/ +โ”‚ โ”‚ โ”œโ”€โ”€ test_terrain.py +โ”‚ โ”‚ โ””โ”€โ”€ test_sync.py +โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€ .gitignore +โ”‚ โ”œโ”€โ”€ requirements.txt +โ”‚ โ”œโ”€โ”€ Dockerfile +โ”‚ โ””โ”€โ”€ README.md +โ”‚ +โ”œโ”€โ”€ docs/ +โ”‚ โ”œโ”€โ”€ README.md # Project overview +โ”‚ โ”œโ”€โ”€ USAGE-uk.md # User guide (Ukrainian) +โ”‚ โ”œโ”€โ”€ USAGE-en.md # User guide (English) +โ”‚ โ”œโ”€โ”€ API.md # API documentation +โ”‚ โ””โ”€โ”€ DEPLOYMENT.md # Deployment guide +โ”‚ +โ”œโ”€โ”€ docker-compose.yml +โ”œโ”€โ”€ Caddyfile +โ”œโ”€โ”€ .gitignore +โ”œโ”€โ”€ LICENSE +โ””โ”€โ”€ RFCP-TechSpec-v3.0.md # This file +``` + +--- + +## Performance Targets + +### Load Time +- Initial page load (online): < 3 seconds +- Initial page load (offline, cached): < 1 second +- Service Worker registration: < 500ms +- PWA installation prompt: < 2 seconds after load + +### Calculation Performance +- Single site, 5km radius, 100m resolution: < 1 second +- Three sites, 5km radius, 100m resolution: < 2.5 seconds +- Five sites, 10km radius, 100m resolution: < 8 seconds +- With 30m terrain data: +20% calculation time (acceptable) + +### Rendering Performance +- Heatmap render (1000 points): < 500ms +- Heatmap render (5000 points): < 1.5 seconds +- Heatmap render (10000 points): < 3 seconds +- Map pan/zoom: 60 FPS (smooth) + +### Memory Usage +- Base app: < 100MB +- With 3 sites calculated: < 200MB +- With 30m terrain loaded (1 region): < 600MB +- With 30m terrain loaded (3 regions): < 1.5GB + +### Storage +- Base app cache: ~15MB +- 30m terrain per region: ~400MB +- User projects: < 1MB each +- Total max storage: ~1.5GB (for 3 regions) + +### Network +- Terrain download speed: depends on connection (30-60s for 400MB on 4G) +- API response time: < 500ms +- OAuth flow: < 2 seconds + +--- + +## Security & Privacy + +### PWA Security +- Service Worker scope limited to origin +- Content Security Policy (CSP) headers +- No eval() or inline scripts +- HTTPS required for production +- Subresource Integrity (SRI) for CDN assets + +### Data Privacy +- All calculations client-side (no data sent to server) +- Terrain data cached locally +- Optional cloud sync requires authentication +- User projects encrypted in IndexedDB (future) +- No analytics or tracking + +### Authentication +- OAuth 2.0 with MAS +- JWT tokens with short expiry (1 hour) +- Refresh token rotation +- No password storage +- Logout clears all tokens + +### Network Security +- TLS 1.3 required +- HSTS enabled +- Certificate pinning (future) +- Rate limiting on API (100 req/hour) +- CORS properly configured + +--- + +## Testing Strategy + +### Unit Tests + +```typescript +// tests/unit/fspl.test.ts +import { describe, it, expect } from 'vitest'; +import { calculateFSPL } from '@/rf/fspl'; + +describe('Universal FSPL Calculator', () => { + it('calculates correct path loss at 1km, 1800MHz', () => { + const fspl = calculateFSPL(1.0, 1800); + expect(fspl).toBeCloseTo(92.5, 1); + }); + + it('calculates correct path loss for VHF (150 MHz)', () => { + const fspl = calculateFSPL(1.0, 150); + expect(fspl).toBeCloseTo(71.0, 1); + }); + + it('lower frequency = less path loss', () => { + const fspl150 = calculateFSPL(5.0, 150); + const fspl1800 = calculateFSPL(5.0, 1800); + expect(fspl150).toBeLessThan(fspl1800); + }); + + it('handles zero distance', () => { + const fspl = calculateFSPL(0, 1800); + expect(fspl).toBe(0); + }); +}); +``` + +### Integration Tests + +```typescript +// tests/integration/calculator.test.ts +import { describe, it, expect, beforeAll } from 'vitest'; +import { RFCalculator } from '@/rf/calculator'; +import { TerrainManager } from '@/terrain/manager'; + +describe('RF Calculator Integration', () => { + let calculator: RFCalculator; + + beforeAll(() => { + const terrain = new TerrainManager(db); + calculator = new RFCalculator(terrain); + }); + + it('calculates coverage for single LTE site', async () => { + const lteSite: Site = { + id: '1', + name: 'Test LTE', + lat: 48.0, + lon: 35.0, + height: 30, + power: 43, + gain: 8, + frequency: 1800, + antennaType: 'omni', + // ... other fields + }; + + const result = await calculator.calculateCoverage( + [lteSite], + testBounds, + { radius: 5, resolution: 100, rsrpThreshold: -120 } + ); + + expect(result.points.length).toBeGreaterThan(0); + expect(result.calculationTime).toBeLessThan(2000); + }); + + it('calculates coverage for custom frequency', async () => { + const customSite: Site = { + // ... LTE site params + frequency: 450, // UHF + }; + + const result = await calculator.calculateCoverage([customSite], bounds, settings); + expect(result.points.length).toBeGreaterThan(0); + }); +}); +``` + +### E2E Tests + +```typescript +// tests/e2e/coverage-flow.spec.ts +import { test, expect } from '@playwright/test'; + +test('complete coverage calculation flow with manual input', async ({ page }) => { + await page.goto('http://localhost:5173'); + await page.waitForSelector('.leaflet-container'); + + // Click on map to place site + await page.click('.leaflet-container', { + position: { x: 400, y: 300 } + }); + + // Fill in manual parameters + await page.fill('input[name="name"]', 'Test Station'); + await page.getByLabel('ะŸะพั‚ัƒะถะฝั–ัั‚ัŒ ะฟะตั€ะตะดะฐะฒะฐั‡ะฐ').fill('43'); + await page.getByLabel('ะšะพะตั„ั–ั†ั–ั”ะฝั‚ ะฐะฝั‚ะตะฝะธ').fill('8'); + + // Select frequency + await page.click('button:has-text("1800")'); + + await page.getByLabel('ะ’ะธัะพั‚ะฐ ะฐะฝั‚ะตะฝะธ').fill('30'); + + // Save site + await page.click('button:has-text("ะ—ะฑะตั€ะตะณั‚ะธ")'); + + // Calculate coverage + await page.click('button:has-text("ะ ะพะทั€ะฐั…ัƒะฒะฐั‚ะธ ะฟะพะบั€ะธั‚ั‚ั")'); + + // Wait for heatmap + await page.waitForSelector('.leaflet-heatmap-layer', { + timeout: 10000 + }); + + // Verify heatmap is visible + const heatmap = await page.locator('.leaflet-heatmap-layer'); + await expect(heatmap).toBeVisible(); +}); +``` + +--- + +## Deployment + +### Prerequisites + +```bash +# VPS requirements +- Ubuntu 22.04 LTS +- 4GB RAM (for terrain processing) +- 30GB disk space (20GB for terrain data) +- Docker + Docker Compose installed +- Domain configured (rfcp.eliah.one) +``` + +### Deployment Steps + +```bash +# 1. Clone repository +git clone https://git.eliah.one/mytec/rfcp.git +cd rfcp + +# 2. Configure environment +cp .env.example .env +nano .env + +# 3. Download and preprocess 30m SRTM terrain data +cd backend +python scripts/download-srtm.py # Downloads raw SRTM +python scripts/preprocess-terrain.py # Converts to optimized format +cd .. + +# 4. Build containers +docker-compose build + +# 5. Start services +docker-compose up -d + +# 6. Check status +docker-compose ps +curl https://rfcp.eliah.one/health +curl https://rfcp.eliah.one/api/terrain/ukraine-east # Should return 400MB file +``` + +### Docker Compose + +```yaml +version: '3.8' + +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + environment: + - NODE_ENV=production + - VITE_API_URL=https://rfcp.eliah.one/api + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + environment: + - ENVIRONMENT=production + - MAS_URL=https://mas.umtc.dev + - DATABASE_URL=sqlite:///data/rfcp.db + volumes: + - ./backend/data:/app/data + - terrain_data:/app/data/terrain + restart: unless-stopped + + caddy: + image: caddy:2-alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + +volumes: + caddy_data: + caddy_config: + terrain_data: +``` + +### Caddyfile + +``` +rfcp.eliah.one { + # Frontend + reverse_proxy frontend:5173 + + # Backend API + reverse_proxy /api/* backend:8000 + + # Security headers + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.openstreetmap.org; worker-src 'self' blob:; connect-src 'self' https://rfcp.eliah.one" + } + + # Cache static assets + @static { + path *.js *.css *.woff2 *.png *.jpg *.svg + } + header @static Cache-Control "public, max-age=31536000" + + # Service Worker - no cache + @sw { + path /service-worker.js + } + header @sw Cache-Control "no-cache" + + # Terrain data - long cache + @terrain { + path /api/terrain/* + } + header @terrain Cache-Control "public, max-age=7776000" # 90 days +} +``` + +--- + +## Success Metrics + +### MVP Success Criteria + +โœ… **Technical:** +- PWA installable on mobile/desktop/tablet +- Works offline after initial load +- Coverage calculation < 3 seconds (3 sites, 5km, 100m) +- Heatmap renders smoothly (< 2s for 5000 points) +- Supports 5+ sites simultaneously +- 30m terrain integration working +- Manual input fully functional + +โœ… **User Experience:** +- Intuitive UI (no tutorial needed for basic tasks) +- Works on 4G tablet +- Terrain data downloads automatically +- Can save and load projects +- Bilingual support (UK/EN) +- Helpful contextual info (band info, wavelength) + +โœ… **Deployment:** +- Live at https://rfcp.eliah.one +- 99% uptime +- Accessible from military networks +- Offline installer available (~200MB + terrain) + +### KPIs + +- **Performance:** 95% of calculations complete in < 3s +- **Adoption:** 20+ active users in first month +- **Reliability:** < 1 critical bug per week post-launch +- **Offline Usage:** 60%+ of sessions offline +- **Terrain Downloads:** Average 2-3 regions per user + +--- + +## Open Questions & Next Steps + +### Questions Answered โœ… + +1. โœ… **Terrain detail:** 30m SRTM (1 arc-second) +2. โœ… **Frequency bands:** Band 3, 7, 20, 2 + dynamic input (any 400-6000 MHz) +3. โœ… **Input method:** Manual input > equipment presets +4. โœ… **Equipment parameters:** Not critical for MVP, add later +5. โœ… **RF measurements:** v1.1 feature (CSV import) +6. โœ… **Languages:** Ukrainian + English only + +### Next Actions + +1. **Initialize Project** โœ… + - [x] Git repo created + - [x] TechSpec finalized + - [ ] Start Phase 1 implementation + +2. **Phase 1 Start** (Tomorrow) + - [ ] Setup React + TypeScript + Vite + - [ ] Configure Tailwind + - [ ] Create manual input UI + - [ ] Integrate Leaflet map + +3. **Ongoing** + - [ ] Weekly progress reviews + - [ ] Adjust roadmap based on feedback + - [ ] Test on real devices (tablets) + +--- + +## Contact & Support + +**Project Lead:** ะžะปะตะณ (mytec) +**Repository:** https://git.eliah.one/mytec/rfcp +**Documentation:** https://git.eliah.one/mytec/rfcp/wiki +**Issues:** https://git.eliah.one/mytec/rfcp/issues +**Deployment:** https://rfcp.eliah.one + +--- + +**Document Version:** 3.0 (Final) +**Last Updated:** 2025-01-30 +**Status:** ๐Ÿš€ Ready for Development +**Next Action:** Phase 1 Implementation - Manual Input UI + +**Changes from v2.0:** +- โœ… 30m SRTM resolution (vs 90m) +- โœ… Manual input UI (vs equipment presets) +- โœ… Dynamic frequency input (vs fixed dropdown) +- โœ… Removed equipment database +- โœ… Added UHF/VHF roadmap (v1.1) +- โœ… Enhanced terrain manager for 30m data +- โœ… Updated UI mockups for manual input +- โœ… Clarified typical deployment (2-3 regions)