diff --git a/electron-app/.gitignore b/electron-app/.gitignore
index ce4204b7b..2f4d5e1f1 100644
--- a/electron-app/.gitignore
+++ b/electron-app/.gitignore
@@ -7,6 +7,11 @@ dist-ssr
build
binaries
renderer
+!renderer/
+renderer/testing-view/
+renderer/flashing-view/
+!renderer/mode-selector/
+!renderer/mode-selector/**
out
*.local
diff --git a/electron-app/README.md b/electron-app/README.md
index d28d055ad..03c363201 100644
--- a/electron-app/README.md
+++ b/electron-app/README.md
@@ -24,7 +24,7 @@ When running in development mode (unpackaged), the application creates temporary
- `binaries/` - Directory containing compiled backend and BLCU programming executables for your platform. These are generated during the build process, when running `pnpm run build`.
-- `renderer/` - Directory containing built frontend views (control-station, ethernet-view). These are generated during the build process, when running `pnpm run build`.
+- `renderer/` - Directory containing built frontend views (control-station, ethernet-view). These are generated during the build process, when running `pnpm run build`. All its content is ignored, except `mode-selector`
- `dist/` - Build output directory containing compiled and packaged application files. Generated during build and distribution processes, when running `pnpm run dist`.
@@ -100,9 +100,10 @@ sudo ifconfig lo0 alias 127.0.0.9 up
- **Backend Process**: Go backend for data processing
- **BLCU Programming Process**: Packaged FastAPI/TFTP API for firmware transfers
-- **Packet Sender**: Tool for sending test packets
- **Configuration**: TOML-based config management
-- **Views**: Multiple frontend interfaces (Competition/Testing)
+- **Views**: Multiple frontend interfaces (Competition, Testing & Flashing)
+
+- **Mode Selector**: html5 file to chose the mode of the app: testing, competition or flashing. Placed at `renderer/mode-selector`
## Dependencies
diff --git a/electron-app/main.js b/electron-app/main.js
index b5067bd32..6ce31fd17 100644
--- a/electron-app/main.js
+++ b/electron-app/main.js
@@ -1,121 +1,91 @@
/**
* @module main
* @description Main entry point for the Electron application.
- * Handles application lifecycle, initialization, and cleanup of processes and windows.
+ *
+ * Orchestrates application lifecycle and initialization through modular components:
+ * - initialization: Config, IPC, and process cleanup
+ * - modeSelector: Mode selection UI and main window creation
+ * - updater: Auto-update functionality
+ * - lifecycle: App lifecycle event handling
*/
-import { app, BrowserWindow, dialog, screen } from "electron";
-import pkg from "electron-updater";
-import { getConfigManager } from "./src/config/configInstance.js";
-import { setupIpcHandlers } from "./src/ipc/handlers.js";
-import { startBackend, stopBackend } from "./src/processes/backend.js";
-import { startBlcuProgramming, stopBlcuProgramming } from "./src/processes/blcuProgramming.js";
+import { app, BrowserWindow, screen } from "electron";
+import {
+ handleSelectorFallback,
+ initializeApp,
+ setTransitionMode,
+ setupLifecycleHandlers,
+ setupUpdater,
+ showModeSelector,
+} from "./src/app/index.js";
+import { stopBackend } from "./src/processes/backend.js";
+import { stopBlcuProgramming } from "./src/processes/blcuProgramming.js";
import { logger } from "./src/utils/logger.js";
-import { createLogWindow } from "./src/windows/logWindow.js";
-import { createWindow } from "./src/windows/mainWindow.js";
-const { autoUpdater } = pkg;
-
-// Setup IPC handlers for renderer process communication
-setupIpcHandlers();
-
-// App lifecycle: wait for Electron to be ready
+/**
+ * Initializes the application when Electron is ready.
+ * Orchestrates startup sequence:
+ * 1. Initialize config and IPC
+ * 2. Show mode selector
+ * 3. Setup auto-updater
+ * 4. Setup lifecycle handlers
+ */
app.whenReady().then(async () => {
- // Get the screen width and height
- // Only can be used inside app.whenReady()
- const { width: screenWidth, height: screenHeight } =
- screen.getPrimaryDisplay().workAreaSize;
-
- // Initialize ConfigManager and ensure config exists BEFORE starting backend
- logger.electron.header("Initializing configuration...");
- // Get ConfigManager instance (creates config from template if needed)
- await getConfigManager();
- logger.electron.header("Configuration ready");
-
- const logWindow = createLogWindow(screenWidth, screenHeight);
-
- // Start backend process
try {
- await startBackend(logWindow);
- logger.electron.header("Backend process spawned");
- } catch (error) {
- // Start backend already shows these errors
- }
-
- try {
- await startBlcuProgramming(logWindow);
- logger.electron.header("BLCU programming process spawned");
- } catch (error) {
- logger.electron.error("Failed to start BLCU programming:", error);
- }
-
- // Create main application window
- const mainWindow = createWindow(screenWidth, screenHeight);
- mainWindow.maximize();
-
- logger.electron.header("Main application window created");
-
- // Updater setup
- if (!app.isPackaged) {
- autoUpdater.forceDevUpdateConfig = true;
- }
-
- autoUpdater.logger = {
- info: (message) => logger.electron.info(message),
- error: (message) => logger.electron.error(message),
- warn: (message) => logger.electron.warning(message),
- debug: (message) => logger.electron.debug(message),
- };
-
- // Check for updates
- autoUpdater.checkForUpdates();
-
- // Handle update downloaded event
- autoUpdater.on("update-downloaded", (info) => {
- dialog
- .showMessageBox({
- type: "info",
- title: "Update Ready",
- message: `Version ${info.version} has been downloaded. Restart now to install?`,
- buttons: ["Restart", "Later"],
- })
- .then((result) => {
- if (result.response === 0) {
- autoUpdater.quitAndInstall();
- }
- });
- });
-
- // Handle macOS app activation (reopen window when dock icon clicked)
- app.on("activate", () => {
- // Only create window if no windows exist
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
+ // Initialize configuration, IPC, and cleanup
+ await initializeApp();
+
+ // Get screen dimensions for window creation
+ const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
+
+ // Register return-to-selector handler before starting the app.
+ app.on("return-to-selector", async () => {
+ try {
+ // Set transition mode to prevent window-all-closed from quitting
+ setTransitionMode(true);
+
+ logger.electron.info("Returning to selector mode...");
+ await Promise.all([stopBackend(), stopBlcuProgramming()]);
+
+ // Give ports time to be released (increased to 3s for TCP TIME_WAIT state)
+ await new Promise((resolve) => setTimeout(resolve, 3000));
+
+ const existingWindows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed());
+ existingWindows.forEach((window) => window.hide());
+
+ const { width: selectorWidth, height: selectorHeight } = screen.getPrimaryDisplay().workAreaSize;
+ await showModeSelector(selectorWidth, selectorHeight);
+
+ existingWindows.forEach((window) => {
+ if (!window.isDestroyed()) {
+ window.removeAllListeners("close");
+ window.close();
+ }
+ });
+
+ // Exit transition mode once selector is shown
+ setTransitionMode(false);
+ } catch (error) {
+ logger.electron.error("Failed to return to selector:", error);
+ setTransitionMode(false);
+ }
+ });
+
+ // Show mode selector and get user choice
+ try {
+ await showModeSelector(screenWidth, screenHeight);
+ } catch (error) {
+ logger.electron.error("Mode selector failed:", error);
+ await handleSelectorFallback(screenWidth, screenHeight);
}
- });
-});
-// Handle window close behavior
-app.on("window-all-closed", () => {
- // On macOS, keep app running even when all windows are closed
- if (process.platform !== "darwin") {
- // Quit app on other platforms when all windows are closed
+ // Setup auto-updater
+ setupUpdater();
+
+ // Setup application lifecycle handlers (window-all-closed, before-quit, activate, exceptions)
+ setupLifecycleHandlers();
+ } catch (error) {
+ logger.electron.error("Failed to initialize application:", error);
app.quit();
}
});
-
-// Cleanup before app quits
-app.on("before-quit", (e) => {
- e.preventDefault();
- Promise.all([stopBackend(), stopBlcuProgramming()])
- .catch((error) => logger.electron.error("Error during shutdown:", error))
- .finally(() => app.exit());
-});
-
-// Handle uncaught exceptions globally
-process.on("uncaughtException", (error) => {
- // Log error to console
- logger.electron.error("Uncaught exception:", error);
- // Show error dialog to user
- dialog.showErrorBox("Error", error.message);
-});
diff --git a/electron-app/package.json b/electron-app/package.json
index 083a5bd56..58265f789 100644
--- a/electron-app/package.json
+++ b/electron-app/package.json
@@ -1,6 +1,6 @@
{
"name": "hyperloop-control-station",
- "version": "1.0.0",
+ "version": "11.1.0",
"description": "Hyperloop UPV Control Station",
"main": "main.js",
"type": "module",
diff --git a/electron-app/preload.js b/electron-app/preload.js
index 5fda40215..e9fcb690d 100644
--- a/electron-app/preload.js
+++ b/electron-app/preload.js
@@ -38,6 +38,13 @@ contextBridge.exposeInMainWorld("electronAPI", {
selectFolder: () => ipcRenderer.invoke("select-folder"),
// Open a folder path in the OS file explorer
openFolder: (path) => ipcRenderer.invoke("open-folder", path),
+ // Get the application version from the main process
+ getAppVersion: () => ipcRenderer.invoke("get-app-version"),
+ // Set initial mode (used by mode selector renderer)
+ setInitialMode: (mode) => {
+ ipcRenderer.send("mode-selected", mode);
+ return Promise.resolve();
+ },
// Receive log message from backend
onLog: (callback) => {
const listener = (_event, value) => callback(value);
diff --git a/electron-app/renderer/mode-selector/index.html b/electron-app/renderer/mode-selector/index.html
new file mode 100644
index 000000000..e554d12fc
--- /dev/null
+++ b/electron-app/renderer/mode-selector/index.html
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+ Seleccione Modo
+
+
+
+
+
+

+
+

+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/electron-app/renderer/mode-selector/src/Subsystem.png b/electron-app/renderer/mode-selector/src/Subsystem.png
new file mode 100644
index 000000000..aed1aee91
Binary files /dev/null and b/electron-app/renderer/mode-selector/src/Subsystem.png differ
diff --git a/electron-app/renderer/mode-selector/src/logo.svg b/electron-app/renderer/mode-selector/src/logo.svg
new file mode 100644
index 000000000..20d20319d
--- /dev/null
+++ b/electron-app/renderer/mode-selector/src/logo.svg
@@ -0,0 +1,28 @@
+
+
\ No newline at end of file
diff --git a/electron-app/renderer/mode-selector/src/sign.png b/electron-app/renderer/mode-selector/src/sign.png
new file mode 100644
index 000000000..5771951dd
Binary files /dev/null and b/electron-app/renderer/mode-selector/src/sign.png differ
diff --git a/electron-app/src/app/cleanup.js b/electron-app/src/app/cleanup.js
new file mode 100644
index 000000000..3cc8c8ffc
--- /dev/null
+++ b/electron-app/src/app/cleanup.js
@@ -0,0 +1,33 @@
+/**
+ * @module app/cleanup
+ * @description Cleanup utilities for terminating leftover processes from previous sessions.
+ */
+
+import { execSync } from "child_process";
+import { logger } from "../utils/logger.js";
+
+/**
+ * Terminates any leftover backend processes from previous sessions.
+ * Prevents backend/log windows from appearing before user selects a mode.
+ * @returns {Promise}
+ */
+async function cleanupLeftoverBackendProcesses() {
+ try {
+ const out = execSync("pgrep -f backend-linux-amd64 || true").toString().trim();
+ if (out) {
+ const pids = out.split(/\s+/).filter(Boolean);
+ for (const pid of pids) {
+ try {
+ process.kill(Number(pid), "SIGTERM");
+ logger.electron.info(`Terminated leftover backend pid=${pid}`);
+ } catch (e) {
+ logger.electron.debug(`Failed to terminate pid ${pid}:`, e);
+ }
+ }
+ }
+ } catch (e) {
+ logger.electron.debug("Error checking/killing leftover backends:", e);
+ }
+}
+
+export { cleanupLeftoverBackendProcesses };
diff --git a/electron-app/src/app/index.js b/electron-app/src/app/index.js
new file mode 100644
index 000000000..5ae193df7
--- /dev/null
+++ b/electron-app/src/app/index.js
@@ -0,0 +1,11 @@
+/**
+ * @module app
+ * @description Application lifecycle and initialization exports.
+ */
+
+export { cleanupLeftoverBackendProcesses } from "./cleanup.js";
+export { initializeApp } from "./initialization.js";
+export { setupLifecycleHandlers, setTransitionMode } from "./lifecycle.js";
+export { handleSelectorFallback, showModeSelector } from "./modeSelector.js";
+export { setupUpdater } from "./updater.js";
+
diff --git a/electron-app/src/app/initialization.js b/electron-app/src/app/initialization.js
new file mode 100644
index 000000000..3e6195ca0
--- /dev/null
+++ b/electron-app/src/app/initialization.js
@@ -0,0 +1,31 @@
+/**
+ * @module app/initialization
+ * @description Application initialization: config setup and cleanup.
+ */
+
+import { getConfigManager } from "../config/configInstance.js";
+import { setupIpcHandlers } from "../ipc/handlers.js";
+import { logger } from "../utils/logger.js";
+import { cleanupLeftoverBackendProcesses } from "./cleanup.js";
+
+/**
+ * Initializes the application:
+ * - Sets up IPC handlers
+ * - Initializes configuration
+ * - Cleans up leftover processes
+ * @returns {Promise}
+ */
+async function initializeApp() {
+ // Setup IPC handlers for renderer process communication
+ setupIpcHandlers();
+
+ // Initialize ConfigManager and ensure config exists
+ logger.electron.header("Initializing configuration...");
+ await getConfigManager();
+ logger.electron.header("Configuration ready");
+
+ // Clean up leftover processes from previous sessions
+ await cleanupLeftoverBackendProcesses();
+}
+
+export { initializeApp };
diff --git a/electron-app/src/app/lifecycle.js b/electron-app/src/app/lifecycle.js
new file mode 100644
index 000000000..acad03e7b
--- /dev/null
+++ b/electron-app/src/app/lifecycle.js
@@ -0,0 +1,67 @@
+/**
+ * @module app/lifecycle
+ * @description Application lifecycle event handlers.
+ */
+
+import { app, BrowserWindow, dialog } from "electron";
+import { stopBackend } from "../processes/backend.js";
+import { stopBlcuProgramming } from "../processes/blcuProgramming.js";
+import { logger } from "../utils/logger.js";
+import { createWindow } from "../windows/index.js";
+
+// Flag to indicate we're in a transition (e.g., returning to selector)
+let isInTransition = false;
+
+/**
+ * Sets the transition flag
+ * @param {boolean} inTransition
+ */
+function setTransitionMode(inTransition) {
+ isInTransition = inTransition;
+}
+
+/**
+ * Sets up all application lifecycle event handlers.
+ * @returns {void}
+ */
+function setupLifecycleHandlers() {
+ // Handle window close behavior
+ app.on("window-all-closed", () => {
+ // Don't quit if we're in a transition (returning to selector)
+ if (isInTransition) {
+ logger.electron.debug("In transition mode - skipping app quit on window-all-closed");
+ return;
+ }
+
+ // On macOS, keep app running even when all windows are closed
+ if (process.platform !== "darwin") {
+ // Quit app on other platforms when all windows are closed
+ app.quit();
+ }
+ });
+
+ // Cleanup before app quits
+ app.on("before-quit", (e) => {
+ e.preventDefault();
+ Promise.all([stopBackend(), stopBlcuProgramming()])
+ .catch((error) => logger.electron.error("Error during shutdown:", error))
+ .finally(() => app.exit());
+ });
+
+ // Handle macOS app activation (reopen window when dock icon clicked)
+ app.on("activate", () => {
+ // Only create window if no windows exist
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
+ });
+
+ // Handle uncaught exceptions globally
+ process.on("uncaughtException", (error) => {
+ logger.electron.error("Uncaught exception:", error);
+ dialog.showErrorBox("Error", error.message);
+ });
+}
+
+export { setTransitionMode, setupLifecycleHandlers };
+
diff --git a/electron-app/src/app/modeSelector.js b/electron-app/src/app/modeSelector.js
new file mode 100644
index 000000000..a64d54fcf
--- /dev/null
+++ b/electron-app/src/app/modeSelector.js
@@ -0,0 +1,171 @@
+/**
+ * @module app/modeSelector
+ * @description Mode selector window and logic for initial app mode selection.
+ */
+
+import { BrowserWindow, ipcMain } from "electron";
+import fs from "fs";
+import path from "path";
+import { startBackend } from "../processes/backend.js";
+import { startBlcuProgramming } from "../processes/blcuProgramming.js";
+import { logger } from "../utils/logger.js";
+import { getAppPath } from "../utils/paths.js";
+import { createLogWindow, createWindow } from "../windows/index.js";
+import { loadView } from "../windows/mainWindow.js";
+
+const VALID_MODES = {
+ testing: "testing-view",
+ flashing: "flashing-view",
+ default: "testing-view",
+};
+
+/**
+ * Creates and displays the mode selector window.
+ * Returns a Promise that resolves when user selects a mode.
+ * @param {number} screenWidth
+ * @param {number} screenHeight
+ * @returns {Promise<{mode: string, view: string, mainWindow: BrowserWindow}>}
+ */
+async function showModeSelector(screenWidth, screenHeight) {
+ return new Promise(async (resolve, reject) => {
+ let mainWindow = null;
+
+ const selectorWindow = new BrowserWindow({
+ width: 920,
+ height: 680,
+ useContentSize: true,
+ resizable: true,
+ modal: true,
+ parent: mainWindow,
+ show: true,
+ webPreferences: {
+ preload: path.join(getAppPath(), "preload.js"),
+ contextIsolation: true,
+ nodeIntegration: false,
+ },
+ title: "Select Mode",
+ });
+
+ const selectorPath = path.join(getAppPath(), "renderer", "mode-selector", "index.html");
+
+ if (!fs.existsSync(selectorPath)) {
+ logger.electron.warning("Mode selector UI not found, using default testing-view");
+ resolve({ mode: "default", view: VALID_MODES.default, mainWindow: null });
+ return;
+ }
+
+ logger.electron.info(`Mode selector found: ${selectorPath}`);
+
+ try {
+ await selectorWindow.loadFile(selectorPath);
+ selectorWindow.show();
+ selectorWindow.focus();
+ } catch (err) {
+ logger.electron.error("Failed to load selector UI:", err);
+ resolve({ mode: "default", view: VALID_MODES.default, mainWindow: null });
+ return;
+ }
+
+ // Listen for mode selection from renderer
+ ipcMain.once("mode-selected", async (_event, mode) => {
+ try {
+ const view = VALID_MODES[mode] || VALID_MODES.default;
+
+ // Create the main window without loading the view yet.
+ mainWindow = createWindow(screenWidth, screenHeight, null);
+ try {
+ mainWindow.maximize();
+ } catch (e) {}
+ logger.electron.header("Main application window created");
+
+ // Start services and only then load the selected view.
+ if (view === "testing-view" || view === "flashing-view") {
+ await startServices(screenWidth, screenHeight, view);
+ }
+
+ loadView(view);
+
+ // Show and focus main window
+ try {
+ mainWindow.show();
+ mainWindow.focus();
+ } catch (e) {}
+
+ resolve({ mode, view, mainWindow });
+ } catch (error) {
+ logger.electron.error("Error handling mode selection:", error);
+ reject(error);
+ } finally {
+ try {
+ selectorWindow.close();
+ } catch (e) {}
+ }
+ });
+ });
+}
+
+/**
+ * Starts services based on the selected view.
+ * - testing-view: Backend only
+ * - flashing-view: BLCU Programming only
+ * @param {number} screenWidth
+ * @param {number} screenHeight
+ * @param {string} view - The selected view mode
+ * @returns {Promise}
+ */
+async function startServices(screenWidth, screenHeight, view) {
+ // Start backend only for testing view
+ if (view === "testing-view") {
+ const logWindow = createLogWindow(screenWidth, screenHeight);
+ logWindow.show(); // Show the log window
+
+ try {
+ await startBackend(logWindow);
+ logger.electron.header("Backend process spawned");
+ } catch (err) {
+ logger.electron.error("Failed to start backend:", err);
+ if (logWindow && !logWindow.isDestroyed()) {
+ logWindow.close();
+ }
+ }
+ }
+
+ // Start BLCU Programming only for flashing view
+ if (view === "flashing-view") {
+ try {
+ await startBlcuProgramming();
+ logger.electron.header("BLCU programming process spawned");
+ } catch (err) {
+ logger.electron.error("Failed to start BLCU programming:", err);
+ }
+ }
+}
+
+/**
+ * Handles fallback when selector is not available or fails.
+ * @param {number} screenWidth
+ * @param {number} screenHeight
+ * @returns {Promise<{mode: string, view: string, mainWindow: BrowserWindow}>}
+ */
+async function handleSelectorFallback(screenWidth, screenHeight) {
+ const view = VALID_MODES.default;
+ const mainWindow = createWindow(screenWidth, screenHeight, view);
+
+ try {
+ mainWindow.maximize();
+ } catch (e) {}
+
+ logger.electron.header("Main application window created");
+
+ try {
+ mainWindow.show();
+ } catch (e) {}
+
+ // Start services by default (testing view)
+ await startServices(screenWidth, screenHeight, view);
+
+ return { mode: "default", view, mainWindow };
+}
+
+export { handleSelectorFallback, showModeSelector };
+
diff --git a/electron-app/src/app/updater.js b/electron-app/src/app/updater.js
new file mode 100644
index 000000000..c5a9c5dba
--- /dev/null
+++ b/electron-app/src/app/updater.js
@@ -0,0 +1,49 @@
+/**
+ * @module app/updater
+ * @description Auto-updater configuration and event handling.
+ */
+
+import { app, dialog } from "electron";
+import pkg from "electron-updater";
+import { logger } from "../utils/logger.js";
+
+const { autoUpdater } = pkg;
+
+/**
+ * Initializes the auto-updater with appropriate logging and event handlers.
+ * @returns {void}
+ */
+function setupUpdater() {
+ if (!app.isPackaged) {
+ autoUpdater.forceDevUpdateConfig = true;
+ }
+
+ // Configure auto-updater logging
+ autoUpdater.logger = {
+ info: (message) => logger.electron.info(message),
+ error: (message) => logger.electron.error(message),
+ warn: (message) => logger.electron.warning(message),
+ debug: (message) => logger.electron.debug(message),
+ };
+
+ // Handle update downloaded event
+ autoUpdater.on("update-downloaded", (info) => {
+ dialog
+ .showMessageBox({
+ type: "info",
+ title: "Update Ready",
+ message: `Version ${info.version} has been downloaded. Restart now to install?`,
+ buttons: ["Restart", "Later"],
+ })
+ .then((result) => {
+ if (result.response === 0) {
+ autoUpdater.quitAndInstall();
+ }
+ });
+ });
+
+ // Check for updates
+ autoUpdater.checkForUpdates();
+}
+
+export { setupUpdater };
diff --git a/electron-app/src/ipc/handlers.js b/electron-app/src/ipc/handlers.js
index 269ea5235..5a4198d23 100644
--- a/electron-app/src/ipc/handlers.js
+++ b/electron-app/src/ipc/handlers.js
@@ -7,7 +7,7 @@
* - Folder selection dialogs
*/
-import { dialog, ipcMain, shell } from "electron";
+import { app, dialog, ipcMain, shell } from "electron";
import fs from "fs";
import { isAbsolute, join } from "path";
import {
@@ -40,6 +40,8 @@ function setupIpcHandlers() {
*/
ipcMain.handle("get-current-view", () => getCurrentView());
+ ipcMain.handle("get-app-version", () => app.getVersion());
+
/**
* @event switch-view
* @description Switches the main window to the specified view.
@@ -52,6 +54,8 @@ function setupIpcHandlers() {
return view;
});
+
+
/**
* @event save-config
* @async
diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js
index c64b64ff4..a7cdc27e7 100644
--- a/electron-app/src/menu/menu.js
+++ b/electron-app/src/menu/menu.js
@@ -1,15 +1,15 @@
/**
* @module menu
* @description Application menu creation and management for the Electron application.
- * Defines menu structure with File, View, Tools, and Help sections with keyboard shortcuts and actions.
+ * Defines menu structure with File, Tools, and Help sections with keyboard shortcuts and actions.
*/
import { Menu, app, dialog } from "electron";
-import { loadView } from "../windows/mainWindow.js";
/**
- * Creates and sets the application menu with File, View, Tools, and Help sections.
- * Includes menu items for reloading, exiting, switching views, toggling DevTools, and managing packet sender.
+ * Creates and sets the application menu with File, Tools, and Help sections.
+ * Includes menu items for reloading, exiting, toggling DevTools, and app information.
+ * View switching is no longer available since the mode is selected at startup.
* @param {import("electron").BrowserWindow} mainWindow - The main browser window instance to attach menu actions to.
* @returns {void}
* @example
@@ -29,6 +29,11 @@ function createMenu(mainWindow) {
}
},
},
+ {
+ label: "Return to Selector",
+ accelerator: "CmdOrCtrl+Shift+S",
+ click: () => app.emit("return-to-selector"),
+ },
{ type: "separator" },
{
label: "Exit",
@@ -38,23 +43,8 @@ function createMenu(mainWindow) {
],
},
{
- label: "View",
+ label: "Tools",
submenu: [
- {
- label: "Competition View",
- accelerator: "CmdOrCtrl+1",
- click: () => {
- loadView("competition-view");
- },
- },
- {
- label: "Testing View",
- accelerator: "CmdOrCtrl+2",
- click: () => {
- loadView("testing-view");
- },
- },
- { type: "separator" },
{
label: "Toggle DevTools",
accelerator: "F12",
diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js
index b021edfdc..64b51c838 100644
--- a/electron-app/src/processes/backend.js
+++ b/electron-app/src/processes/backend.js
@@ -32,6 +32,18 @@ let storedLogWindow = null;
// Store error messages accumulated from the current process run
let lastBackendError = null;
+/**
+ * Clears the stored log window reference and closes it
+ */
+function clearLogWindow() {
+ if (storedLogWindow && !storedLogWindow.isDestroyed()) {
+ try {
+ storedLogWindow.close();
+ } catch (e) {}
+ }
+ storedLogWindow = null;
+}
+
/**
* Starts the backend process by spawning the backend binary with the user configuration.
* @returns {void}
@@ -46,6 +58,8 @@ async function startBackend(logWindow = null) {
const currentLogWindow = logWindow || storedLogWindow;
return new Promise((resolve, reject) => {
+ let resolved = false;
+
// Get paths for binary and config
const backendBin = getBinaryPath("backend");
const configPath = getUserConfigPath();
@@ -91,9 +105,10 @@ async function startBackend(logWindow = null) {
// Resolve as soon as the HTTP server confirms it is listening.
// Matches: "INF ... > http server listening localAddr=..."
- if (text.includes("http server listening")) {
+ if (text.includes("http server listening") && !resolved) {
logger.backend.info("Backend ready (HTTP server listening)");
clearTimeout(startupTimer);
+ resolved = true;
resolve(backendProcess);
}
});
@@ -118,7 +133,10 @@ async function startBackend(logWindow = null) {
"Backend Error",
`Failed to start backend: ${error.message}`,
);
- return reject(new Error(`Failed to start backend: ${error.message}`));
+ if (!resolved) {
+ resolved = true;
+ return reject(new Error(`Failed to start backend: ${error.message}`));
+ }
});
// Handle process exit
@@ -126,21 +144,32 @@ async function startBackend(logWindow = null) {
logger.backend.info(`Backend process exited with code ${code}`);
clearTimeout(startupTimer);
- if (code !== 0 && code !== null) {
- let errorMessage = `Backend exited with code ${code}`;
-
- if (lastBackendError) {
- const stripped = lastBackendError.replace(/\x1b\[[0-9;]*m/g, "");
- const formatted = formatBackendError(stripped);
- errorMessage += `\n\n${getHint(stripped, formatted)}`;
- } else {
- errorMessage += "\n\n(No error output captured)";
+ // If the process closed without success, reject the promise
+ if (!resolved) {
+ if (code !== 0 && code !== null) {
+ let errorMessage = `Backend exited with code ${code}`;
+
+ if (lastBackendError) {
+ const stripped = lastBackendError.replace(/\x1b\[[0-9;]*m/g, "");
+ const formatted = formatBackendError(stripped);
+ errorMessage += `\n\n${getHint(stripped, formatted)}`;
+ } else {
+ errorMessage += "\n\n(No error output captured)";
+ }
+
+ dialog.showErrorBox("Backend Crashed", errorMessage);
+ lastBackendError = null;
+ backendProcess = null;
+ resolved = true;
+ return reject(new Error(errorMessage));
}
- dialog.showErrorBox("Backend Crashed", errorMessage);
- lastBackendError = null;
- backendProcess = null;
- return reject(new Error(errorMessage));
+ if (code === null || code === 0) {
+ logger.backend.warning("Backend closed before ready signal - likely port conflict or initialization error");
+ backendProcess = null;
+ resolved = true;
+ return reject(new Error("Backend process closed before initialization completed"));
+ }
}
backendProcess = null;
@@ -148,10 +177,13 @@ async function startBackend(logWindow = null) {
// Fallback: if the ready message never appears, resolve anyway after timeout
const startupTimer = setTimeout(() => {
- logger.backend.warning(
- "Backend ready signal not received - resolving after timeout",
- );
- resolve(backendProcess);
+ if (!resolved) {
+ logger.backend.warning(
+ "Backend ready signal not received - resolving after timeout",
+ );
+ resolved = true;
+ resolve(backendProcess);
+ }
}, 5000);
});
}
@@ -176,6 +208,8 @@ async function stopBackend() {
if (localBackendProcess === backendProcess) {
backendProcess = null;
}
+ // Clean up log window
+ clearLogWindow();
resolve();
});
@@ -194,6 +228,8 @@ async function stopBackend() {
fallbackTimer.unref();
} else {
logger.backend.warning("Backend process not found, skipping stop...");
+ // Clean up log window even if process doesn't exist
+ clearLogWindow();
resolve();
}
});
@@ -209,6 +245,10 @@ async function restartBackend() {
// Stop current process first
await stopBackend();
+ if (localBackendProcess.stdin) {
+ localBackendProcess.stdin.end();
+ }
+
// Start a new process
try {
await startBackend();
@@ -225,4 +265,4 @@ function getBackendWorkingDir() {
: path.dirname(getUserConfigPath());
}
-export { getBackendWorkingDir, restartBackend, startBackend, stopBackend };
+export { clearLogWindow, getBackendWorkingDir, restartBackend, startBackend, stopBackend };
diff --git a/electron-app/src/processes/blcuProgramming.js b/electron-app/src/processes/blcuProgramming.js
index aebd2a3a6..80b7d7701 100644
--- a/electron-app/src/processes/blcuProgramming.js
+++ b/electron-app/src/processes/blcuProgramming.js
@@ -139,12 +139,27 @@ async function startBlcuProgramming() {
}
async function stopBlcuProgramming() {
- if (!blcuProgrammingProcess || blcuProgrammingProcess.killed) {
- return;
- }
+ return new Promise((resolve) => {
+ if (!blcuProgrammingProcess || blcuProgrammingProcess.killed) {
+ resolve();
+ return;
+ }
+
+ blcuProgrammingProcess.once("close", () => {
+ resolve();
+ });
- blcuProgrammingProcess.kill("SIGTERM");
- blcuProgrammingProcess = null;
+ blcuProgrammingProcess.kill("SIGTERM");
+
+ const fallbackTimer = setTimeout(() => {
+ if (blcuProgrammingProcess && !blcuProgrammingProcess.killed) {
+ blcuProgrammingProcess.kill("SIGKILL");
+ }
+ resolve();
+ }, 3000);
+
+ fallbackTimer.unref();
+ });
}
export { startBlcuProgramming, stopBlcuProgramming };
diff --git a/electron-app/src/windows/index.js b/electron-app/src/windows/index.js
new file mode 100644
index 000000000..06cd375b2
--- /dev/null
+++ b/electron-app/src/windows/index.js
@@ -0,0 +1,8 @@
+/**
+ * @module windows
+ * @description Window creation and management exports.
+ */
+
+export { createLogWindow } from "./logWindow.js";
+export { createWindow } from "./mainWindow.js";
+
diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js
index 0bf125b85..3fc755f64 100644
--- a/electron-app/src/windows/mainWindow.js
+++ b/electron-app/src/windows/mainWindow.js
@@ -24,7 +24,7 @@ let currentView = "testing-view";
* @example
* createWindow();
*/
-function createWindow(screenWidth, screenHeight) {
+function createWindow(screenWidth, screenHeight, initialView) {
// Create new browser window with configuration
mainWindow = new BrowserWindow({
x: 0,
@@ -47,8 +47,15 @@ function createWindow(screenWidth, screenHeight) {
backgroundColor: "#1a1a1a",
});
- // Load ethernet view by default
- loadView(currentView);
+ // If an initial view string is provided, load it.
+ // If `initialView` is explicitly null, skip loading so caller can decide later.
+ if (typeof initialView === "string") {
+ loadView(initialView);
+ } else if (initialView === null) {
+ // skip loading any view for now
+ } else {
+ loadView(currentView);
+ }
// Create application menu
const menu = createMenu(mainWindow);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3eb9ad9ae..06b11e8bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -329,8 +329,6 @@ importers:
specifier: ^7.3.1
version: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)
- packet-sender: {}
-
packages:
7zip-bin@5.2.0:
@@ -2109,6 +2107,7 @@ packages:
'@xmldom/xmldom@0.8.11':
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
engines: {node: '>=10.0.0'}
+ deprecated: this version has critical issues, please update to the latest version
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
@@ -2325,6 +2324,7 @@ packages:
basic-ftp@5.1.0:
resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==}
engines: {node: '>=10.0.0'}
+ deprecated: Security vulnerability fixed in 5.2.1, please upgrade
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}