Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ jobs:
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
Expand Down Expand Up @@ -188,8 +187,6 @@ 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
Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Hyperloop Control Station H11
![Testing View](https://raw.githubusercontent.com/Hyperloop-UPV/webpage/5c1c827d82d380689856ee61af43da30da22e0fc/src/assets/backgrounds/testing-view.png)

![Testing View](https://raw.githubusercontent.com/Hyperloop-UPV/webpage/5c1c827d82d380689856ee61af43da30da22e0fc/src/assets/backgrounds/testing-view.png)

## Monorepo usage

Expand All @@ -20,17 +21,17 @@ Before starting, ensure you have the following installed:

Our `pnpm-workspace.yaml` defines the following workspaces:

| Workspace | Language | Description |
| :----------------------------- | :------- | :--------------------------------------------- |
| `testing-view` | TS/React | Web interface for telemetry testing |
| `competition-view` | TS/React | UI for the competition |
| `backend` | Go | Data ingestion and pod communication server |
| `packet-sender` | Rust | Utility for simulating vehicle packets |
| `electron-app` | JS | The main Control Station desktop application |
| `@workspace/ui` | TS/React | Shared UI component library (frontend-kit) |
| `@workspace/core` | TS | Shared business logic and types (frontend-kit) |
| `@workspace/eslint-config` | ESLint | Common ESLint configuration (frontend-kit) |
| `@workspace/typescript-config` | TS | Common TypeScript configuration (frontend-kit) |
| Workspace | Language | Description |
| :----------------------------- | :------- | :---------------------------------------------------- |
| `testing-view` | TS/React | Web interface for telemetry testing |
| `competition-view` | TS/React | UI for the competition |
| `backend` | Go | Data ingestion and pod communication server |
| `packet-sender` | Rust | Utility for simulating vehicle packets |
| `hyperloop-control-station` | JS | The main Control Station electron desktop application |
| `@workspace/ui` | TS/React | Shared UI component library (frontend-kit) |
| `@workspace/core` | TS | Shared business logic and types (frontend-kit) |
| `@workspace/eslint-config` | ESLint | Common ESLint configuration (frontend-kit) |
| `@workspace/typescript-config` | TS | Common TypeScript configuration (frontend-kit) |

---

Expand Down
3 changes: 2 additions & 1 deletion electron-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,5 @@ coverage

# Config and config backups
config.toml
config.toml.backup-*
config.toml.backup-*
version.toml
5 changes: 4 additions & 1 deletion electron-app/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ logger.header("Hyperloop Control Station Build");

if (frontendBuilt && !process.env.CI) {
logger.info("Finalizing Electron...");
run("pnpm --filter electron-app install --frozen-lockfile", __dirname);
run(
"pnpm --filter hyperloop-control-station install --frozen-lockfile",
__dirname
);
}

if (allSuccess) {
Expand Down
12 changes: 5 additions & 7 deletions electron-app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ if (process.platform === "linux") {
// Setup IPC handlers for renderer process communication
setupIpcHandlers();

app.setName("hyperloop-control-station");

// App lifecycle: wait for Electron to be ready
app.whenReady().then(async () => {
// Get the screen width and height
Expand Down Expand Up @@ -118,11 +116,11 @@ app.on("window-all-closed", () => {
});

// Cleanup before app quits
app.on("before-quit", () => {
// Stop backend process gracefully
stopBackend();
// Stop packet sender process gracefully
stopPacketSender();
app.on("before-quit", (e) => {
e.preventDefault();
Promise.all([stopBackend(), stopPacketSender()])
.catch((error) => logger.electron.error("Error during shutdown:", error))
.finally(() => app.exit());
});

// Handle uncaught exceptions globally
Expand Down
8 changes: 3 additions & 5 deletions electron-app/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "electron-app",
"name": "hyperloop-control-station",
"version": "1.0.0",
"description": "Hyperloop UPV Control Station",
"main": "main.js",
Expand Down Expand Up @@ -57,7 +57,7 @@
"owner": "Hyperloop-UPV",
"repo": "software"
},
"productName": "Hyperloop-Control-Station",
"productName": "Hyperloop-Ctrl",
"directories": {
"output": "dist"
},
Expand Down Expand Up @@ -107,9 +107,7 @@
"linux": {
"target": [
"AppImage",
"deb",
"rpm",
"pacman"
"deb"
],
"icon": "icons/512x512.png",
"category": "Utility",
Expand Down
8 changes: 6 additions & 2 deletions electron-app/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
// Open folder selection dialog
selectFolder: () => ipcRenderer.invoke("select-folder"),
// Receive log message from backend
onLog: (callback) =>
ipcRenderer.on("log", (_event, value) => callback(value)),
onLog: (callback) => {
const listener = (_event, value) => callback(value);
ipcRenderer.removeAllListeners("log");
ipcRenderer.on("log", listener);
return () => ipcRenderer.removeListener("log", listener);
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import fs from "fs";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ConfigManager } from "../configManager.js";

// Mock fs module
Expand All @@ -8,15 +8,24 @@ vi.mock("fs");
describe("ConfigManager - Initialization", () => {
const templatePath = "/path/to/template.toml";
const userConfigPath = "/path/to/user.toml";
const versionFilePath = "/path/to/version.toml";
const appVersion = "1.0.0";
const appVersionReturnValue = `version = "${appVersion}"`;

beforeEach(() => {
vi.clearAllMocks();
});

it("should create config manager instance", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue(appVersionReturnValue);

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);

expect(manager.userConfigPath).toBe(userConfigPath);
expect(manager.templatePath).toBe(templatePath);
Expand All @@ -28,8 +37,9 @@ describe("ConfigManager - Initialization", () => {
return true;
});
fs.mkdirSync.mockImplementation(() => {});
fs.readFileSync.mockReturnValue(appVersionReturnValue);

new ConfigManager(userConfigPath, templatePath);
new ConfigManager(userConfigPath, templatePath, versionFilePath, appVersion);

expect(fs.mkdirSync).toHaveBeenCalledWith("/path/to", {
recursive: true,
Expand All @@ -39,17 +49,17 @@ describe("ConfigManager - Initialization", () => {
it("should copy template if user config does not exist", () => {
fs.existsSync.mockImplementation((path) => {
if (path === userConfigPath) return false;
if (path === templatePath) return true;
return true;
});
fs.copyFileSync.mockImplementation(() => {});
fs.writeFileSync.mockImplementation(() => {});
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});

new ConfigManager(userConfigPath, templatePath);
new ConfigManager(userConfigPath, templatePath, versionFilePath, appVersion);

expect(fs.copyFileSync).toHaveBeenCalledWith(templatePath, userConfigPath);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Created config from template")
expect.stringContaining("Created config from template"),
);

consoleSpy.mockRestore();
Expand All @@ -59,7 +69,7 @@ describe("ConfigManager - Initialization", () => {
fs.existsSync.mockReturnValue(false);

expect(() => {
new ConfigManager(userConfigPath, templatePath);
new ConfigManager(userConfigPath, templatePath, versionFilePath, appVersion);
}).toThrow("Template not found");
});
});
67 changes: 59 additions & 8 deletions electron-app/src/config/__tests__/ConfigManager.read-write.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import fs from "fs";
describe("ConfigManager - Read/Write Operations", () => {
const templatePath = "/path/to/template.toml";
const userConfigPath = "/path/to/user.toml";
const versionFilePath = "/path/to/version.toml";
const appVersion = "1.0.0";
const appVersionReturnValue = `version = "${appVersion}"`;
const mockTomlContent = `# User Config
name = "test"
enabled = true
Expand All @@ -23,9 +26,15 @@ host = "localhost"`;
describe("read", () => {
it("should read and parse TOML config", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockReturnValue(mockTomlContent);

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);
const config = manager.read();

expect(config).toHaveProperty("name", "test");
Expand All @@ -35,20 +44,32 @@ host = "localhost"`;

it("should throw error on invalid TOML", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockReturnValue("invalid toml [[[");

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);

expect(() => manager.read()).toThrow("Failed to read config");
});

it("should throw error on file read failure", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockImplementation(() => {
throw new Error("Permission denied");
});

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);

expect(() => manager.read()).toThrow("Failed to read config");
});
Expand All @@ -57,9 +78,15 @@ host = "localhost"`;
describe("readRaw", () => {
it("should return raw TOML content", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockReturnValue(mockTomlContent);

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);
const raw = manager.readRaw();

expect(raw).toBe(mockTomlContent);
Expand All @@ -70,11 +97,17 @@ host = "localhost"`;
describe("update", () => {
it("should update config from object", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockReturnValue(mockTomlContent);
fs.writeFileSync.mockImplementation(() => {});
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);
const result = manager.update({
name: "updated",
enabled: false,
Expand All @@ -91,12 +124,18 @@ host = "localhost"`;

it("should throw error on update failure", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockReturnValue(mockTomlContent);
fs.writeFileSync.mockImplementation(() => {
throw new Error("Write failed");
});

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);

expect(() => manager.update({ name: "test" })).toThrow(
"Failed to update config"
Expand All @@ -107,11 +146,17 @@ host = "localhost"`;
describe("updateValue", () => {
it("should update a single value", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockReturnValue(mockTomlContent);
fs.writeFileSync.mockImplementation(() => {});
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);
const result = manager.updateValue("database", "host", "192.168.1.1");

expect(result).toBe(true);
Expand All @@ -125,11 +170,17 @@ host = "localhost"`;

it("should throw error on value update failure", () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
fs.readFileSync.mockImplementation(() => {
throw new Error("Read failed");
});

const manager = new ConfigManager(userConfigPath, templatePath);
const manager = new ConfigManager(
userConfigPath,
templatePath,
versionFilePath,
appVersion,
);

expect(() => manager.updateValue("section", "key", "value")).toThrow(
"Failed to update value"
Expand Down
Loading
Loading