# 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 (