Skip to content

Commit b6c7fc5

Browse files
authored
Merge pull request #2 from Hyperloop-UPV/control-station/config-versioning
Control station/config versioning
2 parents cc058d9 + 8d4528a commit b6c7fc5

31 files changed

Lines changed: 441 additions & 165 deletions

.github/workflows/release.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ jobs:
9090
if: runner.os == 'Linux'
9191
run: |
9292
sudo apt-get update
93-
sudo apt-get install -y rpm libarchive-tools
9493
9594
# Download ONLY the appropriate backend for this platform
9695
- name: Download Linux backend
@@ -188,8 +187,6 @@ jobs:
188187
electron-app/dist/*.exe
189188
electron-app/dist/*.AppImage
190189
electron-app/dist/*.deb
191-
electron-app/dist/*.rpm
192-
electron-app/dist/*.pacman
193190
electron-app/dist/*.dmg
194191
electron-app/dist/*.zip
195192
electron-app/dist/*.yml

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Hyperloop Control Station H11
2-
![Testing View](https://raw.githubusercontent.com/Hyperloop-UPV/webpage/5c1c827d82d380689856ee61af43da30da22e0fc/src/assets/backgrounds/testing-view.png)
2+
3+
![Testing View](https://raw.githubusercontent.com/Hyperloop-UPV/webpage/5c1c827d82d380689856ee61af43da30da22e0fc/src/assets/backgrounds/testing-view.png)
34

45
## Monorepo usage
56

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

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

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

3536
---
3637

electron-app/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,5 @@ coverage
8181

8282
# Config and config backups
8383
config.toml
84-
config.toml.backup-*
84+
config.toml.backup-*
85+
version.toml

electron-app/build.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ logger.header("Hyperloop Control Station Build");
254254

255255
if (frontendBuilt && !process.env.CI) {
256256
logger.info("Finalizing Electron...");
257-
run("pnpm --filter electron-app install --frozen-lockfile", __dirname);
257+
run(
258+
"pnpm --filter hyperloop-control-station install --frozen-lockfile",
259+
__dirname
260+
);
258261
}
259262

260263
if (allSuccess) {

electron-app/main.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ if (process.platform === "linux") {
3636
// Setup IPC handlers for renderer process communication
3737
setupIpcHandlers();
3838

39-
app.setName("hyperloop-control-station");
40-
4139
// App lifecycle: wait for Electron to be ready
4240
app.whenReady().then(async () => {
4341
// Get the screen width and height
@@ -118,11 +116,11 @@ app.on("window-all-closed", () => {
118116
});
119117

120118
// Cleanup before app quits
121-
app.on("before-quit", () => {
122-
// Stop backend process gracefully
123-
stopBackend();
124-
// Stop packet sender process gracefully
125-
stopPacketSender();
119+
app.on("before-quit", (e) => {
120+
e.preventDefault();
121+
Promise.all([stopBackend(), stopPacketSender()])
122+
.catch((error) => logger.electron.error("Error during shutdown:", error))
123+
.finally(() => app.exit());
126124
});
127125

128126
// Handle uncaught exceptions globally

electron-app/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "electron-app",
2+
"name": "hyperloop-control-station",
33
"version": "1.0.0",
44
"description": "Hyperloop UPV Control Station",
55
"main": "main.js",
@@ -57,7 +57,7 @@
5757
"owner": "Hyperloop-UPV",
5858
"repo": "software"
5959
},
60-
"productName": "Hyperloop-Control-Station",
60+
"productName": "Hyperloop-Ctrl",
6161
"directories": {
6262
"output": "dist"
6363
},
@@ -107,9 +107,7 @@
107107
"linux": {
108108
"target": [
109109
"AppImage",
110-
"deb",
111-
"rpm",
112-
"pacman"
110+
"deb"
113111
],
114112
"icon": "icons/512x512.png",
115113
"category": "Utility",

electron-app/preload.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
3737
// Open folder selection dialog
3838
selectFolder: () => ipcRenderer.invoke("select-folder"),
3939
// Receive log message from backend
40-
onLog: (callback) =>
41-
ipcRenderer.on("log", (_event, value) => callback(value)),
40+
onLog: (callback) => {
41+
const listener = (_event, value) => callback(value);
42+
ipcRenderer.removeAllListeners("log");
43+
ipcRenderer.on("log", listener);
44+
return () => ipcRenderer.removeListener("log", listener);
45+
},
4246
});

electron-app/src/config/__tests__/ConfigManager.initialization.test.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { describe, it, expect, beforeEach, vi } from "vitest";
21
import fs from "fs";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
33
import { ConfigManager } from "../configManager.js";
44

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

1215
beforeEach(() => {
1316
vi.clearAllMocks();
1417
});
1518

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

19-
const manager = new ConfigManager(userConfigPath, templatePath);
23+
const manager = new ConfigManager(
24+
userConfigPath,
25+
templatePath,
26+
versionFilePath,
27+
appVersion,
28+
);
2029

2130
expect(manager.userConfigPath).toBe(userConfigPath);
2231
expect(manager.templatePath).toBe(templatePath);
@@ -28,8 +37,9 @@ describe("ConfigManager - Initialization", () => {
2837
return true;
2938
});
3039
fs.mkdirSync.mockImplementation(() => {});
40+
fs.readFileSync.mockReturnValue(appVersionReturnValue);
3141

32-
new ConfigManager(userConfigPath, templatePath);
42+
new ConfigManager(userConfigPath, templatePath, versionFilePath, appVersion);
3343

3444
expect(fs.mkdirSync).toHaveBeenCalledWith("/path/to", {
3545
recursive: true,
@@ -39,17 +49,17 @@ describe("ConfigManager - Initialization", () => {
3949
it("should copy template if user config does not exist", () => {
4050
fs.existsSync.mockImplementation((path) => {
4151
if (path === userConfigPath) return false;
42-
if (path === templatePath) return true;
4352
return true;
4453
});
4554
fs.copyFileSync.mockImplementation(() => {});
55+
fs.writeFileSync.mockImplementation(() => {});
4656
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
4757

48-
new ConfigManager(userConfigPath, templatePath);
58+
new ConfigManager(userConfigPath, templatePath, versionFilePath, appVersion);
4959

5060
expect(fs.copyFileSync).toHaveBeenCalledWith(templatePath, userConfigPath);
5161
expect(consoleSpy).toHaveBeenCalledWith(
52-
expect.stringContaining("Created config from template")
62+
expect.stringContaining("Created config from template"),
5363
);
5464

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

6171
expect(() => {
62-
new ConfigManager(userConfigPath, templatePath);
72+
new ConfigManager(userConfigPath, templatePath, versionFilePath, appVersion);
6373
}).toThrow("Template not found");
6474
});
6575
});

electron-app/src/config/__tests__/ConfigManager.read-write.test.js

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import fs from "fs";
99
describe("ConfigManager - Read/Write Operations", () => {
1010
const templatePath = "/path/to/template.toml";
1111
const userConfigPath = "/path/to/user.toml";
12+
const versionFilePath = "/path/to/version.toml";
13+
const appVersion = "1.0.0";
14+
const appVersionReturnValue = `version = "${appVersion}"`;
1215
const mockTomlContent = `# User Config
1316
name = "test"
1417
enabled = true
@@ -23,9 +26,15 @@ host = "localhost"`;
2326
describe("read", () => {
2427
it("should read and parse TOML config", () => {
2528
fs.existsSync.mockReturnValue(true);
29+
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
2630
fs.readFileSync.mockReturnValue(mockTomlContent);
2731

28-
const manager = new ConfigManager(userConfigPath, templatePath);
32+
const manager = new ConfigManager(
33+
userConfigPath,
34+
templatePath,
35+
versionFilePath,
36+
appVersion,
37+
);
2938
const config = manager.read();
3039

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

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

40-
const manager = new ConfigManager(userConfigPath, templatePath);
50+
const manager = new ConfigManager(
51+
userConfigPath,
52+
templatePath,
53+
versionFilePath,
54+
appVersion,
55+
);
4156

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

4560
it("should throw error on file read failure", () => {
4661
fs.existsSync.mockReturnValue(true);
62+
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
4763
fs.readFileSync.mockImplementation(() => {
4864
throw new Error("Permission denied");
4965
});
5066

51-
const manager = new ConfigManager(userConfigPath, templatePath);
67+
const manager = new ConfigManager(
68+
userConfigPath,
69+
templatePath,
70+
versionFilePath,
71+
appVersion,
72+
);
5273

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

62-
const manager = new ConfigManager(userConfigPath, templatePath);
84+
const manager = new ConfigManager(
85+
userConfigPath,
86+
templatePath,
87+
versionFilePath,
88+
appVersion,
89+
);
6390
const raw = manager.readRaw();
6491

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

77-
const manager = new ConfigManager(userConfigPath, templatePath);
105+
const manager = new ConfigManager(
106+
userConfigPath,
107+
templatePath,
108+
versionFilePath,
109+
appVersion,
110+
);
78111
const result = manager.update({
79112
name: "updated",
80113
enabled: false,
@@ -91,12 +124,18 @@ host = "localhost"`;
91124

92125
it("should throw error on update failure", () => {
93126
fs.existsSync.mockReturnValue(true);
127+
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
94128
fs.readFileSync.mockReturnValue(mockTomlContent);
95129
fs.writeFileSync.mockImplementation(() => {
96130
throw new Error("Write failed");
97131
});
98132

99-
const manager = new ConfigManager(userConfigPath, templatePath);
133+
const manager = new ConfigManager(
134+
userConfigPath,
135+
templatePath,
136+
versionFilePath,
137+
appVersion,
138+
);
100139

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

114-
const manager = new ConfigManager(userConfigPath, templatePath);
154+
const manager = new ConfigManager(
155+
userConfigPath,
156+
templatePath,
157+
versionFilePath,
158+
appVersion,
159+
);
115160
const result = manager.updateValue("database", "host", "192.168.1.1");
116161

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

126171
it("should throw error on value update failure", () => {
127172
fs.existsSync.mockReturnValue(true);
173+
fs.readFileSync.mockReturnValueOnce(appVersionReturnValue);
128174
fs.readFileSync.mockImplementation(() => {
129175
throw new Error("Read failed");
130176
});
131177

132-
const manager = new ConfigManager(userConfigPath, templatePath);
178+
const manager = new ConfigManager(
179+
userConfigPath,
180+
templatePath,
181+
versionFilePath,
182+
appVersion,
183+
);
133184

134185
expect(() => manager.updateValue("section", "key", "value")).toThrow(
135186
"Failed to update value"

0 commit comments

Comments
 (0)