diff --git a/RFCP-Phase-2.1-Desktop-Complete.md b/RFCP-Phase-2.1-Desktop-Complete.md new file mode 100644 index 0000000..cddf9f4 --- /dev/null +++ b/RFCP-Phase-2.1-Desktop-Complete.md @@ -0,0 +1,1085 @@ +# 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** πŸš€ diff --git a/RFCP-Phase-2.1-Desktop-Installer.md b/RFCP-Phase-2.1-Desktop-Installer.md deleted file mode 100644 index 2f9f546..0000000 --- a/RFCP-Phase-2.1-Desktop-Installer.md +++ /dev/null @@ -1,730 +0,0 @@ -# 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** πŸš€ diff --git a/backend/requirements.txt b/backend/requirements.txt index fdd26f9..ec6db3d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,3 +9,5 @@ numpy==1.26.4 scipy==1.12.0 requests==2.31.0 httpx==0.27.0 +aiosqlite>=0.19.0 +sqlalchemy>=2.0.0 diff --git a/desktop/assets/icon-256.png b/desktop/assets/icon-256.png new file mode 100644 index 0000000..1e13030 Binary files /dev/null and b/desktop/assets/icon-256.png differ diff --git a/desktop/assets/icon-original.png b/desktop/assets/icon-original.png new file mode 100644 index 0000000..e7cbfba Binary files /dev/null and b/desktop/assets/icon-original.png differ diff --git a/desktop/assets/icon.ico b/desktop/assets/icon.ico new file mode 100644 index 0000000..7eb140c Binary files /dev/null and b/desktop/assets/icon.ico differ diff --git a/desktop/assets/icon.png b/desktop/assets/icon.png new file mode 100644 index 0000000..d0c62e6 Binary files /dev/null and b/desktop/assets/icon.png differ diff --git a/RFCP-Backend-Roadmap-Complete.md b/docs/devlog/back/RFCP-Backend-Roadmap-Complete.md similarity index 100% rename from RFCP-Backend-Roadmap-Complete.md rename to docs/devlog/back/RFCP-Backend-Roadmap-Complete.md diff --git a/RFCP-Iteration-1.1-Backend-Foundation.md b/docs/devlog/back/RFCP-Iteration-1.1-Backend-Foundation.md similarity index 100% rename from RFCP-Iteration-1.1-Backend-Foundation.md rename to docs/devlog/back/RFCP-Iteration-1.1-Backend-Foundation.md diff --git a/RFCP-Iteration-1.1.1-UX-Safety-Undo-Redo.md b/docs/devlog/back/RFCP-Iteration-1.1.1-UX-Safety-Undo-Redo.md similarity index 100% rename from RFCP-Iteration-1.1.1-UX-Safety-Undo-Redo.md rename to docs/devlog/back/RFCP-Iteration-1.1.1-UX-Safety-Undo-Redo.md diff --git a/RFCP-Iteration-1.2-Terrain-Integration.md b/docs/devlog/back/RFCP-Iteration-1.2-Terrain-Integration.md similarity index 100% rename from RFCP-Iteration-1.2-Terrain-Integration.md rename to docs/devlog/back/RFCP-Iteration-1.2-Terrain-Integration.md diff --git a/RFCP-Iteration-1.3-Coverage-OSM-Buildings.md b/docs/devlog/back/RFCP-Iteration-1.3-Coverage-OSM-Buildings.md similarity index 100% rename from RFCP-Iteration-1.3-Coverage-OSM-Buildings.md rename to docs/devlog/back/RFCP-Iteration-1.3-Coverage-OSM-Buildings.md diff --git a/RFCP-Iteration-1.4-Advanced-Propagation.md b/docs/devlog/back/RFCP-Iteration-1.4-Advanced-Propagation.md similarity index 100% rename from RFCP-Iteration-1.4-Advanced-Propagation.md rename to docs/devlog/back/RFCP-Iteration-1.4-Advanced-Propagation.md diff --git a/RFCP-Iteration-1.5-Frontend-Backend-Integration.md b/docs/devlog/back/RFCP-Iteration-1.5-Frontend-Backend-Integration.md similarity index 100% rename from RFCP-Iteration-1.5-Frontend-Backend-Integration.md rename to docs/devlog/back/RFCP-Iteration-1.5-Frontend-Backend-Integration.md diff --git a/RFCP-Iteration-1.5.1-Fixes-Boundaries.md b/docs/devlog/back/RFCP-Iteration-1.5.1-Fixes-Boundaries.md similarity index 100% rename from RFCP-Iteration-1.5.1-Fixes-Boundaries.md rename to docs/devlog/back/RFCP-Iteration-1.5.1-Fixes-Boundaries.md diff --git a/RFCP-Iteration-1.6-Enhanced-Environment.md b/docs/devlog/back/RFCP-Iteration-1.6-Enhanced-Environment.md similarity index 100% rename from RFCP-Iteration-1.6-Enhanced-Environment.md rename to docs/devlog/back/RFCP-Iteration-1.6-Enhanced-Environment.md diff --git a/RFCP-Iteration-1.6.1-Extra-Factors.md b/docs/devlog/back/RFCP-Iteration-1.6.1-Extra-Factors.md similarity index 100% rename from RFCP-Iteration-1.6.1-Extra-Factors.md rename to docs/devlog/back/RFCP-Iteration-1.6.1-Extra-Factors.md diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..fda259f Binary files /dev/null and b/icon.png differ