Skip to content

Commit 1651ff5

Browse files
tinicclaude
andcommitted
refactor: large maintainability + correctness pass
Codebase audit and cleanup across most of elle-app. Highlights: - Extract useUnits/useNumberEntry/useAxisOffsets/useJog/ParameterButton/PresetGrid and split g-code math into gcode/threading.ts and gcode/turning.ts with tests - Store canned-cycle values in canonical mm; delete convertThreading/Turning - Centralize HAL URLs + constants in frontend/src/config.ts (env-var overridable) - Surface HAL connection failures + settings-save errors via UI banners - Type HAL API end-to-end (HalOut, ThreadingRequest, TurningRequest); distinguish 4xx (silent) from 5xx/network (banner trips) - Centralize settings schema so adding a field only touches one place - Upgrade all deps to latest (electron 36->41, vite 6->8, vitest 2->4, typescript 5->6, vue-router 4->5); fix moduleResolution deprecations - Remove dead deps (@apollo/server, graphql, electron-log, electron-updater) and unused Iosevka font variants (~22MB reclaimed) - Add yarn verify pipeline (typecheck + eslint + prettier + vitest) with pre-commit hook, GitHub Actions CI, and 64 tests Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a587f56 commit 1651ff5

55 files changed

Lines changed: 5424 additions & 5090 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: verify
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
verify:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Node 22
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 22
19+
20+
- name: Install yarn
21+
run: npm install --global yarn
22+
23+
- name: Install root deps
24+
run: cd elle-app && yarn install --frozen-lockfile
25+
26+
- name: Install frontend deps
27+
run: cd elle-app/elle-frontend && yarn install --frozen-lockfile
28+
29+
- name: Typecheck
30+
run: cd elle-app && yarn typecheck
31+
32+
- name: Lint
33+
run: cd elle-app && yarn lint
34+
35+
- name: Prettier check
36+
run: cd elle-app && yarn format:check
37+
38+
- name: Tests
39+
run: cd elle-app && yarn test
40+
41+
- name: Frontend build
42+
run: cd elle-app && yarn build:front
43+
44+
- name: Electron build
45+
run: cd elle-app && yarn build

elle-app/.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cd elle-app && yarn lint-staged

elle-app/.prettierignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
dist
3+
build
4+
elle-frontend/node_modules
5+
elle-frontend/public
6+
elle-hal
7+
yarn.lock
8+
*.min.js
9+
*.min.css

elle-app/.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"singleQuote": true,
3+
"semi": false,
4+
"trailingComma": "none",
5+
"printWidth": 100
6+
}

elle-app/elle-electron/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export const isDev = process.env.APP_IS_DEV ? true : false
2+
3+
// Window sizing for the touch-screen target display
4+
export const WINDOW_WIDTH = 1366
5+
export const WINDOW_HEIGHT = 768
Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,48 @@
11
import Store from 'electron-store'
22

3-
interface Tool {
3+
export interface Tool {
44
id: number
55
offsetX: number
66
offsetZ: number
77
description: string
88
}
99

10+
export interface AppBounds {
11+
x?: number
12+
y?: number
13+
width?: number
14+
height?: number
15+
}
16+
17+
export interface Settings {
18+
appBounds?: AppBounds
19+
diameterMode?: boolean
20+
defaultMetricOnStartup?: boolean
21+
selectedThreadingTab?: number
22+
selectedTurningTab?: number
23+
selectedPitchTab?: number[]
24+
pitchX?: number
25+
pitchZ?: number
26+
encoderScaleZ?: number
27+
encoderScaleX?: number
28+
tools?: Tool[]
29+
currentToolIndex?: number
30+
currentToolOffsetX?: number
31+
currentToolOffsetZ?: number
32+
}
33+
1034
interface AppConfig {
11-
setting: {
12-
appBounds?: any
13-
diameterMode?: boolean
14-
defaultMetricOnStartup?: boolean
15-
selectedThreadingTab?: number
16-
selectedTurningTab?: number
17-
selectedPitchTab?: number[]
18-
pitchX?: number
19-
pitchZ?: number
20-
encoderScaleZ?: number
21-
encoderScaleX?: number
22-
tools?: Tool[]
23-
currentToolIndex?: number
24-
currentToolOffsetX?: number
25-
currentToolOffsetZ?: number
26-
}
35+
setting: Settings
36+
}
37+
38+
// electron-store's Store#get/set are typed on top-level keys only, so we cast
39+
// to `Record<string, unknown>` to reach nested `setting.xxx` dotted paths.
40+
type StoreAny = {
41+
get: <T = unknown>(key: string, defaultValue?: T) => T
42+
set: (key: string, value: unknown) => void
2743
}
2844

29-
export const appConfig = new Store<AppConfig>({
45+
const rawStore = new Store<AppConfig>({
3046
name: 'appConfig',
3147
defaults: {
3248
setting: {
@@ -40,3 +56,5 @@ export const appConfig = new Store<AppConfig>({
4056
}
4157
}
4258
})
59+
60+
export const appConfig = rawStore as unknown as StoreAny
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
1+
import { installExtension, VUEJS_DEVTOOLS } from 'electron-devtools-installer'
22

33
export async function installExt() {
4-
await installExtension(VUEJS_DEVTOOLS)
5-
.then(() => {})
6-
.catch((_err) => {})
4+
try {
5+
await installExtension(VUEJS_DEVTOOLS)
6+
} catch {
7+
// ignore — dev tools install is best-effort
8+
}
79
}

elle-app/elle-electron/main.ts

Lines changed: 33 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const __filename = fileURLToPath(import.meta.url)
66
const __dirname = path.dirname(__filename)
77

88
import type { BrowserWindowConstructorOptions } from 'electron'
9-
import { app, BrowserWindow, ipcMain, screen, nativeTheme } from 'electron'
10-
import { isDev } from './config.js'
11-
import { appConfig } from './electron-store/configuration.js'
9+
import { app, BrowserWindow, ipcMain, screen } from 'electron'
10+
import { isDev, WINDOW_HEIGHT, WINDOW_WIDTH } from './config.js'
11+
import { appConfig, type AppBounds, type Settings } from './electron-store/configuration.js'
1212
import type { ChildProcess } from 'node:child_process'
1313
import { spawn, spawnSync } from 'node:child_process'
1414

@@ -18,14 +18,14 @@ let halquit: boolean = false
1818

1919
async function createWindow() {
2020
const { width, height } = screen.getPrimaryDisplay().workAreaSize
21-
const appBounds: any = (appConfig as any).get('setting.appBounds')
21+
const appBounds = appConfig.get<AppBounds | undefined>('setting.appBounds')
2222
const BrowserWindowOptions: BrowserWindowConstructorOptions = {
23-
width: 1366,
24-
minWidth: 1366,
25-
height: 768,
26-
minHeight: 768,
23+
width: WINDOW_WIDTH,
24+
minWidth: WINDOW_WIDTH,
25+
height: WINDOW_HEIGHT,
26+
minHeight: WINDOW_HEIGHT,
2727
webPreferences: {
28-
preload: `${__dirname }/preload.js`,
28+
preload: `${__dirname}/preload.js`,
2929
devTools: false,
3030
nodeIntegration: false,
3131
contextIsolation: true
@@ -36,7 +36,9 @@ async function createWindow() {
3636
fullscreen: true
3737
}
3838

39-
if (appBounds !== undefined && appBounds !== null) {Object.assign(BrowserWindowOptions, appBounds)}
39+
if (appBounds !== undefined && appBounds !== null) {
40+
Object.assign(BrowserWindowOptions, appBounds)
41+
}
4042
mainWindow = new BrowserWindow(BrowserWindowOptions)
4143

4244
// Remove menu bar
@@ -53,11 +55,13 @@ async function createWindow() {
5355
if (
5456
appBounds !== undefined &&
5557
appBounds !== null &&
56-
appBounds.width > width &&
57-
appBounds.height > height
58-
)
59-
{mainWindow.maximize()}
60-
else {mainWindow.show()}
58+
(appBounds.width ?? 0) > width &&
59+
(appBounds.height ?? 0) > height
60+
) {
61+
mainWindow.maximize()
62+
} else {
63+
mainWindow.show()
64+
}
6165

6266
// this will turn off always on top after opening the application
6367
setTimeout(() => {
@@ -73,60 +77,26 @@ async function createWindow() {
7377
}))
7478
}
7579

76-
// Settings handlers (global)
77-
ipcMain.handle('getSettings', () => ({
78-
diameterMode: (appConfig as any).get('setting.diameterMode'),
79-
defaultMetricOnStartup: (appConfig as any).get('setting.defaultMetricOnStartup'),
80-
selectedThreadingTab: (appConfig as any).get('setting.selectedThreadingTab', 0),
81-
selectedTurningTab: (appConfig as any).get('setting.selectedTurningTab', 0),
82-
selectedPitchTab: (appConfig as any).get('setting.selectedPitchTab', [0, 0]),
83-
pitchX: (appConfig as any).get('setting.pitchX', 0.0),
84-
pitchZ: (appConfig as any).get('setting.pitchZ', 0.0),
85-
encoderScaleZ: (appConfig as any).get('setting.encoderScaleZ', 0.001),
86-
encoderScaleX: (appConfig as any).get('setting.encoderScaleX', -0.001),
87-
tools: (appConfig as any).get('setting.tools'),
88-
currentToolIndex: (appConfig as any).get('setting.currentToolIndex', 0),
89-
currentToolOffsetX: (appConfig as any).get('setting.currentToolOffsetX', 0),
90-
currentToolOffsetZ: (appConfig as any).get('setting.currentToolOffsetZ', 0)
91-
}))
92-
93-
// IMPORTANT: This handler should save ALL settings passed from useSettings.ts
94-
// If you add new settings, make sure they're included both here AND in useSettings.ts
95-
// Do NOT create duplicate save logic elsewhere - use the useSettings saveSettings function
96-
ipcMain.handle('saveSettings', (event, settings) => {
97-
const currentSettings = (appConfig as any).get('setting', {})
98-
;(appConfig as any).set('setting', {
99-
...currentSettings,
100-
diameterMode: settings.diameterMode,
101-
defaultMetricOnStartup: settings.defaultMetricOnStartup,
102-
selectedThreadingTab: settings.selectedThreadingTab,
103-
selectedTurningTab: settings.selectedTurningTab,
104-
selectedPitchTab: settings.selectedPitchTab,
105-
pitchX: settings.pitchX,
106-
pitchZ: settings.pitchZ,
107-
encoderScaleZ: settings.encoderScaleZ,
108-
encoderScaleX: settings.encoderScaleX,
109-
tools: settings.tools,
110-
currentToolIndex: settings.currentToolIndex,
111-
currentToolOffsetX: settings.currentToolOffsetX,
112-
currentToolOffsetZ: settings.currentToolOffsetZ
113-
})
80+
// Settings handlers — transparent passthrough.
81+
// The schema (fields + defaults) lives in elle-frontend/src/composables/useSettings.ts.
82+
// This handler just reads/writes the `setting` blob so adding a new field only touches one file.
83+
ipcMain.handle('getSettings', () => appConfig.get<Settings>('setting', {} as Settings))
84+
85+
ipcMain.handle('saveSettings', (_event, settings: Settings) => {
86+
const current = appConfig.get<Settings>('setting', {} as Settings)
87+
appConfig.set('setting', { ...current, ...settings })
11488
return true
11589
})
11690

11791
app.commandLine.appendSwitch('gtk-version', '3')
11892

11993
app.whenReady().then(async () => {
120-
// Force dark mode for the entire application
121-
nativeTheme.themeSource = 'dark'
122-
12394
if (isDev) {
12495
try {
12596
const { installExt } = await import('./installDevTool.js')
12697
await installExt()
127-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
128-
} catch (_e) {
129-
// Ignore errors during dev tool installation
98+
} catch {
99+
// best-effort: dev tools are optional
130100
}
131101
}
132102
createWindow()
@@ -173,13 +143,13 @@ ipcMain.on('quit', () => {
173143
})
174144

175145
ipcMain.on('startHAL', () => {
176-
const hal_path = `${process.cwd() }/elle-hal`
177-
const halfile_path = `${process.cwd() }/elle-hal/lathe.hal`
146+
const hal_path = `${process.cwd()}/elle-hal`
147+
const halfile_path = `${process.cwd()}/elle-hal/lathe.hal`
178148
if (fs.existsSync(halfile_path)) {
179149
try {
180150
cleanMess()
181151
const env = process.env
182-
env.PATH += `:${ hal_path}`
152+
env.PATH += `:${hal_path}`
183153
halrun = spawn('unbuffer', ['linuxcnc', 'lathe.ini'], { cwd: hal_path, env: env })
184154
halrun.stdout?.on('data', (stdout: Buffer) => {
185155
mainWindow.webContents.send('halStdout', stdout.toString())
@@ -190,7 +160,7 @@ ipcMain.on('startHAL', () => {
190160
halrun.stderr?.on('data', (stderr: Buffer) => {
191161
mainWindow.webContents.send('halStdout', stderr.toString())
192162
})
193-
halrun.on('exit', (_code: any) => {
163+
halrun.on('exit', () => {
194164
mainWindow.webContents.send('halStopped')
195165
cleanMess()
196166
if (halquit) {
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
declare interface api {
2-
send: (channel: any, data: any) => void
3-
receive: (channel: any, func: any) => void
2+
send: (channel: string, data?: unknown) => void
3+
receive: (channel: string, func: (...args: unknown[]) => void) => void
44
}
55

66
declare interface Tool {
@@ -10,7 +10,26 @@ declare interface Tool {
1010
description: string
1111
}
1212

13+
// This shape must match the DEFAULTS object in elle-frontend/src/composables/useSettings.ts.
14+
// The frontend is the source of truth for the schema; this declaration is a runtime
15+
// handshake for the preload bridge.
16+
declare interface AppSettings {
17+
diameterMode: boolean
18+
defaultMetricOnStartup: boolean
19+
selectedThreadingTab: number
20+
selectedTurningTab: number
21+
selectedPitchTab: number[]
22+
pitchX: number
23+
pitchZ: number
24+
encoderScaleZ: number
25+
encoderScaleX: number
26+
tools: Tool[]
27+
currentToolIndex: number
28+
currentToolOffsetX: number
29+
currentToolOffsetZ: number
30+
}
31+
1332
declare interface settings {
14-
get: () => Promise<{ diameterMode: boolean; defaultMetricOnStartup: boolean; selectedThreadingTab: number; selectedTurningTab: number; selectedPitchTab: number[]; pitchX: number; pitchZ: number; encoderScaleZ: number; encoderScaleX: number; tools?: Tool[]; currentToolIndex?: number; currentToolOffsetX?: number; currentToolOffsetZ?: number }>
15-
save: (settings: { diameterMode: boolean; defaultMetricOnStartup: boolean; selectedThreadingTab: number; selectedTurningTab: number; selectedPitchTab: number[]; pitchX: number; pitchZ: number; encoderScaleZ: number; encoderScaleX: number; tools?: Tool[]; currentToolIndex?: number; currentToolOffsetX?: number; currentToolOffsetZ?: number }) => Promise<boolean>
33+
get: () => Promise<AppSettings>
34+
save: (settings: AppSettings) => Promise<boolean>
1635
}

elle-app/elle-electron/preload.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import { contextBridge, ipcRenderer } from 'electron'
2+
import type { Settings } from './electron-store/configuration.js'
23

34
contextBridge.exposeInMainWorld('browserWindow', {
45
versions: () => ipcRenderer.invoke('versions')
56
})
67

78
contextBridge.exposeInMainWorld('settings', {
89
get: () => ipcRenderer.invoke('getSettings'),
9-
save: (settings: any) => ipcRenderer.invoke('saveSettings', settings)
10+
save: (settings: Settings) => ipcRenderer.invoke('saveSettings', settings)
1011
})
1112

1213
contextBridge.exposeInMainWorld('api', {
13-
send: (channel: any, data: any) => {
14+
send: (channel: string, data?: unknown) => {
1415
const validChannels = ['startHAL', 'stopHAL', 'quit']
1516
if (validChannels.includes(channel)) {
1617
ipcRenderer.send(channel, data)
1718
}
1819
},
19-
receive: (channel: any, func: any) => {
20+
receive: (channel: string, func: (...args: unknown[]) => void) => {
2021
const validChannels = ['halStarted', 'halStopped', 'halStdout']
2122
if (validChannels.includes(channel)) {
22-
ipcRenderer.on(channel, (event, ...args) => func(...args))
23+
ipcRenderer.on(channel, (_event, ...args) => func(...args))
2324
}
2425
}
2526
})

0 commit comments

Comments
 (0)