diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2564f79db..516ddbdc7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,11 +53,13 @@ jobs: competition-view: ${{ steps.filter.outputs.competition-view == 'true' || github.event.inputs.rebuild-competition-view == 'true' || inputs.build-competition-view == true }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dorny/paths-filter@v3 id: filter with: - ref: "production" + base: ${{ github.event.before }} filters: | backend: - 'backend/**/*' @@ -113,7 +115,7 @@ jobs: with: workflow: build.yaml branch: production - workflow_conclusion: success + workflow_conclusion: completed name: backend-${{ matrix.platform }} path: backend/cmd diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml index 149d955df..0599a5097 100644 --- a/.github/workflows/frontend-tests.yaml +++ b/.github/workflows/frontend-tests.yaml @@ -41,5 +41,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --filter=testing-view --filter=ui --filter=core + - name: Build frontend + run: pnpm build --filter="./frontend/**" + - name: Run tests run: pnpm test --filter="./frontend/**" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index abcc3c225..01f375565 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -86,6 +86,12 @@ jobs: echo "Updated version to:" cat package.json | grep version + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y rpm libarchive-tools + # Download ONLY the appropriate backend for this platform - name: Download Linux backend if: runner.os == 'Linux' @@ -182,6 +188,8 @@ jobs: electron-app/dist/*.exe electron-app/dist/*.AppImage electron-app/dist/*.deb + electron-app/dist/*.rpm + electron-app/dist/*.pacman electron-app/dist/*.dmg electron-app/dist/*.zip electron-app/dist/*.yml diff --git a/backend/cmd/main.go b/backend/cmd/main.go index addc5c879..b31a17d63 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -5,12 +5,12 @@ import ( "os" "os/signal" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/flags" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" trace "github.com/rs/zerolog/log" diff --git a/backend/cmd/orchestrator.go b/backend/cmd/orchestrator.go index 180b26c8d..6d34b064a 100644 --- a/backend/cmd/orchestrator.go +++ b/backend/cmd/orchestrator.go @@ -7,11 +7,11 @@ import ( "runtime/pprof" "strings" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/flags" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" data_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/data" order_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/order" diff --git a/backend/cmd/setup_transport.go b/backend/cmd/setup_transport.go index c86f9270e..9c9a5b628 100644 --- a/backend/cmd/setup_transport.go +++ b/backend/cmd/setup_transport.go @@ -7,12 +7,12 @@ import ( "net" "time" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/utils" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tcp" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/udp" diff --git a/backend/cmd/setup_vehicle.go b/backend/cmd/setup_vehicle.go index b8a7b62e0..8fe8999e2 100644 --- a/backend/cmd/setup_vehicle.go +++ b/backend/cmd/setup_vehicle.go @@ -12,12 +12,12 @@ import ( h "github.com/HyperloopUPV-H8/h9-backend/pkg/http" "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/broker" connection_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/connection" data_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/data" diff --git a/backend/internal/pod_data/measurement.go b/backend/internal/pod_data/measurement.go index 446b88611..b2581c4e3 100644 --- a/backend/internal/pod_data/measurement.go +++ b/backend/internal/pod_data/measurement.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/utils" + "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" ) const EnumType = "enum" diff --git a/backend/internal/pod_data/pod_data.go b/backend/internal/pod_data/pod_data.go index 7cafa1411..3f091aaaa 100644 --- a/backend/internal/pod_data/pod_data.go +++ b/backend/internal/pod_data/pod_data.go @@ -3,8 +3,8 @@ package pod_data import ( "github.com/HyperloopUPV-H8/h9-backend/internal/utils" - "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" + "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" ) func NewPodData(adjBoards map[string]adj.Board, globalUnits map[string]utils.Operations) (PodData, error) { diff --git a/backend/internal/adj/adj.go b/backend/pkg/adj/adj.go similarity index 100% rename from backend/internal/adj/adj.go rename to backend/pkg/adj/adj.go diff --git a/backend/internal/adj/boards.go b/backend/pkg/adj/boards.go similarity index 100% rename from backend/internal/adj/boards.go rename to backend/pkg/adj/boards.go diff --git a/backend/internal/adj/git.go b/backend/pkg/adj/git.go similarity index 100% rename from backend/internal/adj/git.go rename to backend/pkg/adj/git.go diff --git a/backend/internal/adj/models.go b/backend/pkg/adj/models.go similarity index 100% rename from backend/internal/adj/models.go rename to backend/pkg/adj/models.go diff --git a/electron-app/README.md b/electron-app/README.md index 854930745..7034c163e 100644 --- a/electron-app/README.md +++ b/electron-app/README.md @@ -79,7 +79,7 @@ pnpm run dist:linux # Linux On macOS, the backend requires the loopback address `127.0.0.9` to be configured. If you encounter a "can't assign requested address" error when starting the backend, run: ``` -sudo ipconfig set en0 INFORM 127.0.0.9 +sudo ifconfig lo0 alias 127.0.0.9 up ``` ## Available Scripts @@ -89,6 +89,7 @@ sudo ipconfig set en0 INFORM 127.0.0.9 - `pnpm start` - Run application in development mode - `pnpm run dist` - Build production executable - `pnpm test` - Run tests +- `pnpm build-icons` - build icon from the icon.png file in the `/electron-app` folder ...and many custom variations (see package.json) # Only works and makes sense after running `pnpm run dist` diff --git a/electron-app/build.mjs b/electron-app/build.mjs index f83964a19..b4dff1753 100644 --- a/electron-app/build.mjs +++ b/electron-app/build.mjs @@ -5,7 +5,7 @@ */ import { execSync } from "child_process"; -import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from "fs"; +import { cpSync, existsSync, mkdirSync, rmSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { logger } from "./src/utils/logger.js"; @@ -20,6 +20,7 @@ const CONFIG = { type: "go", path: join(ROOT, "backend"), // Root of backend (where package.json is) output: join(__dirname, "binaries"), + entry: "./cmd", commands: ["pnpm run build:ci"], platforms: [ { @@ -52,18 +53,43 @@ const CONFIG = { }, ], }, - "packet-sender": { - type: "rust", - path: join(ROOT, "packet-sender"), - output: join(__dirname, "binaries"), - commands: ["pnpm run build"], - binaryPath: "target/release/packet-sender", - platforms: [ - { id: "win64", ext: ".exe", tags: ["win", "windows"] }, - { id: "linux64", ext: "", tags: ["linux"] }, - { id: "mac64", ext: "", tags: ["mac", "macos"] }, - ], - }, + // "packet-sender": { + // type: "go", + // path: join(ROOT, "packet-sender"), + // output: join(__dirname, "binaries"), + // entry: ".", + // commands: ["pnpm run build:ci"], + // platforms: [ + // { + // id: "win64", + // goos: "windows", + // goarch: "amd64", + // ext: ".exe", + // tags: ["win", "windows"], + // }, + // { + // id: "linux64", + // goos: "linux", + // goarch: "amd64", + // ext: "", + // tags: ["linux"], + // }, + // { + // id: "mac64", + // goos: "darwin", + // goarch: "amd64", + // ext: "", + // tags: ["mac", "macos"], + // }, + // { + // id: "macArm", + // goos: "darwin", + // goarch: "arm64", + // ext: "", + // tags: ["mac", "macos"], + // }, + // ], + // }, "testing-view": { type: "frontend", path: join(ROOT, "frontend/testing-view"), @@ -98,8 +124,8 @@ const run = (cmd, cwd, env = {}) => { } }; -const buildBackend = (config, requestedPlatforms, extraArgs = "") => { - logger.info("Building Backend (Go)..."); +const buildGo = (name, config, requestedPlatforms, extraArgs = "") => { + logger.info(`Building ${name} (Go)...`); mkdirSync(config.output, { recursive: true }); const targets = config.platforms.filter((p) => { @@ -112,22 +138,15 @@ const buildBackend = (config, requestedPlatforms, extraArgs = "") => { return p.tags.some((tag) => requestedPlatforms.includes(tag)); }); - if (targets.length === 0) { - logger.error( - `No matching platforms found for: ${requestedPlatforms.join(", ")}` - ); - return false; - } - let success = true; for (const p of targets) { - const filename = `backend-${p.goos}-${p.goarch}${p.ext}`; + const filename = `${name}-${p.goos}-${p.goarch}${p.ext}`; logger.step(`Building ${p.goos}/${p.goarch}...`); + const entryPath = config.entry || "."; + for (const cmd of config.commands) { - // cmd is like "pnpm run build:ci --" - // We append the output flag and target directory - const buildCmd = `${cmd} -o "${join(config.output, filename)}" ${extraArgs} ./cmd`; + const buildCmd = `${cmd} -o "${join(config.output, filename)}" ${extraArgs} ${entryPath}`; const result = run(buildCmd, config.path, { GOOS: p.goos, @@ -145,37 +164,6 @@ const buildBackend = (config, requestedPlatforms, extraArgs = "") => { return success; }; -const buildRust = (name, config, requestedPlatforms, extraArgs = "") => { - logger.info(`Building ${name} (Rust)...`); - mkdirSync(config.output, { recursive: true }); - - for (const cmd of config.commands) { - // Only append extra args to build commands - const finalCmd = cmd.includes("build") ? `${cmd} ${extraArgs}` : cmd; - if (!run(finalCmd, config.path)) return false; - } - - const isWin = - process.platform === "win32" || - (requestedPlatforms && requestedPlatforms.includes("win")); - const ext = isWin ? ".exe" : ""; - - // Check for source binary - const sourceBin = join(config.path, config.binaryPath + ext); - const destName = `packet-sender${ext}`; - const destPath = join(config.output, destName); - - logger.step(`Copying binary to ${destPath}...`); - - if (existsSync(sourceBin)) { - copyFileSync(sourceBin, destPath); - return true; - } else { - logger.error(`Rust binary not found at ${sourceBin}`); - return false; - } -}; - const buildFrontend = (name, config, extraArgs = "") => { if (config.optional && !existsSync(join(config.path, "package.json"))) { logger.warning(`Skipping ${name} (not initialized)`); @@ -252,9 +240,7 @@ logger.header("Hyperloop Control Station Build"); let success = true; if (config.type === "go") { - success = buildBackend(config, requestedPlatforms, extraArgs); - } else if (config.type === "rust") { - success = buildRust(key, config, requestedPlatforms, extraArgs); + success = buildGo(key, config, requestedPlatforms, extraArgs); } else if (config.type === "frontend") { success = buildFrontend(key, config, extraArgs); if (success && !config.optional) frontendBuilt = true; diff --git a/electron-app/main.js b/electron-app/main.js index ef5841b58..c468fbbe3 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -4,17 +4,35 @@ * Handles application lifecycle, initialization, and cleanup of processes and windows. */ -import { app, BrowserWindow, dialog } from "electron"; +import { app, BrowserWindow, dialog, screen } from "electron"; import pkg from "electron-updater"; +import fs from "fs"; import { getConfigManager } from "./src/config/configInstance.js"; import { setupIpcHandlers } from "./src/ipc/handlers.js"; import { startBackend, stopBackend } from "./src/processes/backend.js"; import { stopPacketSender } from "./src/processes/packetSender.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; +// Disable sandbox for Linux +if (process.platform === "linux") { + try { + const userns = fs + .readFileSync("/proc/sys/kernel/unprivileged_userns_clone", "utf8") + .trim(); + if (userns === "0") { + app.commandLine.appendSwitch("no-sandbox"); + } + } catch (e) {} + + if (process.getuid && process.getuid() === 0) { + app.commandLine.appendSwitch("no-sandbox"); + } +} + // Setup IPC handlers for renderer process communication setupIpcHandlers(); @@ -22,15 +40,22 @@ app.setName("hyperloop-control-station"); // App lifecycle: wait for Electron to be ready 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(); + await startBackend(logWindow); logger.electron.header("Backend process spawned"); } catch (error) { // Start backend already shows these errors @@ -38,7 +63,9 @@ app.whenReady().then(async () => { } // Create main application window - createWindow(); + const mainWindow = createWindow(screenWidth, screenHeight); + mainWindow.maximize(); + logger.electron.header("Main application window created"); // Updater setup diff --git a/electron-app/package.json b/electron-app/package.json index a37d20f36..c5957648d 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", + "ansi-to-html": "^0.7.2", "electron-store": "^11.0.2", "electron-updater": "^6.7.3", "picocolors": "^1.1.1" @@ -106,7 +107,9 @@ "linux": { "target": [ "AppImage", - "deb" + "deb", + "rpm", + "pacman" ], "icon": "icons/512x512.png", "category": "Utility", diff --git a/electron-app/preload.js b/electron-app/preload.js index 761fa8dcf..2bc27909c 100644 --- a/electron-app/preload.js +++ b/electron-app/preload.js @@ -36,4 +36,7 @@ contextBridge.exposeInMainWorld("electronAPI", { importConfig: () => ipcRenderer.invoke("import-config"), // Open folder selection dialog selectFolder: () => ipcRenderer.invoke("select-folder"), + // Receive log message from backend + onLog: (callback) => + ipcRenderer.on("log", (_event, value) => callback(value)), }); diff --git a/electron-app/src/ipc/handlers.js b/electron-app/src/ipc/handlers.js index 2aefe2bf4..79d8c1075 100644 --- a/electron-app/src/ipc/handlers.js +++ b/electron-app/src/ipc/handlers.js @@ -19,6 +19,7 @@ import { getCurrentView, getMainWindow, loadView, + reloadWindow, } from "../windows/mainWindow.js"; /** @@ -61,7 +62,10 @@ function setupIpcHandlers() { ipcMain.handle("save-config", async (event, config) => { try { await writeConfig(config); - restartBackend(); + await restartBackend(); + + reloadWindow(); + return true; } catch (error) { logger.electron.error("Error saving config:", error); @@ -96,7 +100,10 @@ function setupIpcHandlers() { ipcMain.handle("import-config", async () => { try { await importConfig(); - restartBackend(); + await restartBackend(); + + reloadWindow(); + return true; } catch (error) { logger.electron.error("Error importing config:", error); diff --git a/electron-app/src/log-viewer/index.html b/electron-app/src/log-viewer/index.html new file mode 100644 index 000000000..c9496d24a --- /dev/null +++ b/electron-app/src/log-viewer/index.html @@ -0,0 +1,34 @@ + + + + Backend Logs + + + +
+ + + + diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js index c0436ab79..c2000533d 100644 --- a/electron-app/src/menu/menu.js +++ b/electron-app/src/menu/menu.js @@ -30,7 +30,11 @@ function createMenu(mainWindow) { { label: "Reload", accelerator: "CmdOrCtrl+R", - click: () => mainWindow.reload(), + click: (_, browserWindow) => { + if (browserWindow) { + browserWindow.reload(); + } + }, }, { type: "separator" }, { @@ -61,7 +65,11 @@ function createMenu(mainWindow) { { label: "Toggle DevTools", accelerator: "F12", - click: () => mainWindow.webContents.toggleDevTools(), + click: (_, browserWindow) => { + if (browserWindow) { + browserWindow.webContents.toggleDevTools(); + } + }, }, ], }, @@ -83,7 +91,7 @@ function createMenu(mainWindow) { } const packetSenderProcess = getPacketSenderProcess(); if (!packetSenderProcess || packetSenderProcess.killed) { - startPacketSender(["random"]); + startPacketSender(); } }, }, @@ -118,7 +126,7 @@ function createMenu(mainWindow) { ]; const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); + return menu; } export { createMenu }; diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index 1e215619d..3c2c9720d 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -4,6 +4,7 @@ * Handles starting, stopping, and restarting the backend process with proper error handling and logging. */ +import AnsiToHtml from "ansi-to-html"; import { spawn } from "child_process"; import { app, dialog } from "electron"; import fs from "fs"; @@ -15,12 +16,18 @@ import { getUserConfigPath, } from "../utils/paths.js"; +// Create ANSI to HTML converter +const convert = new AnsiToHtml(); + // Get the application root path const appPath = getAppPath(); // Store the backend process instance let backendProcess = null; +// Common log window instance for all backend processes +let storedLogWindow = null; + // Store error messages (keep last 10 lines to avoid memory issues) let lastBackendError = null; @@ -30,7 +37,13 @@ let lastBackendError = null; * @example * startBackend(); */ -function startBackend() { +async function startBackend(logWindow = null) { + if (logWindow) { + storedLogWindow = logWindow; + } + + const currentLogWindow = logWindow || storedLogWindow; + return new Promise((resolve, reject) => { // Get paths for binary and config const backendBin = getBinaryPath("backend"); @@ -63,6 +76,12 @@ function startBackend() { // Log stdout output from backend backendProcess.stdout.on("data", (data) => { logger.backend.info(`${data.toString().trim()}`); + + // Send log message to log window + if (currentLogWindow && !currentLogWindow.isDestroyed()) { + const htmlData = convert.toHtml(data.toString().trim()); + currentLogWindow.webContents.send("log", htmlData); + } }); // Capture stderr output (where Go errors/panics are written) @@ -71,6 +90,12 @@ function startBackend() { logger.backend.error(errorMsg); // Store the last error message lastBackendError = errorMsg; + + // Send error message to log window + if (currentLogWindow && !currentLogWindow.isDestroyed()) { + const htmlError = convert.toHtml(errorMsg); + currentLogWindow.webContents.send("log", htmlError); + } }); // Handle spawn errors @@ -86,7 +111,7 @@ function startBackend() { // If the backend didn't fail in this period of time, resolve the promise setTimeout(() => { resolve(backendProcess); - }, 1000); + }, 2000); // Handle process exit backendProcess.on("close", (code) => { @@ -111,20 +136,46 @@ function startBackend() { } /** - * Stops the backend process by sending a SIGTERM signal. + * Stops the backend process by sending a SIGTERM and std.in.end() signal. + * If the process does not exit gracefully after defined time, it will be force killed. * @returns {void} * @example * stopBackend(); */ -function stopBackend() { - // Only stop if process exists and is still running - if (backendProcess && !backendProcess.killed) { - logger.backend.info("Stopping backend..."); - // Send termination signal - backendProcess.kill("SIGTERM"); - // Clear the process reference - backendProcess = null; - } +async function stopBackend() { + return new Promise((resolve, reject) => { + const localBackendProcess = backendProcess; + + // Only stop if process exists and is still running + if (localBackendProcess && !localBackendProcess.killed) { + logger.backend.info("Stopping backend..."); + + localBackendProcess.once("close", () => { + // Clear the process reference + if (localBackendProcess === backendProcess) { + backendProcess = null; + } + resolve(); + }); + + localBackendProcess.kill("SIGTERM"); + localBackendProcess.stdin.end(); + + const fallbackTimer = setTimeout(() => { + if (localBackendProcess && !localBackendProcess.killed) { + logger.backend.warning( + "Backend did not exit gracefully, force killing..." + ); + localBackendProcess.kill("SIGKILL"); + } + }, 2000); + + fallbackTimer.unref(); + } else { + logger.backend.warning("Backend process not found, skipping stop..."); + resolve(); + } + }); } /** @@ -133,11 +184,18 @@ function stopBackend() { * @example * restartBackend(); */ -function restartBackend() { +async function restartBackend() { // Stop current process first - stopBackend(); + await stopBackend(); + // Start a new process - startBackend(); + try { + await startBackend(); + logger.electron.info("Backend restarted successfully"); + } catch (error) { + logger.electron.error("Failed to restart backend:", error); + throw error; // Let the IPC handler know it failed + } } export { restartBackend, startBackend, stopBackend }; diff --git a/electron-app/src/processes/packetSender.js b/electron-app/src/processes/packetSender.js index e6efc5cf9..74b653fa1 100644 --- a/electron-app/src/processes/packetSender.js +++ b/electron-app/src/processes/packetSender.js @@ -12,16 +12,18 @@ import { getBinaryPath } from "../utils/paths.js"; // Store the packet sender process instance let packetSenderProcess = null; +// Default arguments for packet sender +const DEFAULT_ARGS = ["1", "1"]; // Send mode, Random type + /** * Starts the packet sender process by spawning the packet-sender binary with optional arguments. * Sets up event handlers for stdout and stderr with appropriate logging. * @param {string[]} [args=[]] - Optional array of command-line arguments to pass to the packet-sender binary. * @returns {import("child_process").ChildProcessWithoutNullStreams | null} The spawned ChildProcess object, or null if the binary is not found. * @example - * const process = startPacketSender(["--port", "8080"]); * startPacketSender(); */ -function startPacketSender(args = []) { +function startPacketSender(args = DEFAULT_ARGS) { // Get the path to the packet-sender binary const packetSenderBin = getBinaryPath("packet-sender"); @@ -44,6 +46,14 @@ function startPacketSender(args = []) { // Log stdout output from packet sender process.stdout.on("data", (data) => { logger.packetSender.info(`${data.toString().trim()}`); + + if (data.toString().includes("1) Send packets")) { + process.stdin.write("1\n"); + } + + if (data.toString().includes("1) Random")) { + process.stdin.write("1\n"); + } }); // Log stderr output as errors @@ -90,7 +100,7 @@ function restartPacketSender() { // Wait before starting new process to ensure cleanup setTimeout(() => { // Start with help arguments - startPacketSender(["random"]); + startPacketSender(); }, 500); } } diff --git a/electron-app/src/utils/paths.js b/electron-app/src/utils/paths.js index f7bf70033..1d44ceb39 100644 --- a/electron-app/src/utils/paths.js +++ b/electron-app/src/utils/paths.js @@ -41,13 +41,6 @@ function getBinaryPath(name) { const arch = process.arch; const ext = platform === "win32" ? ".exe" : ""; - if (name === "packet-sender") { - if (!app.isPackaged) { - return path.join(getAppPath(), "binaries", `${name}${ext}`); - } - return path.join(process.resourcesPath, "binaries", `${name}${ext}`); - } - const goosMap = { win32: "windows", darwin: "darwin", diff --git a/electron-app/src/windows/README.md b/electron-app/src/windows/README.md index bb4e4ec5a..7698de6aa 100644 --- a/electron-app/src/windows/README.md +++ b/electron-app/src/windows/README.md @@ -4,24 +4,25 @@ Window management module for the Electron application. Handles creation, configu ## Overview -Manages the primary Electron `BrowserWindow` instance and provides functionality for switching between different application views (Competition View and Testing View). +Manages the primary and logs Electron `BrowserWindow` instances and provides functionality for switching between different application views (Competition View and Testing View). ## Files +- `logWindow.js` - Backend logs and messages - `mainWindow.js` - Main window creation and management ## Window Configuration - **Default Size**: 1920x1080 pixels -- **Minimum Size**: 1280x720 pixels +- **Minimum Size**: 800x600 pixels - **Title**: "Hyperloop Control Station" - **Background Color**: `#1a1a1a` (dark theme) - **Security**: Context isolation enabled, node integration disabled ## Available Views -- **Ethernet View** (default) - Testing interface, loads from `renderer/ethernet-view/index.html` -- **Control Station** - Competition interface, loads from `renderer/control-station/index.html` +- **Testing View** (default) - Testing interface, loads from `renderer/testing-view/index.html` +- **Competition View** - Competition interface, loads from `renderer/competition-view/index.html` ## Functions @@ -29,6 +30,10 @@ Manages the primary Electron `BrowserWindow` instance and provides functionality Creates and initializes the main application window. Loads default view, sets up menu, and opens DevTools in development mode. +### `reloadWindow()` + +Reloads main window. + ### `loadView(view)` Switches the main window to display a different view. Updates window title and validates view file exists. diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js new file mode 100644 index 000000000..4c1c0e27e --- /dev/null +++ b/electron-app/src/windows/logWindow.js @@ -0,0 +1,27 @@ +import { BrowserWindow } from "electron"; +import path from "path"; + +import { getAppPath } from "../utils/paths.js"; + +// Get the application root path +const appPath = getAppPath(); + +export const createLogWindow = (screenWidth, screenHeight) => { + const logWindow = new BrowserWindow({ + x: Math.floor(screenWidth * 0.65), + y: 0, + width: Math.floor(screenWidth * 0.35), + height: screenHeight, + title: "Backend Logs", + webPreferences: { + preload: path.join(appPath, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + const logFilePath = path.join(appPath, "src", "log-viewer", "index.html"); + logWindow.loadFile(logFilePath); + + return logWindow; +}; diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index 259f8dc79..ae5fc29b2 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -24,13 +24,15 @@ let currentView = "testing-view"; * @example * createWindow(); */ -function createWindow() { +function createWindow(screenWidth, screenHeight) { // Create new browser window with configuration mainWindow = new BrowserWindow({ - width: 1920, - height: 1080, - minWidth: 1280, - minHeight: 720, + x: 0, + y: 0, + width: screenWidth, + height: screenHeight, + minWidth: 800, + minHeight: 600, webPreferences: { // Path to preload script for secure IPC preload: path.join(appPath, "preload.js"), @@ -49,7 +51,8 @@ function createWindow() { loadView(currentView); // Create application menu - createMenu(mainWindow); + const menu = createMenu(mainWindow); + mainWindow.setMenu(menu); // Open DevTools in development mode if (!app.isPackaged) { @@ -60,6 +63,8 @@ function createWindow() { mainWindow.on("closed", () => { mainWindow = null; }); + + return mainWindow; } /** @@ -93,6 +98,18 @@ function loadView(view) { } } +/** + * Reloads the main window. + * @returns {void} + * @example + * reloadWindow(); + */ +function reloadWindow() { + if (mainWindow) { + mainWindow.reload(); + } +} + /** * Returns the name of the currently loaded view. * @returns {string} The current view name (e.g., "ethernet-view", "control-station"). @@ -119,4 +136,4 @@ function getMainWindow() { return mainWindow; } -export { createWindow, getCurrentView, getMainWindow, loadView }; +export { createWindow, getCurrentView, getMainWindow, loadView, reloadWindow }; diff --git a/frontend/frontend-kit/ui/README.md b/frontend/frontend-kit/ui/README.md new file mode 100644 index 000000000..713357c0a --- /dev/null +++ b/frontend/frontend-kit/ui/README.md @@ -0,0 +1,40 @@ +# UI Package - Frontend Kit + +This package is the main UI and React shared component library for the Hyperloop Control Station. It provides reusable, styled components and hooks built on top of **React**, **Shadcn/UI**, and **Tailwind CSS**. + +## Project Guidelines + +### React-Only Logic + +> This package is specifically for **UI and React-related components**. +> +> - If your logic requires React hooks (`useState`, `useEffect`, etc.) or TSX, it belongs here. +> - **If your logic does NOT need React** (e.g., websocket connections), it should be implemented in the `@workspace/core` package. This keeps the codebase clean and allows for better reuse across different environments. + +--- + +## Icon Management + +We use a custom Rust-based tool, **icons-master**, to manage our Lucide icon exports. This tool helps keep our icons organized by category and ensures we don't have duplicate exports. + +> **⚠️ Windows Support Only** +> +> The `icons-master` CLI currently only supports **Windows**. If you are on macOS or Linux, you must manually update the `.ts` files in `src/icons/`. + +### Usage + +Here are the scripts you can run: + +```bash +# Install dependencies +pnpm install + +# Run linter +pnpm lint + +# Add a new icon from Lucide (Example: pnpm icon:add arrow-up src/icons) +pnpm icon:add src/icons + +# Remove an existing icon export (Example: pnpm icon:remove arrow-up src/icons) +pnpm icon:remove src/icons +``` diff --git a/frontend/frontend-kit/ui/package.json b/frontend/frontend-kit/ui/package.json index 9c26d70ce..78d4fb424 100644 --- a/frontend/frontend-kit/ui/package.json +++ b/frontend/frontend-kit/ui/package.json @@ -5,7 +5,9 @@ "private": true, "scripts": { "preinstall": "npx only-allow pnpm", - "lint": "eslint ." + "lint": "eslint .", + "icon:add": "icons-master add", + "icon:remove": "icons-master remove" }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", @@ -34,6 +36,7 @@ "zustand": "^5.0.11" }, "devDependencies": { + "@maximka76667/icons-master": "^1.0.1", "@tailwindcss/postcss": "^4.1.18", "@turbo/gen": "^2.8.3", "@types/node": "^25.2.0", diff --git a/frontend/frontend-kit/ui/src/icons/arrows.ts b/frontend/frontend-kit/ui/src/icons/arrows.ts index 5f6067552..7c8bbb5dc 100644 --- a/frontend/frontend-kit/ui/src/icons/arrows.ts +++ b/frontend/frontend-kit/ui/src/icons/arrows.ts @@ -2,8 +2,8 @@ export { ChevronDown, ChevronLeft, ChevronRight, - ChevronsUpDown, ChevronUp, + ChevronsUpDown, Play, RefreshCw, TrendingDown, diff --git a/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx index eb3980a2f..8ece18567 100644 --- a/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx +++ b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx @@ -20,21 +20,27 @@ export const MultiCheckboxField = ({
- {field.options?.map((opt) => ( -
- handleToggle(opt, !!checked)} - /> - -
- ))} + {!field.options || field.options.length === 0 ? ( +

+ No boards detected. Connect to the backend to see available options. +

+ ) : ( + field.options?.map((opt) => ( +
+ handleToggle(opt, !!checked)} + /> + +
+ )) + )}
); diff --git a/frontend/testing-view/src/components/settings/SettingsForm.tsx b/frontend/testing-view/src/components/settings/SettingsForm.tsx index 96357b57e..c69512c55 100644 --- a/frontend/testing-view/src/components/settings/SettingsForm.tsx +++ b/frontend/testing-view/src/components/settings/SettingsForm.tsx @@ -1,5 +1,7 @@ import { get, set } from "lodash"; -import { SETTINGS_SCHEMA } from "../../constants/settingsSchema"; +import { useMemo } from "react"; +import { getSettingsSchema } from "../../constants/settingsSchema"; +import { useStore } from "../../store/store"; import type { ConfigData } from "../../types/common/config"; import type { SettingField } from "../../types/common/settings"; import { BooleanField } from "./BooleanField"; @@ -23,6 +25,9 @@ export const SettingsForm = ({ config, onChange }: SettingsFormProps) => { onChange(nextConfig); }; + const boards = useStore((s) => s.boards); + const schema = useMemo(() => getSettingsSchema(boards), [boards]); + const renderField = (field: SettingField) => { const currentValue = get(config, field.path); @@ -94,9 +99,9 @@ export const SettingsForm = ({ config, onChange }: SettingsFormProps) => { return (
- {SETTINGS_SCHEMA.map((section) => ( + {schema.map((section) => (
-

+

{section.title}

diff --git a/frontend/testing-view/src/constants/boards.ts b/frontend/testing-view/src/constants/boards.ts deleted file mode 100644 index 68a8a4127..000000000 --- a/frontend/testing-view/src/constants/boards.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** List of names of available boards. */ -export const BOARD_NAMES: readonly string[] = [ - "BCU", // Battery Control Unit - "PCU", // Propulsion Control Unit - "LCU", // Levitation Control Unit - "HVSCU", // High Voltage System Control Unit - "BMSL", // Battery Management System Level - "VCU", // Vehicle Control Unit - "HVSCU-Cabinet", // High Voltage System Control Unit Cabinet -]; diff --git a/frontend/testing-view/src/constants/settingsSchema.ts b/frontend/testing-view/src/constants/settingsSchema.ts index eecc75f85..28561ea8c 100644 --- a/frontend/testing-view/src/constants/settingsSchema.ts +++ b/frontend/testing-view/src/constants/settingsSchema.ts @@ -1,8 +1,8 @@ import type { SettingsSection } from "../types/common/settings"; -import { BOARD_NAMES } from "./boards"; +import type { BoardName } from "../types/data/board"; /** Settings form is generated from this schema. */ -export const SETTINGS_SCHEMA: SettingsSection[] = [ +export const getSettingsSchema = (boards: BoardName[]): SettingsSection[] => [ { title: "Vehicle Configuration", fields: [ @@ -10,7 +10,7 @@ export const SETTINGS_SCHEMA: SettingsSection[] = [ label: "Boards", path: "vehicle.boards", type: "multi-checkbox", - options: BOARD_NAMES as string[], + options: boards, }, ], }, diff --git a/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx index 747f80aff..05f2a2467 100644 --- a/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx @@ -22,7 +22,7 @@ export const FilterCategoryItem = ({ category }: FilterCategoryItemProps) => { const toggleCategoryFilter = useStore((s) => s.toggleCategoryFilter); const toggleItemFilter = useStore((s) => s.toggleItemFilter); - const items = useStore((s) => s.getCatalog(scope)[category]); + const items = useStore((s) => s.getCatalog(scope)[category]) || []; const totalItems = items.length; const selectedIds = useStore( @@ -61,7 +61,7 @@ export const FilterCategoryItem = ({ category }: FilterCategoryItemProps) => {
- {items.map((item) => ( + {items?.map((item) => ( { const { isOpen, scope } = useStore((s) => s.filterDialog); const close = useStore((s) => s.closeFilterDialog); + const boards = useStore((s) => s.boards); + const activeFilters = useStore(useShallow((s) => s.getActiveFilters(scope))); + const clearFilters = useStore((s) => s.clearFilters); const selectAllFilters = useStore((s) => s.selectAllFilters); if (!scope) return null; + const extraBoards = detectExtraBoards(activeFilters, boards); + return ( { onClose={close} onClearAll={() => clearFilters(scope)} onSelectAll={() => selectAllFilters(scope)} - categories={BOARD_NAMES} + categories={boards} + extraCategories={extraBoards} FilterCategoryComponent={FilterCategoryItem} /> ); diff --git a/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx index 6c72ba88c..f211b2b33 100644 --- a/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx @@ -6,6 +6,7 @@ import { DialogHeader, DialogTitle, } from "@workspace/ui"; +import { AlertTriangle } from "@workspace/ui/icons"; import { type ComponentType } from "react"; import type { BoardName } from "../../../types/data/board"; @@ -17,6 +18,7 @@ interface FilterDialogProps { onClearAll: () => void; onSelectAll: () => void; categories: readonly BoardName[]; + extraCategories: readonly BoardName[]; FilterCategoryComponent: ComponentType<{ category: BoardName }>; } @@ -28,11 +30,13 @@ export const FilterDialog = ({ onClearAll, onSelectAll, categories, + extraCategories, FilterCategoryComponent, }: FilterDialogProps) => { + console.log(extraCategories); return ( - + {title} {description && {description}} @@ -47,6 +51,30 @@ export const FilterDialog = ({
+ {extraCategories.length > 0 && ( +
+
+ + Stale filters detected +
+

+ The following boards are in your saved filters but not in the + current configuration:{" "} + + {extraCategories.join(", ")} + +

+ +
+ )} +
{categories.map((category) => ( diff --git a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts index 1bcc4cc6d..58cefaccc 100644 --- a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts +++ b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts @@ -38,7 +38,7 @@ export interface FilteringSlice { workspaceFilters: Record; initializeWorkspaceFilters: () => void; updateFilters: (scope: FilterScope, filters: TabFilter) => void; - getActiveFilters: (scope: FilterScope) => TabFilter | undefined; + getActiveFilters: (scope: FilterScope | null) => TabFilter | undefined; /** Filter Actions */ selectAllFilters: (scope: FilterScope) => void; @@ -135,7 +135,7 @@ export const createFilteringSlice: StateCreator< const currentWorkspaceFilters = get().workspaceFilters[workspaceId] || {}; const currentTabFilter = - currentWorkspaceFilters[scope] || createEmptyFilter(); + currentWorkspaceFilters[scope] || createEmptyFilter(get().boards); const currentCategoryIds = currentTabFilter[category] || []; @@ -157,13 +157,13 @@ export const createFilteringSlice: StateCreator< const items = get().getCatalog(scope); - const fullFilter = createFullFilter(items); + const fullFilter = createFullFilter(items, get().boards); get().updateFilters(scope, fullFilter); }, clearFilters: (scope) => { const workspaceId = get().getActiveWorkspaceId(); if (!workspaceId) return; - const emptyFilter = createEmptyFilter(); + const emptyFilter = createEmptyFilter(get().boards); get().updateFilters(scope, emptyFilter); }, toggleCategoryFilter: (scope, category, checked) => { @@ -173,7 +173,8 @@ export const createFilteringSlice: StateCreator< const catalog = get().getCatalog(scope); const currentFilters = - get().workspaceFilters[workspaceId]?.[scope] || createEmptyFilter(); + get().workspaceFilters[workspaceId]?.[scope] || + createEmptyFilter(get().boards); const newItems = checked ? catalog?.[category]?.map((item) => item.id) || [] @@ -196,9 +197,9 @@ export const createFilteringSlice: StateCreator< if (Object.keys(currentFilters).length === 0) { set({ workspaceFilters: generateInitialFilters({ - commands: createFullFilter(commands), - telemetry: createFullFilter(telemetry), - logs: createFullFilter(telemetry), + commands: createFullFilter(commands, get().boards), + telemetry: createFullFilter(telemetry, get().boards), + logs: createFullFilter(telemetry, get().boards), }), }); } @@ -228,6 +229,7 @@ export const createFilteringSlice: StateCreator< // Helper getters getActiveFilters: (scope) => { const id = get().getActiveWorkspaceId(); + if (!scope) return {}; return id ? get().workspaceFilters[id]?.[scope] : undefined; }, diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx index 60b1a0876..59b3dbaff 100644 --- a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx @@ -1,15 +1,19 @@ -import { BOARD_NAMES } from "../../../../../constants/boards"; +import { useStore } from "../../../../../store/store"; import type { CommandCatalogItem } from "../../../../../types/data/commandCatalogItem"; import { CommandItem } from "../tabs/commands/CommandItem"; import { Tab } from "../tabs/Tab"; -export const CommandsSection = () => ( - ( - - )} - /> -); +export const CommandsSection = () => { + const boards = useStore((s) => s.boards); + + return ( + ( + + )} + /> + ); +}; diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx index 4798da54d..3cb19cfce 100644 --- a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx @@ -1,16 +1,20 @@ -import { BOARD_NAMES } from "../../../../../constants/boards"; +import { useStore } from "../../../../../store/store"; import type { TelemetryCatalogItem } from "../../../../../types/data/telemetryCatalogItem"; import { Tab } from "../tabs/Tab"; import { TelemetryItem } from "../tabs/telemetry/TelemetryItem"; -export const TelemetrySection = () => ( - ( - - )} - virtualized - /> -); +export const TelemetrySection = () => { + const boards = useStore((s) => s.boards); + + return ( + ( + + )} + virtualized + /> + ); +}; diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx index 08730d14c..e93c10110 100644 --- a/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx @@ -1,5 +1,7 @@ import { Button } from "@workspace/ui"; -import { ListFilterPlus } from "@workspace/ui/icons"; +import { AlertTriangle, ListFilterPlus } from "@workspace/ui/icons"; +import { useShallow } from "zustand/shallow"; +import { detectExtraBoards } from "../../../../../lib/utils"; import { useStore } from "../../../../../store/store"; import type { SidebarTab } from "../../../types/sidebar"; @@ -13,23 +15,40 @@ export const TabHeader = ({ title, scope }: TabHeaderProps) => { const totalCount = useStore((state) => state.getTotalCount(scope)); const filteredCount = useStore((state) => state.getFilteredCount(scope)); + const boards = useStore((s) => s.boards); + const activeFilters = useStore(useShallow((s) => s.getActiveFilters(scope))); + const extraBoards = detectExtraBoards(activeFilters, boards); + return ( -
-

- {title} - - {filteredCount} / {totalCount} - -

- +
+
+

+ {title} + + {filteredCount} / {totalCount} + +

+ +
+ + {/* Warning for stale boards */} + {extraBoards.length > 0 && ( +
+ + {extraBoards.length} stale board(s) affecting counts +
+ )}
); }; diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts index ea09a6844..28a1be861 100644 --- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -63,9 +63,9 @@ export const createWorkspacesSlice: StateCreator< const newWorkspaceFilters = { ...state.workspaceFilters, [newWorkspaceId]: { - commands: createFullFilter(commands), - telemetry: createFullFilter(telemetry), - logs: createFullFilter(telemetry), + commands: createFullFilter(commands, get().boards), + telemetry: createFullFilter(telemetry, get().boards), + logs: createFullFilter(telemetry, get().boards), }, }; diff --git a/frontend/testing-view/src/hooks/useAppMode.ts b/frontend/testing-view/src/hooks/useAppMode.ts index e390a0cc4..4e279b3be 100644 --- a/frontend/testing-view/src/hooks/useAppMode.ts +++ b/frontend/testing-view/src/hooks/useAppMode.ts @@ -31,7 +31,6 @@ export function useAppMode( // logger.testingView.log("[DEBUG] isDev", isDev); // logger.testingView.log("[DEBUG] isLoading", isLoading); // logger.testingView.log("[DEBUG] hasData", hasData); - // logger.testingView.log("[DEBUG] backendConnected", backendConnected); // logger.testingView.log("[DEBUG] hasError", hasError); if (isLoading || isRestarting) return "loading"; diff --git a/frontend/testing-view/src/hooks/useBoardData.ts b/frontend/testing-view/src/hooks/useBoardData.ts index 22ff3d8f9..3f40f4cfc 100644 --- a/frontend/testing-view/src/hooks/useBoardData.ts +++ b/frontend/testing-view/src/hooks/useBoardData.ts @@ -90,6 +90,8 @@ export function useBoardData( logger.testingView.log("[useBoardData] Commands data processed"); + console.log("availableBoards", availableBoards); + return { telemetryCatalog: telemetryCatalogResult, commandsCatalog: commandsCatalogResult, diff --git a/frontend/testing-view/src/hooks/useTransformedBoards.ts b/frontend/testing-view/src/hooks/useTransformedBoards.ts index c8fc04c40..aa7cda62b 100644 --- a/frontend/testing-view/src/hooks/useTransformedBoards.ts +++ b/frontend/testing-view/src/hooks/useTransformedBoards.ts @@ -12,6 +12,7 @@ export function useTransformedBoards( const setTelemetryCatalog = useStore((s) => s.setTelemetryCatalog); const setCommandsCatalog = useStore((s) => s.setCommandsCatalog); + const setBoards = useStore((s) => s.setBoards); const initializeWorkspaceFilters = useStore( (s) => s.initializeWorkspaceFilters, ); @@ -25,7 +26,14 @@ export function useTransformedBoards( setTelemetryCatalog(transformedBoards.telemetryCatalog); setCommandsCatalog(transformedBoards.commandsCatalog); - initializeWorkspaceFilters(); + setBoards(Array.from(transformedBoards.boards)); + + const hasTelemetryData = + Object.keys(transformedBoards.telemetryCatalog).length > 0; + const hasCommandsData = + Object.keys(transformedBoards.commandsCatalog).length > 0; + + if (hasTelemetryData && hasCommandsData) initializeWorkspaceFilters(); }, [ transformedBoards, setTelemetryCatalog, diff --git a/frontend/testing-view/src/lib/utils.test.ts b/frontend/testing-view/src/lib/utils.test.ts index 947b6c5c7..fa667af7a 100644 --- a/frontend/testing-view/src/lib/utils.test.ts +++ b/frontend/testing-view/src/lib/utils.test.ts @@ -105,7 +105,17 @@ describe("getTypeBadgeClass", () => { describe("emptyFilter", () => { it("should return the correct empty filter", () => { - expect(createEmptyFilter()).toStrictEqual({ + const boards = [ + "BCU", + "PCU", + "LCU", + "HVSCU", + "HVSCU-Cabinet", + "BMSL", + "VCU", + ]; + + expect(createEmptyFilter(boards)).toStrictEqual({ BCU: [], PCU: [], LCU: [], @@ -133,7 +143,17 @@ describe("fullFilter", () => { VCU: [], }; - expect(createFullFilter(testDataSource)).toStrictEqual({ + const boards = [ + "BCU", + "PCU", + "LCU", + "HVSCU", + "HVSCU-Cabinet", + "BMSL", + "VCU", + ]; + + expect(createFullFilter(testDataSource, boards)).toStrictEqual({ BCU: [1], PCU: [2], LCU: [3], diff --git a/frontend/testing-view/src/lib/utils.ts b/frontend/testing-view/src/lib/utils.ts index fe0306338..0df30e9c3 100644 --- a/frontend/testing-view/src/lib/utils.ts +++ b/frontend/testing-view/src/lib/utils.ts @@ -1,5 +1,4 @@ import { ACRONYMS } from "../constants/acronyms"; -import { BOARD_NAMES } from "../constants/boards"; import { variablesBadgeClasses } from "../constants/variablesBadgeClasses"; import type { FilterScope, @@ -29,8 +28,8 @@ export const generateInitialFilters = ( ); }; -export const createEmptyFilter = (): TabFilter => { - return BOARD_NAMES.reduce((acc, category) => { +export const createEmptyFilter = (boards: BoardName[]): TabFilter => { + return boards.reduce((acc, category) => { acc[category] = []; return acc; }, {} as TabFilter); @@ -38,8 +37,9 @@ export const createEmptyFilter = (): TabFilter => { export const createFullFilter = ( dataSource: Record, + boards: BoardName[], ): TabFilter => { - return BOARD_NAMES.reduce((acc, category) => { + return boards.reduce((acc, category) => { acc[category] = dataSource[category]?.map((item) => item.id) || []; return acc; }, {} as TabFilter); @@ -119,3 +119,11 @@ export const formatTimestamp = (ts: MessageTimestamp) => { if (!ts) return "00:00:00"; return `${ts.hour.toString().padStart(2, "0")}:${ts.minute.toString().padStart(2, "0")}:${ts.second.toString().padStart(2, "0")}`; }; + +export const detectExtraBoards = ( + activeFilters: TabFilter | undefined, + boards: BoardName[], +) => + Object.keys(activeFilters || {}).filter( + (key) => !boards.includes(key), + ) as BoardName[]; diff --git a/frontend/testing-view/src/store/slices/catalogSlice.ts b/frontend/testing-view/src/store/slices/catalogSlice.ts index e2e60017f..dcd365534 100644 --- a/frontend/testing-view/src/store/slices/catalogSlice.ts +++ b/frontend/testing-view/src/store/slices/catalogSlice.ts @@ -15,6 +15,10 @@ export interface CatalogSlice { setTelemetryCatalog: ( telemetryCatalog: Record, ) => void; + + // Boards + boards: BoardName[]; + setBoards: (boards: BoardName[]) => void; } export const createCatalogSlice: StateCreator = ( @@ -24,4 +28,6 @@ export const createCatalogSlice: StateCreator = ( telemetryCatalog: {} as Record, setCommandsCatalog: (commandsCatalog) => set({ commandsCatalog }), setTelemetryCatalog: (telemetryCatalog) => set({ telemetryCatalog }), + boards: [] as BoardName[], + setBoards: (boards) => set({ boards }), }); diff --git a/go.work b/go.work index 521811c76..957518330 100644 --- a/go.work +++ b/go.work @@ -2,4 +2,5 @@ go 1.23.1 use ( ./backend + ./packet-sender ) diff --git a/packet-sender/main.go b/packet-sender/main.go index 9a52ccb99..f748519f3 100644 --- a/packet-sender/main.go +++ b/packet-sender/main.go @@ -7,8 +7,6 @@ import ( boardpkg "packet_sender/pkg/board" "packet_sender/pkg/listener" "packet_sender/pkg/sender" - "path" - "path/filepath" "strings" adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" @@ -66,11 +64,13 @@ func getConn(lip string, lport uint16, rip string, rport uint16) *net.UDPConn { // getADJ loads the same ADJ used by backend directly from backend/cmd/adj. func getADJ() adj_module.ADJ { - adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj")) - if err != nil { - log.Fatalf("Failed to resolve ADJ path: %v", err) - } - adj_module.RepoPath = adjPath + string(filepath.Separator) + // adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj")) + // if err != nil { + // log.Fatalf("Failed to resolve ADJ path: %v", err) + // } + // adj_module.RepoPath = adjPath + string(filepath.Separator) + + // Uses the same ADJ RepoPath as the backend by default adj, err := adj_module.NewADJ("") if err != nil { diff --git a/packet-sender/package.json b/packet-sender/package.json new file mode 100644 index 000000000..d96551e08 --- /dev/null +++ b/packet-sender/package.json @@ -0,0 +1,12 @@ +{ + "name": "packet-sender", + "version": "1.0.0", + "private": true, + "author": "Hyperloop UPV Team", + "license": "MIT", + "scripts": { + "build": "go build -o packet-sender main.go", + "build:ci": "go build", + "test": "go test ./..." + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9698bd788..0597f9802 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@iarna/toml': specifier: ^2.2.5 version: 2.2.5 + ansi-to-html: + specifier: ^0.7.2 + version: 0.7.2 electron-store: specifier: ^11.0.2 version: 11.0.2 @@ -192,6 +195,9 @@ importers: specifier: ^5.0.11 version: 5.0.11(@types/react@19.2.11)(immer@11.1.0)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: + '@maximka76667/icons-master': + specifier: ^1.0.1 + version: 1.0.1 '@tailwindcss/postcss': specifier: ^4.1.18 version: 4.1.18 @@ -753,6 +759,10 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} + '@maximka76667/icons-master@1.0.1': + resolution: {integrity: sha512-2mQG0k3p3c2b8KctuH/1KGgy0nlBMTyzC0hguEDNNA3bYzwiehwGJ63bspbQvAbNHxENJ8YhdJFvWX4tDK+W/g==} + hasBin: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1805,6 +1815,11 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansi-to-html@0.7.2: + resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==} + engines: {node: '>=8.0.0'} + hasBin: true + app-builder-bin@4.0.0: resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} @@ -2392,6 +2407,9 @@ packages: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -4233,6 +4251,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} @@ -5178,6 +5197,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@maximka76667/icons-master@1.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6218,6 +6239,10 @@ snapshots: ansi-styles@6.2.3: {} + ansi-to-html@0.7.2: + dependencies: + entities: 2.2.0 + app-builder-bin@4.0.0: {} app-builder-bin@5.0.0-alpha.12: {} @@ -7045,6 +7070,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@2.2.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {}