@mytec: iter2.1 ready for testing
This commit is contained in:
22
backend/run_server.py
Normal file
22
backend/run_server.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""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',
|
||||||
|
)
|
||||||
314
desktop/main.js
Normal file
314
desktop/main.js
Normal 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
79
desktop/package.json
Normal 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
29
desktop/preload.js
Normal 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
82
desktop/splash.html
Normal 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>
|
||||||
47
frontend/src/lib/desktop.ts
Normal file
47
frontend/src/lib/desktop.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
interface RFCPDesktop {
|
||||||
|
getDataPath: () => Promise<string>;
|
||||||
|
getAppVersion: () => Promise<string>;
|
||||||
|
getPlatform: () => Promise<string>;
|
||||||
|
getGpuInfo: () => Promise<{
|
||||||
|
available: boolean;
|
||||||
|
name: string | null;
|
||||||
|
cudaVersion: string | null;
|
||||||
|
}>;
|
||||||
|
selectDirectory: () => Promise<string | null>;
|
||||||
|
selectFile: (options?: { filters?: Array<{ name: string; extensions: string[] }> }) => Promise<string | null>;
|
||||||
|
saveFile: (options?: { defaultPath?: string; filters?: Array<{ name: string; extensions: string[] }> }) => Promise<string | null>;
|
||||||
|
getSetting: (key: string) => Promise<unknown>;
|
||||||
|
setSetting: (key: string, value: unknown) => Promise<void>;
|
||||||
|
openExternal: (url: string) => Promise<void>;
|
||||||
|
openPath: (path: string) => Promise<void>;
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Backend API client for RFCP coverage calculation
|
* Backend API client for RFCP coverage calculation
|
||||||
*/
|
*/
|
||||||
|
import { getApiBaseUrl } from '@/lib/desktop.ts';
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || 'https://api.rfcp.eliah.one';
|
const API_BASE = getApiBaseUrl();
|
||||||
|
|
||||||
// === Request types ===
|
// === Request types ===
|
||||||
|
|
||||||
|
|||||||
38
installer/build-linux.sh
Normal file
38
installer/build-linux.sh
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/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/"
|
||||||
61
installer/build-mac.sh
Normal file
61
installer/build-mac.sh
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/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/"
|
||||||
39
installer/build-win.sh
Normal file
39
installer/build-win.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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/"
|
||||||
67
installer/rfcp-server.spec
Normal file
67
installer/rfcp-server.spec
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- 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,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user