@mytec: iter2.1 ready for testing

This commit is contained in:
2026-01-31 14:26:11 +02:00
parent cdbf0127bf
commit fa55fec94a
11 changed files with 780 additions and 1 deletions

314
desktop/main.js Normal file
View File

@@ -0,0 +1,314 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const { spawn } = 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, filePath) => shell.openPath(filePath));

79
desktop/package.json Normal file
View File

@@ -0,0 +1,79 @@
{
"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" }
]
}
}
}

29
desktop/preload.js Normal file
View File

@@ -0,0 +1,29 @@
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',
});

82
desktop/splash.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>RFCP Loading</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
-webkit-app-region: drag;
user-select: none;
border-radius: 12px;
}
.logo {
font-size: 48px;
font-weight: bold;
margin-bottom: 8px;
background: linear-gradient(90deg, #00d4ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: #888;
margin-bottom: 30px;
}
.loader {
width: 200px;
height: 4px;
background: #333;
border-radius: 2px;
overflow: hidden;
}
.loader-bar {
height: 100%;
width: 30%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
border-radius: 2px;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.status {
margin-top: 15px;
font-size: 12px;
color: #666;
}
.version {
position: absolute;
bottom: 15px;
font-size: 11px;
color: #444;
}
</style>
</head>
<body>
<div class="logo">RFCP</div>
<div class="subtitle">RF Coverage Planner</div>
<div class="loader">
<div class="loader-bar"></div>
</div>
<div class="status">Starting backend server...</div>
<div class="version">v1.6.1</div>
</body>
</html>