# 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 RFCP Loading
RF Coverage Planner
Starting backend server...
v1.6.1
``` --- ### Task 2.1.5: PyInstaller Backend Bundle (3-4 hours) **backend/run_server.py (new entry point):** ```python """Entry point for PyInstaller bundle""" import os import sys # Set base path for PyInstaller if getattr(sys, 'frozen', False): # Running as compiled os.chdir(os.path.dirname(sys.executable)) import uvicorn from app.main import app if __name__ == '__main__': host = os.environ.get('RFCP_HOST', '127.0.0.1') port = int(os.environ.get('RFCP_PORT', '8888')) uvicorn.run( app, host=host, port=port, log_level='info', ) ``` **installer/rfcp-server.spec:** ```python # -*- mode: python ; coding: utf-8 -*- import sys from pathlib import Path block_cipher = None backend_path = Path('../backend') a = Analysis( [str(backend_path / 'run_server.py')], pathex=[str(backend_path)], binaries=[], datas=[ (str(backend_path / 'app'), 'app'), ], hiddenimports=[ 'uvicorn.logging', 'uvicorn.protocols.http', 'uvicorn.protocols.http.auto', 'uvicorn.protocols.http.h11_impl', 'uvicorn.protocols.websockets', 'uvicorn.protocols.websockets.auto', 'uvicorn.lifespan', 'uvicorn.lifespan.on', 'uvicorn.lifespan.off', 'httpx', 'h11', 'numpy', 'scipy', 'scipy.special', 'scipy.interpolate', 'aiosqlite', 'sqlalchemy', 'sqlalchemy.dialects.sqlite', ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=['tkinter', 'matplotlib', 'PIL', 'pandas', 'IPython'], 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=False, # No console window disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon='../desktop/assets/icon.ico' if sys.platform == 'win32' else None, ) ``` --- ### Task 2.1.6: Build Scripts (2-3 hours) **installer/build-win.sh:** ```bash #!/bin/bash set -e echo "=========================================" echo " RFCP Desktop Build (Windows)" echo "=========================================" cd "$(dirname "$0")/.." # 1. Build frontend echo "[1/4] Building frontend..." cd frontend npm ci npm run build cd .. # 2. Build backend with PyInstaller echo "[2/4] Building backend..." cd backend python -m pip install -r requirements.txt python -m pip install pyinstaller cd ../installer python -m PyInstaller rfcp-server.spec --clean --noconfirm mkdir -p ../desktop/backend-dist/win32 cp dist/rfcp-server.exe ../desktop/backend-dist/win32/ cd .. # 3. Build Electron app echo "[3/4] Building Electron app..." cd desktop npm ci npm run build:win cd .. # 4. Done echo "[4/4] Build complete!" echo "" echo "Output: desktop/dist/" ls -la desktop/dist/*.exe 2>/dev/null || echo "Build artifacts in desktop/dist/" ``` **installer/build-linux.sh:** ```bash #!/bin/bash set -e echo "=========================================" echo " RFCP Desktop Build (Linux)" echo "=========================================" cd "$(dirname "$0")/.." # 1. Build frontend echo "[1/4] Building frontend..." cd frontend npm ci npm run build cd .. # 2. Build backend with PyInstaller echo "[2/4] Building backend..." cd backend python3 -m pip install -r requirements.txt python3 -m pip install pyinstaller cd ../installer python3 -m PyInstaller rfcp-server.spec --clean --noconfirm mkdir -p ../desktop/backend-dist/linux cp dist/rfcp-server ../desktop/backend-dist/linux/ cd .. # 3. Build Electron app echo "[3/4] Building Electron app..." cd desktop npm ci npm run build:linux cd .. # 4. Done echo "[4/4] Build complete!" ls -la desktop/dist/*.AppImage 2>/dev/null || echo "AppImage in desktop/dist/" ls -la desktop/dist/*.deb 2>/dev/null || echo "Deb in desktop/dist/" ``` **installer/build-mac.sh:** ```bash #!/bin/bash set -e echo "=========================================" echo " RFCP Desktop Build (macOS)" echo "=========================================" cd "$(dirname "$0")/.." # Check if running on macOS if [[ "$(uname)" != "Darwin" ]]; then echo "Error: This script must run on macOS" exit 1 fi # 1. Build frontend echo "[1/4] Building frontend..." cd frontend npm ci npm run build cd .. # 2. Build backend with PyInstaller echo "[2/4] Building backend..." cd backend python3 -m pip install -r requirements.txt python3 -m pip install pyinstaller cd ../installer python3 -m PyInstaller rfcp-server.spec --clean --noconfirm mkdir -p ../desktop/backend-dist/darwin cp dist/rfcp-server ../desktop/backend-dist/darwin/ cd .. # 3. Create .icns icon if not exists if [ ! -f desktop/assets/icon.icns ]; then echo "Creating macOS icon..." mkdir -p icon.iconset sips -z 16 16 desktop/assets/icon.png --out icon.iconset/icon_16x16.png sips -z 32 32 desktop/assets/icon.png --out icon.iconset/icon_16x16@2x.png sips -z 32 32 desktop/assets/icon.png --out icon.iconset/icon_32x32.png sips -z 64 64 desktop/assets/icon.png --out icon.iconset/icon_32x32@2x.png sips -z 128 128 desktop/assets/icon.png --out icon.iconset/icon_128x128.png sips -z 256 256 desktop/assets/icon.png --out icon.iconset/icon_128x128@2x.png sips -z 256 256 desktop/assets/icon.png --out icon.iconset/icon_256x256.png sips -z 512 512 desktop/assets/icon.png --out icon.iconset/icon_256x256@2x.png sips -z 512 512 desktop/assets/icon.png --out icon.iconset/icon_512x512.png sips -z 1024 1024 desktop/assets/icon.png --out icon.iconset/icon_512x512@2x.png iconutil -c icns icon.iconset -o desktop/assets/icon.icns rm -rf icon.iconset fi # 4. Build Electron app echo "[3/4] Building Electron app..." cd desktop npm ci npm run build:mac cd .. # 5. Done echo "[4/4] Build complete!" ls -la desktop/dist/*.dmg 2>/dev/null || echo "DMG in desktop/dist/" ``` --- ### Task 2.1.7: Frontend Desktop Detection (1 hour) **frontend/src/lib/desktop.ts:** ```typescript interface RFCPDesktop { getDataPath: () => Promise; getAppVersion: () => Promise; getPlatform: () => Promise; getGpuInfo: () => Promise<{ available: boolean; name: string | null; cudaVersion: string | null; }>; selectDirectory: () => Promise; selectFile: (options?: { filters?: Array<{ name: string; extensions: string[] }> }) => Promise; saveFile: (options?: { defaultPath?: string; filters?: Array<{ name: string; extensions: string[] }> }) => Promise; getSetting: (key: string) => Promise; setSetting: (key: string, value: any) => Promise; openExternal: (url: string) => Promise; openPath: (path: string) => Promise; platform: string; isDesktop: boolean; isMac: boolean; isWindows: boolean; isLinux: boolean; } declare global { interface Window { rfcp?: RFCPDesktop; } } export const isDesktop = (): boolean => { return window.rfcp?.isDesktop === true; }; export const getDesktopApi = (): RFCPDesktop | null => { return window.rfcp || null; }; export const getApiBaseUrl = (): string => { if (isDesktop()) { return 'http://127.0.0.1:8888'; } return import.meta.env.VITE_API_URL || 'https://api.rfcp.eliah.one'; }; export const isMac = (): boolean => { return window.rfcp?.isMac === true; }; ``` **Update frontend/src/services/api.ts:** ```typescript import { getApiBaseUrl } from '../lib/desktop'; const API_BASE = getApiBaseUrl(); // ... rest remains same ``` --- ## ๐Ÿงช Testing ### Development Testing ```bash # Terminal 1: Backend cd backend uvicorn app.main:app --host 127.0.0.1 --port 8888 --reload # Terminal 2: Frontend cd frontend npm run dev # Terminal 3: Electron cd desktop npm run dev ``` ### Build Testing **Windows:** ```bash cd installer ./build-win.sh # Test: desktop/dist/RFCP Setup 1.6.1.exe ``` **Linux:** ```bash cd installer ./build-linux.sh # Test: desktop/dist/RFCP-1.6.1.AppImage ``` **macOS (run on Mac):** ```bash cd installer ./build-mac.sh # Test: desktop/dist/RFCP-1.6.1.dmg ``` ### Checklist - [ ] Installer runs without errors - [ ] Backend starts automatically (check splash โ†’ main window) - [ ] Frontend loads in Electron window - [ ] Coverage calculation works - [ ] Data persists between sessions (SQLite) - [ ] Window state persists (size, position) - [ ] File dialogs work (import/export) - [ ] External links open in browser --- ## ๐Ÿ“ฆ Deliverables | Platform | File | Size | |----------|------|------| | Windows | `RFCP-Setup-1.6.1.exe` | ~250MB | | Linux | `RFCP-1.6.1.AppImage` | ~200MB | | Linux | `RFCP-1.6.1.deb` | ~200MB | | macOS | `RFCP-1.6.1.dmg` | ~220MB | | macOS | `RFCP-1.6.1-mac.zip` | ~210MB | --- ## ๐Ÿ”œ Future (2.2+) - [ ] First-run region download wizard - [ ] GPU acceleration toggle in settings - [ ] Auto-updater (electron-updater) - [ ] Portable mode (no install, run from USB) - [ ] Code signing (Windows/macOS) - [ ] Notarization (macOS) --- ## ๐Ÿ“ Notes - **macOS signing:** Without Apple Developer account ($99/yr), users see "unverified developer" warning. They can bypass via System Preferences โ†’ Security. - **SQLite** replaces MongoDB โ€” simpler, no server needed - **SRTM tiles** ~25MB each, Ukraine needs ~120 tiles = ~3GB - **Icons:** Need 512x512 PNG source, scripts generate .ico and .icns --- **Ready for Claude Code** ๐Ÿš€