# RFCP Phase 2.1: Desktop Application & Installer **Date:** January 31, 2025 **Type:** Packaging & Distribution **Estimated:** 20-30 hours **Priority:** After frontend-backend integration (1.5) --- ## 🎯 Goal Package RFCP as standalone desktop application with installer for Windows and Linux. Fully offline capable after initial setup. --- ## πŸ“Š Target Specs | Metric | Target | |--------|--------| | Installer size | 200-300 MB | | Installed size | 500MB - 1GB | | Platforms | Windows 10/11, Ubuntu 22.04+ | | Offline | Full offline after region download | | GPU | Optional, configurable in settings | --- ## πŸ—οΈ Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RFCP Desktop App β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Electron Shell β”‚ β”‚ β”‚ β”‚ (Chromium + Node.js runtime) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ React Frontend (UI) β”‚ β”‚ β”‚ β”‚ localhost:5173 (dev) / bundled β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ HTTP API β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ FastAPI Backend (Python) β”‚ β”‚ β”‚ β”‚ localhost:8888 β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ Propagation Engine β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CPU (default) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── GPU (optional/CUDA) β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Local Data Store β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SQLite (projects, settings) β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SRTM tiles (elevation) β”‚ β”‚ β”‚ β”‚ └── OSM cache (buildings, roads) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## πŸ“ Directory Structure ### Development ``` rfcp/ β”œβ”€β”€ electron/ β”‚ β”œβ”€β”€ main.js # Electron main process β”‚ β”œβ”€β”€ preload.js # Context bridge β”‚ β”œβ”€β”€ package.json # Electron deps β”‚ └── build/ # electron-builder config β”‚ β”œβ”€β”€ icon.ico β”‚ β”œβ”€β”€ icon.png β”‚ └── installer.nsh # NSIS customization β”œβ”€β”€ frontend/ # React (existing) β”œβ”€β”€ backend/ # FastAPI (existing) └── scripts/ β”œβ”€β”€ build-windows.sh β”œβ”€β”€ build-linux.sh └── package-python.sh ``` ### Installed (Windows) ``` C:\Program Files\RFCP\ β”œβ”€β”€ RFCP.exe # Electron app β”œβ”€β”€ resources/ β”‚ β”œβ”€β”€ app.asar # Frontend bundle β”‚ └── backend/ β”‚ β”œβ”€β”€ rfcp-server.exe # PyInstaller bundle β”‚ └── app/ # Python code β”œβ”€β”€ data/ β”‚ β”œβ”€β”€ rfcp.db # SQLite database β”‚ β”œβ”€β”€ srtm/ # Elevation tiles β”‚ β”œβ”€β”€ osm/ # OSM cache β”‚ └── projects/ # User projects β”œβ”€β”€ python/ # Embedded Python (if not PyInstaller) └── Uninstall RFCP.exe ``` ### Installed (Linux) ``` /opt/rfcp/ β”œβ”€β”€ rfcp # AppImage or binary β”œβ”€β”€ resources/ β”‚ └── ... └── data/ └── ... ~/.local/share/rfcp/ # User data β”œβ”€β”€ rfcp.db β”œβ”€β”€ srtm/ β”œβ”€β”€ osm/ └── projects/ ``` --- ## βœ… Tasks ### Task 2.1.1: Electron Shell (4-6 hours) **electron/main.js:** ```javascript const { app, BrowserWindow, ipcMain } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); let mainWindow; let backendProcess; // Paths const isDev = process.env.NODE_ENV === 'development'; const backendPath = isDev ? path.join(__dirname, '../backend') : path.join(process.resourcesPath, 'backend'); const dataPath = isDev ? path.join(__dirname, '../data') : path.join(app.getPath('userData'), 'data'); // Ensure data directories exist function ensureDataDirs() { const dirs = ['srtm', 'osm', 'projects']; dirs.forEach(dir => { const fullPath = path.join(dataPath, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } }); } // Start Python backend function startBackend() { const pythonExe = isDev ? 'python' : path.join(process.resourcesPath, 'backend', 'rfcp-server.exe'); const env = { ...process.env, RFCP_DATA_PATH: dataPath, RFCP_PORT: '8888' }; if (isDev) { backendProcess = spawn(pythonExe, ['-m', 'uvicorn', 'app.main:app', '--port', '8888'], { cwd: backendPath, env }); } else { backendProcess = spawn(pythonExe, [], { env }); } backendProcess.stdout.on('data', (data) => { console.log(`Backend: ${data}`); }); backendProcess.stderr.on('data', (data) => { console.error(`Backend Error: ${data}`); }); return new Promise((resolve) => { // Wait for backend to be ready const checkBackend = setInterval(async () => { try { const response = await fetch('http://localhost:8888/api/health/'); if (response.ok) { clearInterval(checkBackend); resolve(); } } catch (e) { // Not ready yet } }, 500); // Timeout after 30s setTimeout(() => { clearInterval(checkBackend); resolve(); // Continue anyway }, 30000); }); } // Create window async function createWindow() { ensureDataDirs(); // Show splash while loading const splash = new BrowserWindow({ width: 400, height: 300, frame: false, alwaysOnTop: true, transparent: true }); splash.loadFile('splash.html'); // Start backend await startBackend(); // Create main window mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 1024, minHeight: 768, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') }, icon: path.join(__dirname, 'build/icon.png'), title: 'RFCP - RF Coverage Planner' }); // Load frontend if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(process.resourcesPath, 'frontend', 'index.html')); } // Close splash splash.close(); mainWindow.on('closed', () => { mainWindow = null; }); } // App lifecycle app.whenReady().then(createWindow); app.on('window-all-closed', () => { if (backendProcess) { backendProcess.kill(); } if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); // IPC handlers ipcMain.handle('get-data-path', () => dataPath); ipcMain.handle('get-gpu-info', () => { // Detect GPU availability // Could use node-gpu or check CUDA return { available: false, // TODO: implement detection name: null }; }); ``` **electron/preload.js:** ```javascript const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('rfcp', { getDataPath: () => ipcRenderer.invoke('get-data-path'), getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'), platform: process.platform, version: require('./package.json').version }); ``` --- ### Task 2.1.2: Python Packaging (4-6 hours) **PyInstaller spec file (rfcp-server.spec):** ```python # -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis( ['app/main.py'], pathex=[], binaries=[], datas=[ ('app', 'app'), ], hiddenimports=[ 'uvicorn.logging', 'uvicorn.protocols.http', 'uvicorn.protocols.http.auto', 'uvicorn.protocols.websockets', 'uvicorn.protocols.websockets.auto', 'uvicorn.lifespan.on', 'uvicorn.lifespan.off', ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='rfcp-server', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=True, # False for production disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon='../electron/build/icon.ico' ) ``` **Build script (scripts/package-python.sh):** ```bash #!/bin/bash set -e cd backend # Create venv for packaging python -m venv build_env source build_env/bin/activate # or build_env\Scripts\activate on Windows # Install deps pip install -r requirements.txt pip install pyinstaller # Build pyinstaller rfcp-server.spec --clean # Output in dist/rfcp-server.exe echo "Built: dist/rfcp-server.exe" ``` --- ### Task 2.1.3: Electron Builder Config (3-4 hours) **electron/package.json:** ```json { "name": "rfcp", "version": "1.0.0", "description": "RF Coverage Planner for Tactical Communications", "main": "main.js", "author": "UMTC Project", "license": "MIT", "scripts": { "start": "electron .", "build:win": "electron-builder --win", "build:linux": "electron-builder --linux", "build:all": "electron-builder --win --linux" }, "devDependencies": { "electron": "^28.0.0", "electron-builder": "^24.9.0" }, "build": { "appId": "one.eliah.rfcp", "productName": "RFCP", "copyright": "Copyright Β© 2025 UMTC Project", "directories": { "output": "dist", "buildResources": "build" }, "files": [ "main.js", "preload.js", "splash.html" ], "extraResources": [ { "from": "../frontend/dist", "to": "frontend" }, { "from": "../backend/dist/rfcp-server", "to": "backend" } ], "win": { "target": [ { "target": "nsis", "arch": ["x64"] } ], "icon": "build/icon.ico" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "installerIcon": "build/icon.ico", "uninstallerIcon": "build/icon.ico", "installerHeaderIcon": "build/icon.ico", "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "RFCP" }, "linux": { "target": [ "AppImage", "deb" ], "icon": "build/icon.png", "category": "Science" } } } ``` --- ### Task 2.1.4: First Run & Region Selection (4-6 hours) **First run wizard:** ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Welcome to RFCP β”‚ β”‚ RF Coverage Planner β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ Select your region for offline maps: β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [x] Ukraine (~2.5 GB) β”‚ β”‚ β”‚ β”‚ [ ] Poland (~1.8 GB) β”‚ β”‚ β”‚ β”‚ [ ] Germany (~2.1 GB) β”‚ β”‚ β”‚ β”‚ [ ] Custom bounding box... β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Data includes: β”‚ β”‚ β€’ Terrain elevation (SRTM 30m) β”‚ β”‚ β€’ Building footprints (OSM) β”‚ β”‚ β€’ Road network (OSM) β”‚ β”‚ β€’ Base map tiles β”‚ β”‚ β”‚ β”‚ [ ] Download now β”‚ β”‚ [ ] Download later (online mode) β”‚ β”‚ β”‚ β”‚ [Continue β†’] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Region download API endpoint:** ```python @router.post("/regions/download") async def download_region(region: str, background_tasks: BackgroundTasks): """Start region download in background""" REGIONS = { "ukraine": { "bbox": [44.0, 22.0, 52.5, 40.5], # S, W, N, E "srtm_tiles": ["N44E022", "N44E023", ...], # ~120 tiles "estimated_size": 2.5 * 1024 * 1024 * 1024 # 2.5 GB }, # ... } if region not in REGIONS: raise HTTPException(400, "Unknown region") task_id = str(uuid4()) background_tasks.add_task(download_region_data, task_id, REGIONS[region]) return {"task_id": task_id, "status": "started"} @router.get("/regions/progress/{task_id}") async def get_download_progress(task_id: str): """Get download progress""" return { "task_id": task_id, "status": "downloading", # started, downloading, extracting, done, error "progress": 45.5, # percentage "current_file": "N48E035.hgt", "downloaded_mb": 1250, "total_mb": 2500 } ``` --- ### Task 2.1.5: Settings - GPU Configuration (2-3 hours) **Settings panel addition:** ```typescript // frontend/src/components/settings/PerformanceSettings.tsx interface PerformanceSettingsProps { gpuInfo: { available: boolean; name: string | null; memory: number | null; }; } export function PerformanceSettings({ gpuInfo }: PerformanceSettingsProps) { const [useGpu, setUseGpu] = useState(false); const [maxWorkers, setMaxWorkers] = useState(4); return (

Performance

{gpuInfo.available ? ( Detected: {gpuInfo.name} ) : ( No compatible GPU detected )}
setMaxWorkers(Number(e.target.value))} /> {maxWorkers} threads
); } ``` **Backend GPU detection:** ```python # app/services/gpu_service.py def detect_gpu() -> dict: """Detect available GPU for CUDA""" result = { "available": False, "name": None, "memory": None, "cuda_version": None } try: import cupy as cp device = cp.cuda.Device(0) props = cp.cuda.runtime.getDeviceProperties(0) result["available"] = True result["name"] = props["name"].decode() result["memory"] = props["totalGlobalMem"] // (1024**3) # GB result["cuda_version"] = cp.cuda.runtime.runtimeGetVersion() except ImportError: pass # CuPy not installed except Exception as e: print(f"GPU detection error: {e}") return result ``` --- ### Task 2.1.6: Build & Test (3-4 hours) **Build script (scripts/build-windows.sh):** ```bash #!/bin/bash set -e echo "=== RFCP Windows Build ===" # 1. Build frontend echo "Building frontend..." cd frontend npm run build cd .. # 2. Build backend echo "Building backend..." cd backend python -m venv build_env source build_env/Scripts/activate pip install -r requirements.txt pip install pyinstaller pyinstaller rfcp-server.spec --clean cd .. # 3. Build Electron echo "Building Electron app..." cd electron npm install npm run build:win cd .. echo "=== Build complete ===" echo "Installer: electron/dist/RFCP Setup*.exe" ``` **Test checklist:** ```markdown ## Install Test - [ ] Installer runs without admin (user install) - [ ] Installer runs with admin (program files) - [ ] Desktop shortcut created - [ ] Start menu entry created - [ ] Uninstaller works ## First Run - [ ] Splash screen appears - [ ] Backend starts successfully - [ ] Main window loads - [ ] Region selection dialog shows - [ ] Can skip region download ## Functionality - [ ] Map loads (online tiles) - [ ] Can create sites - [ ] Coverage calculation works - [ ] All presets work - [ ] Settings persist ## Offline - [ ] Works without internet (after region download) - [ ] Offline tiles load - [ ] Terrain data works - [ ] Buildings/roads cached ## Performance - [ ] GPU toggle appears (if GPU present) - [ ] GPU acceleration works - [ ] Memory usage reasonable (<1GB idle) ``` --- ## πŸ“¦ Deliverables 1. **RFCP-Setup-1.0.0.exe** β€” Windows installer (~200MB) 2. **RFCP-1.0.0.AppImage** β€” Linux portable (~180MB) 3. **RFCP-1.0.0.deb** β€” Debian package (~180MB) 4. **SHA256SUMS** β€” Checksums file 5. **README.md** β€” Installation instructions --- ## πŸ”œ Future Enhancements (2.2+) - [ ] Auto-updater (electron-updater) - [ ] macOS support (.dmg) - [ ] Portable mode (no install, run from USB) - [ ] Silent install for deployment - [ ] MSI installer for enterprise - [ ] Code signing (Windows/macOS) --- ## πŸ“ Notes - SQLite Π·Π°ΠΌΡ–ΡΡ‚ΡŒ MongoDB для локального збСрігання (ΠΏΡ€ΠΎΡΡ‚Ρ–ΡˆΠ΅, Π½Π΅ ΠΏΠΎΡ‚Ρ€Π΅Π±ΡƒΡ” сСрвСра) - SRTM tiles ~25MB ΠΊΠΎΠΆΠ΅Π½, Π£ΠΊΡ€Π°Ρ—Π½Π° ΠΏΠΎΡ‚Ρ€Π΅Π±ΡƒΡ” ~120 tiles = ~3GB - OSM Π΄Π°Π½Ρ– ΠΌΠΎΠΆΠ½Π° Π·Π°Π²Π°Π½Ρ‚Π°ΠΆΠΈΡ‚ΠΈ Π· Geofabrik (ukraine-latest.osm.pbf ~1.5GB) - Map tiles ΠΌΠΎΠΆΠ½Π° ΠΊΠ΅ΡˆΡƒΠ²Π°Ρ‚ΠΈ Π· OpenStreetMap Π°Π±ΠΎ використати MBTiles --- **Ready for implementation after 1.5** πŸš€