# RFCP Phase 2.1: Desktop Application (Windows, Linux, macOS) **Date:** January 31, 2025 **Type:** Packaging & Distribution **Estimated:** 16-20 hours **Priority:** HIGH โ main product delivery --- ## ๐ฏ Goal Package RFCP as standalone desktop application for Windows, Linux, and macOS. Fully offline after initial region download. No VPS, no MongoDB, no internet required. --- ## ๐ Target Specs | Metric | Target | |--------|--------| | Installer size | 200-300 MB | | Installed size | 500MB - 1GB (without map data) | | With Ukraine region | ~3.5 GB | | **Platforms** | **Windows 10/11, Ubuntu 22.04+, macOS 12+** | | Offline | Full offline after region download | | GPU | Optional CUDA acceleration (Win/Linux) | --- ## ๐๏ธ Architecture ``` โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ RFCP Desktop App โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ Electron Shell โ โ โ โ (Chromium + Node.js runtime) โ โ โ โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ React Frontend (bundled) โ โ โ โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ http://localhost:8888 โ โ โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ FastAPI Backend (PyInstaller exe) โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ โ โ Propagation Engine โ โ โ โ โ โ โโโ CPU (NumPy/SciPy) โ โ โ โ โ โ โโโ GPU (CuPy) [optional] โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ โ โ โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ Local Data Store โ โ โ โ โโโ SQLite (projects, settings) โ โ โ โ โโโ SRTM tiles (~25MB each) โ โ โ โ โโโ OSM cache (buildings, water, veg) โ โ โ โ โโโ Map tiles (optional offline maps) โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ``` --- ## ๐ Project Structure ``` rfcp/ โโโ frontend/ # React (existing) โโโ backend/ # FastAPI (existing) โโโ desktop/ # NEW โ Electron app โ โโโ main.js # Electron main process โ โโโ preload.js # Context bridge โ โโโ splash.html # Loading screen โ โโโ package.json # Electron deps + build config โ โโโ assets/ โ โโโ icon.ico # Windows icon (256x256) โ โโโ icon.png # Linux icon (512x512) โ โโโ icon.icns # macOS icon โโโ installer/ # NEW โ Build scripts โ โโโ build-win.sh # Windows build script โ โโโ build-linux.sh # Linux build script โ โโโ build-mac.sh # macOS build script โ โโโ build-all.sh # Build all platforms โ โโโ rfcp-server.spec # PyInstaller spec โโโ docs/ โโโ scripts/ โโโ CLAUDE.md ``` --- ## ๐ Installed Directory Structure ### Windows ``` C:\Program Files\RFCP\ โโโ RFCP.exe # Electron app โโโ resources/ โ โโโ app.asar # Frontend bundle โ โโโ backend/ โ โโโ rfcp-server.exe # PyInstaller bundle โโโ Uninstall RFCP.exe %APPDATA%\RFCP\ # User data โโโ rfcp.db # SQLite database โโโ terrain/ # SRTM tiles โโโ osm/ # OSM cache โโโ projects/ # User projects ``` ### Linux ``` /opt/RFCP/ # AppImage extracts here โโโ rfcp # Main binary โโโ resources/ โโโ ... ~/.local/share/RFCP/ # User data โโโ rfcp.db โโโ terrain/ โโโ osm/ โโโ projects/ ``` ### macOS ``` /Applications/RFCP.app/ # App bundle โโโ Contents/ โโโ MacOS/rfcp # Main binary โโโ Resources/ โ โโโ backend/ โ โโโ frontend/ โโโ Info.plist ~/Library/Application Support/RFCP/ # User data โโโ rfcp.db โโโ terrain/ โโโ osm/ โโโ projects/ ``` --- ## โ Tasks ### Task 2.1.1: Project Setup (1-2 hours) **Create `desktop/package.json`:** ```json { "name": "rfcp-desktop", "version": "1.6.1", "description": "RF Coverage Planner - Tactical Communications", "main": "main.js", "author": "UMTC Project", "license": "MIT", "scripts": { "start": "electron .", "dev": "NODE_ENV=development electron .", "build": "electron-builder", "build:win": "electron-builder --win", "build:linux": "electron-builder --linux", "build:mac": "electron-builder --mac", "build:all": "electron-builder --win --linux --mac" }, "dependencies": { "electron-store": "^8.1.0" }, "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": "assets" }, "files": [ "main.js", "preload.js", "splash.html" ], "extraResources": [ { "from": "../frontend/dist", "to": "frontend" }, { "from": "./backend-dist/${os}", "to": "backend" } ], "win": { "target": ["nsis"], "icon": "assets/icon.ico" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "RFCP" }, "linux": { "target": ["AppImage", "deb"], "icon": "assets/icon.png", "category": "Science" }, "mac": { "target": ["dmg", "zip"], "icon": "assets/icon.icns", "category": "public.app-category.developer-tools", "hardenedRuntime": false, "gatekeeperAssess": false }, "dmg": { "title": "RFCP Installer", "backgroundColor": "#1a1a2e", "contents": [ { "x": 130, "y": 220 }, { "x": 410, "y": 220, "type": "link", "path": "/Applications" } ] } } } ``` --- ### Task 2.1.2: Electron Main Process (3-4 hours) **desktop/main.js:** ```javascript const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { spawn, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const Store = require('electron-store'); const store = new Store(); let mainWindow; let splashWindow; let backendProcess; // Paths const isDev = process.env.NODE_ENV === 'development'; const getResourcePath = (relativePath) => { if (isDev) { return path.join(__dirname, '..', relativePath); } return path.join(process.resourcesPath, relativePath); }; const getDataPath = () => { return path.join(app.getPath('userData'), 'data'); }; // Get backend executable name based on platform const getBackendExeName = () => { switch (process.platform) { case 'win32': return 'rfcp-server.exe'; case 'darwin': return 'rfcp-server'; case 'linux': return 'rfcp-server'; default: return 'rfcp-server'; } }; // Ensure data directories exist function ensureDataDirs() { const dirs = ['terrain', 'osm', 'projects', 'cache']; const dataPath = getDataPath(); dirs.forEach(dir => { const fullPath = path.join(dataPath, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } }); return dataPath; } // Create splash window function createSplashWindow() { splashWindow = new BrowserWindow({ width: 400, height: 300, frame: false, transparent: true, alwaysOnTop: true, resizable: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); splashWindow.loadFile(path.join(__dirname, 'splash.html')); splashWindow.center(); } // Start Python backend async function startBackend() { const dataPath = ensureDataDirs(); const env = { ...process.env, RFCP_DATA_PATH: dataPath, RFCP_DATABASE_URL: `sqlite:///${path.join(dataPath, 'rfcp.db')}`, RFCP_HOST: '127.0.0.1', RFCP_PORT: '8888', }; if (isDev) { // Development: run uvicorn const pythonCmd = process.platform === 'win32' ? 'python' : 'python3'; backendProcess = spawn(pythonCmd, [ '-m', 'uvicorn', 'app.main:app', '--host', '127.0.0.1', '--port', '8888', '--reload' ], { cwd: path.join(__dirname, '..', 'backend'), env, stdio: ['ignore', 'pipe', 'pipe'] }); } else { // Production: run PyInstaller bundle const backendExe = path.join( getResourcePath('backend'), getBackendExeName() ); // Make executable on Unix if (process.platform !== 'win32') { try { fs.chmodSync(backendExe, '755'); } catch (e) { console.log('chmod failed:', e); } } backendProcess = spawn(backendExe, [], { env, stdio: ['ignore', 'pipe', 'pipe'] }); } // Log backend output backendProcess.stdout?.on('data', (data) => { console.log(`[Backend] ${data}`); }); backendProcess.stderr?.on('data', (data) => { console.error(`[Backend Error] ${data}`); }); backendProcess.on('error', (err) => { console.error('Failed to start backend:', err); dialog.showErrorBox('Backend Error', `Failed to start backend: ${err.message}`); }); // Wait for backend to be ready return new Promise((resolve) => { const maxAttempts = 60; // 30 seconds let attempts = 0; const checkBackend = setInterval(async () => { attempts++; try { const response = await fetch('http://127.0.0.1:8888/api/health/'); if (response.ok) { clearInterval(checkBackend); console.log('Backend ready!'); resolve(true); } } catch (e) { // Not ready yet } if (attempts >= maxAttempts) { clearInterval(checkBackend); console.error('Backend failed to start in time'); resolve(false); } }, 500); }); } // Create main window function createMainWindow() { const windowState = store.get('windowState', { width: 1400, height: 900 }); mainWindow = new BrowserWindow({ width: windowState.width, height: windowState.height, x: windowState.x, y: windowState.y, minWidth: 1024, minHeight: 768, show: false, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') }, icon: path.join(__dirname, 'assets', process.platform === 'win32' ? 'icon.ico' : 'icon.png' ), title: 'RFCP - RF Coverage Planner', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', }); // Save window state on close mainWindow.on('close', () => { const bounds = mainWindow.getBounds(); store.set('windowState', bounds); }); // Load frontend if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(getResourcePath('frontend'), 'index.html')); } mainWindow.once('ready-to-show', () => { if (splashWindow) { splashWindow.close(); splashWindow = null; } mainWindow.show(); }); mainWindow.on('closed', () => { mainWindow = null; }); // Handle external links mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); } // App lifecycle app.whenReady().then(async () => { createSplashWindow(); const backendStarted = await startBackend(); if (!backendStarted) { const result = await dialog.showMessageBox({ type: 'error', title: 'Startup Error', message: 'Failed to start backend server.', detail: 'Please check logs and try again. Click "Show Logs" to open the log folder.', buttons: ['Quit', 'Show Logs'] }); if (result.response === 1) { shell.openPath(app.getPath('logs')); } app.quit(); return; } createMainWindow(); }); app.on('window-all-closed', () => { if (backendProcess) { backendProcess.kill(); backendProcess = null; } if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createMainWindow(); } }); app.on('before-quit', () => { if (backendProcess) { backendProcess.kill(); } }); // IPC Handlers ipcMain.handle('get-data-path', () => getDataPath()); ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-platform', () => process.platform); ipcMain.handle('get-gpu-info', async () => { // TODO: Implement GPU detection return { available: false, name: null, cudaVersion: null }; }); ipcMain.handle('select-directory', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] }); return result.filePaths[0] || null; }); ipcMain.handle('select-file', async (event, options) => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: options?.filters || [] }); return result.filePaths[0] || null; }); ipcMain.handle('save-file', async (event, options) => { const result = await dialog.showSaveDialog(mainWindow, { defaultPath: options?.defaultPath, filters: options?.filters || [] }); return result.filePath || null; }); ipcMain.handle('get-setting', (event, key) => store.get(key)); ipcMain.handle('set-setting', (event, key, value) => store.set(key, value)); ipcMain.handle('open-external', (event, url) => shell.openExternal(url)); ipcMain.handle('open-path', (event, path) => shell.openPath(path)); ``` --- ### Task 2.1.3: Preload Script (1 hour) **desktop/preload.js:** ```javascript const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('rfcp', { // System info getDataPath: () => ipcRenderer.invoke('get-data-path'), getAppVersion: () => ipcRenderer.invoke('get-app-version'), getPlatform: () => ipcRenderer.invoke('get-platform'), getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'), // Dialogs selectDirectory: () => ipcRenderer.invoke('select-directory'), selectFile: (options) => ipcRenderer.invoke('select-file', options), saveFile: (options) => ipcRenderer.invoke('save-file', options), // Settings (persistent) getSetting: (key) => ipcRenderer.invoke('get-setting', key), setSetting: (key, value) => ipcRenderer.invoke('set-setting', key, value), // Shell openExternal: (url) => ipcRenderer.invoke('open-external', url), openPath: (path) => ipcRenderer.invoke('open-path', path), // Platform info platform: process.platform, isDesktop: true, isMac: process.platform === 'darwin', isWindows: process.platform === 'win32', isLinux: process.platform === 'linux', }); ``` --- ### Task 2.1.4: Splash Screen (30 min) **desktop/splash.html:** ```html