From c4caf31695d247da5f786709fd3b01d545e953db Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:56:43 +0100 Subject: [PATCH 01/29] feat: add .gitignore for packet-sender --- packet-sender/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 packet-sender/.gitignore diff --git a/packet-sender/.gitignore b/packet-sender/.gitignore new file mode 100644 index 000000000..6d23150b2 --- /dev/null +++ b/packet-sender/.gitignore @@ -0,0 +1,2 @@ +# Rust target directory +/target/ \ No newline at end of file From 708f32713813ce90732aa0d7c50ed35fca671402 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:58:38 +0100 Subject: [PATCH 02/29] feat: add no-sandbox fix for linux --- electron-app/main.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/electron-app/main.js b/electron-app/main.js index ef5841b58..fd7a0a40d 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -15,6 +15,22 @@ 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(); From a50bdc2059ccb7e4399260bb9cfd29721e286afe Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:46:54 +0100 Subject: [PATCH 03/29] feat: some packet-sender tweaks --- backend/cmd/main.go | 2 +- backend/cmd/orchestrator.go | 2 +- backend/cmd/setup_transport.go | 2 +- backend/cmd/setup_vehicle.go | 2 +- backend/internal/pod_data/measurement.go | 2 +- backend/internal/pod_data/pod_data.go | 2 +- backend/{internal => pkg}/adj/adj.go | 0 backend/{internal => pkg}/adj/boards.go | 0 backend/{internal => pkg}/adj/git.go | 0 backend/{internal => pkg}/adj/models.go | 0 electron-app/build.mjs | 106 +++++++++------------ electron-app/src/menu/menu.js | 2 +- electron-app/src/processes/packetSender.js | 16 +++- electron-app/src/utils/paths.js | 7 -- go.work | 1 + packet-sender/.gitignore | 2 - packet-sender/main.go | 14 +-- packet-sender/package.json | 13 +++ 18 files changed, 87 insertions(+), 86 deletions(-) rename backend/{internal => pkg}/adj/adj.go (100%) rename backend/{internal => pkg}/adj/boards.go (100%) rename backend/{internal => pkg}/adj/git.go (100%) rename backend/{internal => pkg}/adj/models.go (100%) delete mode 100644 packet-sender/.gitignore create mode 100644 packet-sender/package.json 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/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/src/menu/menu.js b/electron-app/src/menu/menu.js index c0436ab79..64f776da9 100644 --- a/electron-app/src/menu/menu.js +++ b/electron-app/src/menu/menu.js @@ -83,7 +83,7 @@ function createMenu(mainWindow) { } const packetSenderProcess = getPacketSenderProcess(); if (!packetSenderProcess || packetSenderProcess.killed) { - startPacketSender(["random"]); + startPacketSender(); } }, }, 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/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/.gitignore b/packet-sender/.gitignore deleted file mode 100644 index 6d23150b2..000000000 --- a/packet-sender/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Rust target directory -/target/ \ No newline at end of file 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..4842b09d2 --- /dev/null +++ b/packet-sender/package.json @@ -0,0 +1,13 @@ +{ + "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", + "dev": "go run main.go", + "test": "go test ./..." + } +} From 715edba7a9f549d49517e8183602479202a8e108 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:18:14 +0100 Subject: [PATCH 04/29] feat: add global boards state --- .../settings/MultiCheckboxField.tsx | 36 +++++++++++-------- .../src/components/settings/SettingsForm.tsx | 11 ++++-- frontend/testing-view/src/constants/boards.ts | 10 ------ .../src/constants/settingsSchema.ts | 6 ++-- .../components/FilterCategoryItem.tsx | 4 +-- .../filtering/components/FilterController.tsx | 5 +-- .../filtering/store/filteringSlice.ts | 15 ++++---- .../rightSidebar/sections/CommandsSection.tsx | 26 ++++++++------ .../sections/TelemetrySection.tsx | 28 ++++++++------- .../testing-view/src/hooks/useBoardData.ts | 2 ++ .../src/hooks/useTransformedBoards.ts | 10 +++++- frontend/testing-view/src/lib/utils.ts | 8 ++--- .../src/store/slices/catalogSlice.ts | 6 ++++ packet-sender/package.json | 1 - 14 files changed, 97 insertions(+), 71 deletions(-) delete mode 100644 frontend/testing-view/src/constants/boards.ts 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 clearFilters = useStore((s) => s.clearFilters); const selectAllFilters = useStore((s) => s.selectAllFilters); @@ -20,7 +21,7 @@ export const FilterController = () => { onClose={close} onClearAll={() => clearFilters(scope)} onSelectAll={() => selectAllFilters(scope)} - categories={BOARD_NAMES} + categories={boards} FilterCategoryComponent={FilterCategoryItem} /> ); diff --git a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts index 1bcc4cc6d..ecb047379 100644 --- a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts +++ b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts @@ -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), }), }); } 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/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.ts b/frontend/testing-view/src/lib/utils.ts index fe0306338..adbcdfc2f 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); 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/packet-sender/package.json b/packet-sender/package.json index 4842b09d2..d96551e08 100644 --- a/packet-sender/package.json +++ b/packet-sender/package.json @@ -7,7 +7,6 @@ "scripts": { "build": "go build -o packet-sender main.go", "build:ci": "go build", - "dev": "go run main.go", "test": "go test ./..." } } From 8168f40a2d2420879ac3a0e7b9c650b858863108 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:28:37 +0100 Subject: [PATCH 05/29] feat: increase backend resolving time --- electron-app/src/processes/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index 1e215619d..c994a94e3 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -86,7 +86,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) => { From b6e9f00a79ce141cc3e843a34e8a7fc33c302cb7 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:37:34 +0100 Subject: [PATCH 06/29] fix: workflows --- .github/workflows/build.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2564f79db..37d11b4c4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -57,7 +57,6 @@ jobs: - uses: dorny/paths-filter@v3 id: filter with: - ref: "production" filters: | backend: - 'backend/**/*' From eb85a77697d21bc1e1ec7b7b6802f7a6a38d09bb Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:41:31 +0100 Subject: [PATCH 07/29] fix --- .github/workflows/build.yaml | 2 +- .../workspace/store/workspacesSlice.ts | 6 ++--- frontend/testing-view/src/lib/utils.test.ts | 24 +++++++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 37d11b4c4..dbbfce409 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -112,7 +112,7 @@ jobs: with: workflow: build.yaml branch: production - workflow_conclusion: success + workflow_conclusion: completed name: backend-${{ matrix.platform }} path: backend/cmd 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/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], From 47156f5f16bc67f45f3a6128db3cf37aa77b6882 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:54:52 +0100 Subject: [PATCH 08/29] Update build.yaml --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dbbfce409..c6a335ac0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,6 +53,8 @@ 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 From 7ccfe4ba9907591c5ab0f1f09a1abe6327754ca2 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:56:16 +0100 Subject: [PATCH 09/29] check --- .../testing-view/src/features/workspace/store/workspacesSlice.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts index 28a1be861..191ab3c75 100644 --- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -68,6 +68,7 @@ export const createWorkspacesSlice: StateCreator< logs: createFullFilter(telemetry, get().boards), }, }; + // test // Initialize expanded items for the new workspace const newExpandedItems = { From 913fe95a44b5a683694812720229852d20d03d47 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:01:40 +0100 Subject: [PATCH 10/29] Update build.yaml --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c6a335ac0..516ddbdc7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -59,6 +59,7 @@ jobs: - uses: dorny/paths-filter@v3 id: filter with: + base: ${{ github.event.before }} filters: | backend: - 'backend/**/*' From 322c199d92259d52570f0e9b2a9634f9e027d850 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:02:37 +0100 Subject: [PATCH 11/29] fix --- .../testing-view/src/features/workspace/store/workspacesSlice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts index 191ab3c75..28a1be861 100644 --- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -68,7 +68,6 @@ export const createWorkspacesSlice: StateCreator< logs: createFullFilter(telemetry, get().boards), }, }; - // test // Initialize expanded items for the new workspace const newExpandedItems = { From c55f0ac82508e3d521a45e01554d24d0a075ac5b Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:43:31 +0100 Subject: [PATCH 12/29] feat: add building to frontend testing --- .github/workflows/frontend-tests.yaml | 3 +++ 1 file changed, 3 insertions(+) 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/**" From 225cbe5a2c23df9abadea0b8b44aa34a146ad8b9 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:54:13 +0100 Subject: [PATCH 13/29] feat: increase backend resolving time --- electron-app/src/processes/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index c994a94e3..747336e3d 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -86,7 +86,7 @@ function startBackend() { // If the backend didn't fail in this period of time, resolve the promise setTimeout(() => { resolve(backendProcess); - }, 2000); + }, 4000); // Handle process exit backendProcess.on("close", (code) => { From 6840db855b67b7bf8560122fe930c48b1d800689 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:11:57 +0100 Subject: [PATCH 14/29] feat: include rpm and pacman distributives --- electron-app/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electron-app/package.json b/electron-app/package.json index a37d20f36..30344aa16 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -106,7 +106,9 @@ "linux": { "target": [ "AppImage", - "deb" + "deb", + "rpm", + "pacman" ], "icon": "icons/512x512.png", "category": "Utility", From 4740414d812f30fca609c7989d171cdd12e88ac4 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:20:39 +0100 Subject: [PATCH 15/29] fix: include dependencies and new files --- .github/workflows/release.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) 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 From 33630b216013516a9d2cef649e40851ccd5cb713 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:30:59 +0100 Subject: [PATCH 16/29] Update README.md --- electron-app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/README.md b/electron-app/README.md index 854930745..a881da8e5 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 From ee6441cdc57928ab8886e616febfdcbe9a49488f Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:33:43 +0100 Subject: [PATCH 17/29] Update README.md --- electron-app/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/electron-app/README.md b/electron-app/README.md index a881da8e5..7034c163e 100644 --- a/electron-app/README.md +++ b/electron-app/README.md @@ -89,6 +89,7 @@ sudo ifconfig lo0 alias 127.0.0.9 up - `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` From ef5bd72490e123896ec9e82c74c66fedacbdd2e7 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:34:46 +0100 Subject: [PATCH 18/29] feat: add stale boards indication --- .../filtering/components/FilterController.tsx | 6 +++ .../filtering/components/FilterDialog.tsx | 30 ++++++++++- .../filtering/store/filteringSlice.ts | 3 +- .../rightSidebar/tabs/TabHeader.tsx | 53 +++++++++++++------ frontend/testing-view/src/lib/utils.ts | 8 +++ 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/frontend/testing-view/src/features/filtering/components/FilterController.tsx b/frontend/testing-view/src/features/filtering/components/FilterController.tsx index 538cef4a9..fdea53a8d 100644 --- a/frontend/testing-view/src/features/filtering/components/FilterController.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterController.tsx @@ -1,3 +1,5 @@ +import { useShallow } from "zustand/shallow"; +import { detectExtraBoards } from "../../../lib/utils"; import { useStore } from "../../../store/store"; import { FilterCategoryItem } from "./FilterCategoryItem"; import { FilterDialog } from "./FilterDialog"; @@ -7,12 +9,15 @@ export const FilterController = () => { 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 ( { onClearAll={() => clearFilters(scope)} onSelectAll={() => selectAllFilters(scope)} 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 ecb047379..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; @@ -229,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/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/lib/utils.ts b/frontend/testing-view/src/lib/utils.ts index adbcdfc2f..0df30e9c3 100644 --- a/frontend/testing-view/src/lib/utils.ts +++ b/frontend/testing-view/src/lib/utils.ts @@ -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[]; From e2fc74e5c45e76216556df70f34461db2ab1e779 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:57:43 +0100 Subject: [PATCH 19/29] feat: add backend logs window to electron --- electron-app/main.js | 16 +++++++++--- electron-app/package.json | 1 + electron-app/preload.js | 3 +++ electron-app/src/processes/backend.js | 34 +++++++++++++++++++++++--- electron-app/src/windows/logWindow.js | 26 ++++++++++++++++++++ electron-app/src/windows/mainWindow.js | 14 +++++++---- pnpm-lock.yaml | 17 +++++++++++++ 7 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 electron-app/src/windows/logWindow.js diff --git a/electron-app/main.js b/electron-app/main.js index fd7a0a40d..ea52752c4 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -4,13 +4,14 @@ * 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 { 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; @@ -38,15 +39,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 @@ -54,7 +62,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 30344aa16..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" 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/processes/backend.js b/electron-app/src/processes/backend.js index 747336e3d..9075f3b45 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,6 +16,9 @@ import { getUserConfigPath, } from "../utils/paths.js"; +// Create ANSI to HTML converter +const convert = new AnsiToHtml(); + // Get the application root path const appPath = getAppPath(); @@ -30,7 +34,7 @@ let lastBackendError = null; * @example * startBackend(); */ -function startBackend() { +function startBackend(logWindow = null) { return new Promise((resolve, reject) => { // Get paths for binary and config const backendBin = getBinaryPath("backend"); @@ -63,6 +67,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 (logWindow) { + const htmlData = convert.toHtml(data.toString().trim()); + logWindow.webContents.send("log", htmlData); + } }); // Capture stderr output (where Go errors/panics are written) @@ -71,6 +81,12 @@ function startBackend() { logger.backend.error(errorMsg); // Store the last error message lastBackendError = errorMsg; + + // Send error message to log window + if (logWindow) { + const htmlError = convert.toHtml(errorMsg); + logWindow.webContents.send("log", htmlError); + } }); // Handle spawn errors @@ -120,8 +136,20 @@ 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"); + + backendProcess.stdin.end(); + + const fallbackTimer = setTimeout(() => { + if (backendProcess && !backendProcess.killed) { + logger.backend.warning( + "Backend did not exit gracefully, force killing..." + ); + backendProcess.kill("SIGKILL"); + } + }, 2000); + + fallbackTimer.unref(); + // Clear the process reference backendProcess = null; } diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js new file mode 100644 index 000000000..7aac0e928 --- /dev/null +++ b/electron-app/src/windows/logWindow.js @@ -0,0 +1,26 @@ +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, + }, + }); + + logWindow.loadFile(path.join(appPath, "src/logs/logs.html")); + + return logWindow; +}; diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index 259f8dc79..213dc03e9 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"), @@ -60,6 +62,8 @@ function createWindow() { mainWindow.on("closed", () => { mainWindow = null; }); + + return mainWindow; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9698bd788..29d83d227 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 @@ -1805,6 +1808,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 +2400,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'} @@ -6218,6 +6229,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 +7060,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: {} From df01f6b6ebfb90f461e7ba752257a1a0a329f1b9 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:40:35 +0100 Subject: [PATCH 20/29] feat: fix menus --- electron-app/src/menu/menu.js | 14 +++++++++++--- electron-app/src/windows/mainWindow.js | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js index 64f776da9..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(); + } + }, }, ], }, @@ -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/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index 213dc03e9..7b9d8a5f0 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -51,7 +51,8 @@ function createWindow(screenWidth, screenHeight) { loadView(currentView); // Create application menu - createMenu(mainWindow); + const menu = createMenu(mainWindow); + mainWindow.setApplicationMenu(menu); // Open DevTools in development mode if (!app.isPackaged) { From 050b5112f269d08615e3fe1378e74473695fe223 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:43:33 +0100 Subject: [PATCH 21/29] feat: update README.md --- electron-app/src/windows/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/electron-app/src/windows/README.md b/electron-app/src/windows/README.md index bb4e4ec5a..c13435b7a 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 From e4ce2d00393124c62adf495127696585685ab274 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:03:40 +0100 Subject: [PATCH 22/29] fix: logs path --- electron-app/src/windows/logWindow.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js index 7aac0e928..fe0d09106 100644 --- a/electron-app/src/windows/logWindow.js +++ b/electron-app/src/windows/logWindow.js @@ -20,7 +20,8 @@ export const createLogWindow = (screenWidth, screenHeight) => { }, }); - logWindow.loadFile(path.join(appPath, "src/logs/logs.html")); + const logFilePath = path.join(appPath, "src", "logs", "logs.html"); + logWindow.loadFile(logFilePath); return logWindow; }; From e804e4b9d472d09ca94f602f1c5f657f5b3382ab Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:33:52 +0100 Subject: [PATCH 23/29] feat: rename folders --- electron-app/src/log-viewer/index.html | 34 ++++++++++++++++++++++++++ electron-app/src/windows/logWindow.js | 2 +- electron-app/src/windows/mainWindow.js | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 electron-app/src/log-viewer/index.html diff --git a/electron-app/src/log-viewer/index.html b/electron-app/src/log-viewer/index.html new file mode 100644 index 000000000..2513be5f8 --- /dev/null +++ b/electron-app/src/log-viewer/index.html @@ -0,0 +1,34 @@ + + + + Backend Logs + + + +
+ + + + diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js index fe0d09106..4c1c0e27e 100644 --- a/electron-app/src/windows/logWindow.js +++ b/electron-app/src/windows/logWindow.js @@ -20,7 +20,7 @@ export const createLogWindow = (screenWidth, screenHeight) => { }, }); - const logFilePath = path.join(appPath, "src", "logs", "logs.html"); + 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 7b9d8a5f0..a6b41380c 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -52,7 +52,7 @@ function createWindow(screenWidth, screenHeight) { // Create application menu const menu = createMenu(mainWindow); - mainWindow.setApplicationMenu(menu); + mainWindow.setMenu(menu); // Open DevTools in development mode if (!app.isPackaged) { From 8b64a887353f1a59378c04c36779b175973b378a Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:44:50 +0100 Subject: [PATCH 24/29] fix: scrolling --- electron-app/src/log-viewer/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/src/log-viewer/index.html b/electron-app/src/log-viewer/index.html index 2513be5f8..c9496d24a 100644 --- a/electron-app/src/log-viewer/index.html +++ b/electron-app/src/log-viewer/index.html @@ -27,7 +27,7 @@ // Use innerHTML to render the colors, but be careful of XSS if logs are untrusted entry.innerHTML = data; container.appendChild(entry); - container.scrollTop = container.scrollHeight; + window.scrollTo(0, document.body.scrollHeight); }); From e5bc6cc18cec3a956725e6a3884cadb576649796 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:04:08 +0100 Subject: [PATCH 25/29] feat: make main window reload after restart, handle better backend start and restart --- electron-app/src/ipc/handlers.js | 11 ++- electron-app/src/processes/backend.js | 88 +++++++++++++------ electron-app/src/windows/mainWindow.js | 14 ++- frontend/testing-view/src/hooks/useAppMode.ts | 1 - 4 files changed, 81 insertions(+), 33 deletions(-) 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/processes/backend.js b/electron-app/src/processes/backend.js index 9075f3b45..3c2c9720d 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -25,6 +25,9 @@ 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; @@ -34,7 +37,13 @@ let lastBackendError = null; * @example * startBackend(); */ -function startBackend(logWindow = null) { +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"); @@ -69,9 +78,9 @@ function startBackend(logWindow = null) { logger.backend.info(`${data.toString().trim()}`); // Send log message to log window - if (logWindow) { + if (currentLogWindow && !currentLogWindow.isDestroyed()) { const htmlData = convert.toHtml(data.toString().trim()); - logWindow.webContents.send("log", htmlData); + currentLogWindow.webContents.send("log", htmlData); } }); @@ -83,9 +92,9 @@ function startBackend(logWindow = null) { lastBackendError = errorMsg; // Send error message to log window - if (logWindow) { + if (currentLogWindow && !currentLogWindow.isDestroyed()) { const htmlError = convert.toHtml(errorMsg); - logWindow.webContents.send("log", htmlError); + currentLogWindow.webContents.send("log", htmlError); } }); @@ -102,7 +111,7 @@ function startBackend(logWindow = null) { // If the backend didn't fail in this period of time, resolve the promise setTimeout(() => { resolve(backendProcess); - }, 4000); + }, 2000); // Handle process exit backendProcess.on("close", (code) => { @@ -127,32 +136,46 @@ function startBackend(logWindow = null) { } /** - * 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..."); - - backendProcess.stdin.end(); - - const fallbackTimer = setTimeout(() => { - if (backendProcess && !backendProcess.killed) { - logger.backend.warning( - "Backend did not exit gracefully, force killing..." - ); - backendProcess.kill("SIGKILL"); - } - }, 2000); +async function stopBackend() { + return new Promise((resolve, reject) => { + const localBackendProcess = backendProcess; - fallbackTimer.unref(); + // Only stop if process exists and is still running + if (localBackendProcess && !localBackendProcess.killed) { + logger.backend.info("Stopping backend..."); - // Clear the process reference - backendProcess = null; - } + 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(); + } + }); } /** @@ -161,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/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index a6b41380c..ae5fc29b2 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -98,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"). @@ -124,4 +136,4 @@ function getMainWindow() { return mainWindow; } -export { createWindow, getCurrentView, getMainWindow, loadView }; +export { createWindow, getCurrentView, getMainWindow, loadView, reloadWindow }; 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"; From 58bff09e2607ba0fa96271bd63ec6db38dae2085 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:05:52 +0100 Subject: [PATCH 26/29] fix: fs import --- electron-app/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/electron-app/main.js b/electron-app/main.js index ea52752c4..c468fbbe3 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -6,6 +6,7 @@ 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"; From 86f0f5df389e67f121883b3b4f83d9869a36da23 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:16:45 +0100 Subject: [PATCH 27/29] docs: update README.md --- electron-app/src/windows/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electron-app/src/windows/README.md b/electron-app/src/windows/README.md index c13435b7a..7698de6aa 100644 --- a/electron-app/src/windows/README.md +++ b/electron-app/src/windows/README.md @@ -30,6 +30,10 @@ Manages the primary and logs Electron `BrowserWindow` instances and provides fun 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. From aa5ab6f2795cd1012271f93deb29873aeb736e4a Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:07:31 +0100 Subject: [PATCH 28/29] feat: implement icons-master --- frontend/frontend-kit/ui/package.json | 5 ++++- frontend/frontend-kit/ui/src/icons/arrows.ts | 2 +- pnpm-lock.yaml | 10 ++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) 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/pnpm-lock.yaml b/pnpm-lock.yaml index 29d83d227..0597f9802 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,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 @@ -756,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'} @@ -4244,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==} @@ -5189,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 From cc058d95570f8d78299ddc3e20c655877c078956 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:19:47 +0100 Subject: [PATCH 29/29] docs: add README.md --- frontend/frontend-kit/ui/README.md | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 frontend/frontend-kit/ui/README.md 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 +```