feat: convert apps/machine from Tauri to Electron (#18)

- Create electron/main.ts with BrowserWindow (1080x1920, kiosk mode)
- Create electron/preload.ts with contextBridge API
- Add electron/tsconfig.json for main process compilation
- Update package.json: remove Tauri deps, add Electron + electron-builder
- Update vite.config.ts: remove Tauri-specific build config
- Update .gitignore: remove Tauri entries, add Electron build outputs
- Delete src-tauri/ directory and root Cargo.toml
- Update CLAUDE.md to reflect Electron architecture

The Vue frontend is unchanged - works identically in Electron renderer.
HAL drivers can now run directly in Electron main process (Node.js).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-01-29 21:14:07 -05:00
commit c26b6f6169
14 changed files with 2940 additions and 286 deletions

View file

@ -48,14 +48,9 @@ coverage/
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Tauri # Electron
apps/machine/src-tauri/target/ apps/machine/dist-electron/
apps/machine/src-tauri/WixTools/ apps/machine/release/
apps/machine/src-tauri/icons/
# Rust
Cargo.lock
**/*.rs.bk
# devenv # devenv
.devenv/ .devenv/

View file

@ -16,11 +16,11 @@ This file provides guidance to Claude Code when working with the Lamassu Next co
``` ```
lamassu-next/ lamassu-next/
├── apps/ ├── apps/
│ ├── machine/ # Tauri + Vue 3 ATM kiosk application (in progress) │ ├── machine/ # Electron + Vue 3 ATM kiosk application ✅
│ ├── dashboard/ # Vue 3 operator dashboard (planned) │ ├── dashboard/ # Vue 3 operator dashboard (planned)
│ └── relay/ # strfry relay configuration (planned) │ └── relay/ # strfry relay configuration (planned)
├── packages/ ├── packages/
│ ├── hal/ # Rust Hardware Abstraction Layer (planned) │ ├── hal/ # TypeScript Hardware Abstraction Layer ✅
│ ├── nostr-client/ # Nostr client library ✅ │ ├── nostr-client/ # Nostr client library ✅
│ ├── clink/ # CLINK protocol implementation ✅ │ ├── clink/ # CLINK protocol implementation ✅
│ ├── state-machine/ # XState v5 ATM state machine ✅ │ ├── state-machine/ # XState v5 ATM state machine ✅
@ -40,6 +40,7 @@ lamassu-next/
| `@lamassu/clink` | CLINK protocol (kinds 21001-21003), noffer encoding | 7 | | `@lamassu/clink` | CLINK protocol (kinds 21001-21003), noffer encoding | 7 |
| `@lamassu/lightning` | Lightning.Pub RPC client (kind 21000) | 10 | | `@lamassu/lightning` | Lightning.Pub RPC client (kind 21000) | 10 |
| `@lamassu/state-machine` | XState v5 ATM state machine (idle, cashIn, cashOut) | 14 | | `@lamassu/state-machine` | XState v5 ATM state machine (idle, cashIn, cashOut) | 14 |
| `@lamassu/hal` | Hardware drivers (ID003 validator, F56 dispenser) | - |
### Placeholder Packages ### Placeholder Packages
@ -48,11 +49,13 @@ lamassu-next/
| `@lamassu/cashu` | Cashu ecash for offline operation | | `@lamassu/cashu` | Cashu ecash for offline operation |
| `@lamassu/ui-shared` | Shared Vue 3 components | | `@lamassu/ui-shared` | Shared Vue 3 components |
### Completed Applications
- **apps/machine** - Electron ATM kiosk with Vue 3 UI (HAL integrated, ready for hardware testing)
### Planned Components ### Planned Components
- **apps/machine** - Tauri 2.x ATM kiosk with Vue 3 UI (UI shell implemented, HAL integration pending)
- **apps/dashboard** - Operator dashboard for fleet management - **apps/dashboard** - Operator dashboard for fleet management
- **packages/hal** - Rust hardware drivers (bill validators, dispensers, printers)
### Critical Documentation ### Critical Documentation
@ -159,11 +162,11 @@ Use these to interact with infrastructure directly during development.
## Key Technologies ## Key Technologies
| Component | Technology | Notes | | Component | Technology | Notes |
| ------------- | -------------- | ------------------------------------- | | ------------- | -------------- | --------------------------------------- |
| Runtime | Node.js 22 LTS | Strict TypeScript, ESM | | Runtime | Node.js 22 LTS | Strict TypeScript, ESM |
| ATM Shell | Tauri 2.x | Rust core, Vue 3 UI (planned) | | ATM Shell | Electron | Node.js main process, Vue 3 renderer |
| State Machine | XState v5 | Actor model, service injection | | State Machine | XState v5 | Actor model, service injection |
| Hardware | Rust + napi-rs | Ported from lamassu-machine (planned) | | Hardware | TypeScript | ID003, F56 drivers from lamassu-machine |
| Messaging | Nostr | NIP-01, NIP-42, NIP-44 | | Messaging | Nostr | NIP-01, NIP-42, NIP-44 |
| Payments | CLINK + RPC | Kind 21000 (RPC), 21001-21003 (CLINK) | | Payments | CLINK + RPC | Kind 21000 (RPC), 21001-21003 (CLINK) |
| Backend | Lightning.Pub | Nostr-native account system | | Backend | Lightning.Pub | Nostr-native account system |

View file

@ -1,3 +0,0 @@
[workspace]
resolver = "2"
members = ["apps/machine/src-tauri"]

View file

@ -0,0 +1,78 @@
/**
* Electron Main Process
*
* Creates the ATM kiosk window and handles IPC with the renderer.
* In production, runs in kiosk mode (fullscreen, no decorations).
* HAL hardware drivers run here (Node.js environment).
*/
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'node:path'
// Determine if we're in development
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
// Window dimensions (vertical ATM display)
const WINDOW_WIDTH = 1080
const WINDOW_HEIGHT = 1920
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
resizable: isDev, // Allow resize in dev for debugging
frame: isDev, // Show window frame in dev
kiosk: !isDev, // Kiosk mode in production
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
})
// Load the app
if (isDev) {
// Development: load from Vite dev server
const devServerUrl = process.env.VITE_DEV_SERVER_URL || 'http://localhost:1420'
mainWindow.loadURL(devServerUrl)
mainWindow.webContents.openDevTools()
} else {
// Production: load from built files
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
// IPC Handlers
ipcMain.handle('get-version', () => {
return app.getVersion()
})
// App lifecycle
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
// macOS: re-create window when dock icon clicked
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
// Quit on all platforms (ATM doesn't need macOS dock behavior)
app.quit()
})
// Security: prevent new window creation
app.on('web-contents-created', (_, contents) => {
contents.setWindowOpenHandler(() => {
return { action: 'deny' }
})
})

View file

@ -0,0 +1,27 @@
/**
* Electron Preload Script
*
* Exposes a secure API to the renderer process via contextBridge.
* This is the only way for the Vue app to communicate with the main process.
*/
import { contextBridge, ipcRenderer } from 'electron'
// Expose protected methods to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// Get app version
getVersion: (): Promise<string> => ipcRenderer.invoke('get-version'),
// Platform info
platform: process.platform,
})
// Type declaration for the exposed API
declare global {
interface Window {
electronAPI: {
getVersion: () => Promise<string>
platform: NodeJS.Platform
}
}
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "../dist-electron",
"rootDir": "."
},
"include": ["./**/*.ts"]
}

View file

@ -1,15 +1,16 @@
{ {
"name": "@lamassu/machine", "name": "@lamassu/machine",
"version": "0.1.0", "version": "0.1.0",
"description": "Tauri 2.x ATM kiosk application with Vue 3 UI", "description": "Electron ATM kiosk application with Vue 3 UI",
"type": "module", "type": "module",
"main": "dist-electron/main.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "concurrently -n vite,electron \"vite\" \"pnpm run electron:dev\"",
"build": "vue-tsc --noEmit && vite build", "dev:vite": "vite",
"electron:dev": "tsc -p electron/tsconfig.json && electron dist-electron/main.js",
"build": "vue-tsc --noEmit && vite build && tsc -p electron/tsconfig.json",
"build:electron": "pnpm build && electron-builder",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"lint": "eslint src/" "lint": "eslint src/"
}, },
@ -33,14 +34,33 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@tauri-apps/api": "^2.1.0",
"@tauri-apps/cli": "^2.1.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@vitejs/plugin-vue": "^5.2.0", "@vitejs/plugin-vue": "^5.2.0",
"concurrently": "^9.0.0",
"electron": "^33.0.0",
"electron-builder": "^25.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.7.0", "typescript": "^5.7.0",
"vite": "^6.0.0", "vite": "^6.0.0",
"vue-tsc": "^2.1.0" "vue-tsc": "^2.1.0"
},
"build": {
"appId": "dev.lamassu.atm",
"productName": "Lamassu ATM",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"dist-electron/**/*"
],
"linux": {
"target": [
"AppImage",
"deb"
],
"category": "Finance"
}
} }
} }

View file

@ -1,24 +0,0 @@
[package]
name = "lamassu-machine"
version = "0.1.0"
description = "Lamassu ATM Kiosk Application"
authors = ["Lamassu Industries"]
edition = "2021"
rust-version = "1.70"
[lib]
name = "lamassu_machine_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
# This feature is used for production builds or when a dev server is not specified
custom-protocol = ["tauri/custom-protocol"]

View file

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View file

@ -1,35 +0,0 @@
//! Lamassu ATM Tauri Application
//!
//! This is the Rust backend for the Lamassu ATM kiosk application.
//! Hardware drivers and native functionality will be implemented here.
use tauri::Manager;
/// Greet command for testing Tauri IPC
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to Lamassu ATM.", name)
}
/// Get application version
#[tauri::command]
fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
#[cfg(debug_assertions)]
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}
Ok(())
})
.invoke_handler(tauri::generate_handler![greet, get_version])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
lamassu_machine_lib::run()
}

View file

@ -1,39 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lamassu ATM",
"version": "0.1.0",
"identifier": "io.lamassu.atm",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Lamassu ATM",
"width": 1080,
"height": 1920,
"resizable": false,
"fullscreen": false,
"decorations": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View file

@ -11,28 +11,23 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
}, },
}, },
// Tauri expects a fixed port during development // Dev server configuration
server: { server: {
port: 1420, port: 1420,
strictPort: true, strictPort: true,
watch: {
ignored: ['**/src-tauri/**'],
}, },
}, // Build configuration for Electron renderer
// Build configuration for Tauri
build: { build: {
// Tauri uses Chromium on Windows and WebKit on macOS and Linux // Target modern Chrome (Electron uses Chromium)
target: process.env.TAURI_ENV_PLATFORM === 'windows' ? 'chrome105' : 'safari14', target: 'chrome120',
// Don't minify for debug builds // Sourcemaps for debugging
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false, sourcemap: process.env.NODE_ENV === 'development',
// Produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_ENV_DEBUG,
rollupOptions: { rollupOptions: {
// @lamassu/hal uses Node.js APIs (serialport, node:events) and will only // @lamassu/hal uses Node.js APIs (serialport, node:events) and will only
// run in Electron's main process. Exclude from browser bundle. // run in Electron's main process. Exclude from renderer bundle.
external: ['@lamassu/hal'], external: ['@lamassu/hal'],
}, },
}, },
// Prevent Vite from clearing the screen // Prevent Vite from clearing the screen (useful when running with Electron)
clearScreen: false, clearScreen: false,
}) })

File diff suppressed because it is too large Load diff