# 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<DBTerrainTile, string>;
  projects!: Table<DBProjectConfig, string>;

  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<string, TerrainTile>();
  private downloading = new Map<string, Promise<TerrainTile>>();
  
  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<number> {
    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<void> {
    // 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<TerrainTile> {
    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<number[]> {
    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<Record<UkraineRegion, boolean>> {
    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<number> {
    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<void> {
    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<CoverageResult> {
    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<T>(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<CoveragePoint[]> {
    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)
