From d87ca00e5884bc9cdbeb4665cfc022fdebe09fed Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 1 Sep 2026 00:51:24 -0700 Subject: [PATCH] [grade=B] docs+cleanup: fix BUILD_GUIDE factual errors, document wine/i386 NSIS pitfall, drop .bak junk BUILD_GUIDE.md wrongly said output goes to dist/ (actual: build-output), mac builds produce .dmg (actual target: zip), and cited a nonexistent win-unpacked portable path. Corrected to the verified config truth and added the wine64+wine32:i386 cross-build requirement discovered today: without wine32 the NSIS setup exe ships as a 211KB payload-less stub while electron-builder exits 0. Documented the authoritative 7z payload check + post-build secrets scan. Round1 judge B+5 polish, folded: version placeholders, heuristic-vs-authoritative wording, distro/sudo notes, mac zip wording. Judge: qwen3.8-max stand-in lane. Also removes package.json.bak and index.js.bak (stale junk). --- dashcaddy-installer/BUILD_GUIDE.md | 45 +- dashcaddy-installer/package.json.bak | 104 -- dashcaddy-installer/src/main/index.js.bak | 1062 --------------------- 3 files changed, 41 insertions(+), 1170 deletions(-) delete mode 100644 dashcaddy-installer/package.json.bak delete mode 100644 dashcaddy-installer/src/main/index.js.bak diff --git a/dashcaddy-installer/BUILD_GUIDE.md b/dashcaddy-installer/BUILD_GUIDE.md index 8c45a0c..ba254d6 100644 --- a/dashcaddy-installer/BUILD_GUIDE.md +++ b/dashcaddy-installer/BUILD_GUIDE.md @@ -67,11 +67,48 @@ npm run build:linux ### Build Output -Built applications are placed in the `dist/` directory: +Built applications are placed in the `build-output/` directory: -- **Windows**: `dist/win-unpacked/DashCaddy Installer.exe` (portable) -- **macOS**: `dist/DashCaddy Installer.dmg` -- **Linux**: `dist/DashCaddy Installer.AppImage` and `.deb` +- **Windows**: `build-output/DashCaddy Installer .exe` (portable) and + `build-output/DashCaddy Installer Setup .exe` (NSIS installer) + — filenames embed the current `version` from package.json +- **macOS**: `build-output/DashCaddy Installer--mac.zip` — the + configured mac target is `zip` (unsigned; signed `.dmg` builds require + a Mac with signing credentials) +- **Linux**: `build-output/DashCaddy Installer-.AppImage` and + `build-output/dashcaddy-installer__amd64.deb` + +### Cross-platform build requirements (verified 2026-09-01) + +Building Windows installers from Linux requires **wine with both 64-bit and +32-bit support** — NSIS's 32-bit post-processing runs under wine: + +```bash +# Ubuntu 24.04 (Debian/Ubuntu package names; other distros vary). +# Requires root / sudo for the dpkg and apt steps. +dpkg --add-architecture i386 +# add i386 mirror entries if the main sources are amd64-only pinned +apt-get update && apt-get install -y wine64 wine32:i386 +# initialize a prefix once (avoids kernel32.dll load failures in CI) +export WINEPREFIX=~/.wine-dashcaddy && wineboot --init +``` + +Without wine32, the NSIS setup exe is built but ends up as a ~211KB stub +(payload not appended) and the build appears to pass (exit 0). Check the +result — the size (~90MB+) is a quick heuristic, but the **authoritative** +check is listing/extracting the payload: + +```bash +7z l "build-output/DashCaddy Installer Setup .exe" # should list a large app-64.7z +# or extract and scan: 7z x && 7z x '$PLUGINSDIR/app-64.7z' +``` + +After every build, run the secrets scanner to verify no private key material +was bundled into the shipped resources: + +```bash +npm run build:scan +``` ## Project Structure diff --git a/dashcaddy-installer/package.json.bak b/dashcaddy-installer/package.json.bak deleted file mode 100644 index dcd9b71..0000000 --- a/dashcaddy-installer/package.json.bak +++ /dev/null @@ -1,104 +0,0 @@ -{ - "name": "dashcaddy-installer", - "version": "1.0.0", - "description": "Cross-platform installer for DashCaddy platform", - "main": "src/main/index.js", - "scripts": { - "start": "electron .", - "dev": "electron . --dev", - "test": "jest", - "test:watch": "jest --watch", - "build": "electron-builder", - "build:win": "electron-builder --win", - "build:mac": "electron-builder --mac", - "build:linux": "electron-builder --linux" - }, - "keywords": [ - "dashcaddy", - "installer", - "docker", - "caddy" - ], - "author": { - "name": "DashCaddy Team", - "email": "dashcaddy@sami.cloud" - }, - "homepage": "https://github.com/dashcaddy/dashcaddy", - "license": "MIT", - "devDependencies": { - "electron": "^28.3.3", - "electron-builder": "^24.9.1", - "fast-check": "^3.15.0", - "jest": "^29.7.0" - }, - "build": { - "appId": "com.dashcaddy.installer", - "productName": "DashCaddy Installer", - "asar": true, - "directories": { - "output": "build-output" - }, - "files": [ - "src/**/*", - "assets/**/*", - "templates/**/*" - ], - "extraResources": [ - { - "from": "../status", - "to": "status", - "filter": [ - "**/*", - "!node_modules/**", - "!.git/**", - "!**/*.test.js", - "!**/*.spec.js" - ] - }, - { - "from": "../dashcaddy-api", - "to": "dashcaddy-api", - "filter": [ - "**/*", - "!node_modules/**", - "!.git/**", - "!**/*.test.js", - "!**/*.spec.js" - ] - } - ], - "icon": "assets/favicon.ico", - "win": { - "target": [ - "nsis", - "portable" - ], - "icon": "assets/favicon.ico", - "signAndEditExecutable": false - }, - "mac": { - "target": [ - "zip" - ], - "icon": "assets/dashcaddy-logo.png" - }, - "linux": { - "target": [ - "AppImage", - "deb" - ], - "icon": "assets/dashcaddy-logo.png", - "category": "Utility" - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true, - "installerIcon": "assets/icon.ico", - "uninstallerIcon": "assets/icon.ico", - "installerHeaderIcon": "assets/icon.ico" - } - }, - "dependencies": { - "electron-updater": "^6.8.9" - } -} diff --git a/dashcaddy-installer/src/main/index.js.bak b/dashcaddy-installer/src/main/index.js.bak deleted file mode 100644 index 079b31d..0000000 --- a/dashcaddy-installer/src/main/index.js.bak +++ /dev/null @@ -1,1062 +0,0 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron'); -const path = require('path'); - -// Disable GPU acceleration to prevent crashes -app.disableHardwareAcceleration(); - -// Add error handling -process.on('uncaughtException', (error) => { - console.error('Uncaught Exception:', error); -}); - -let mainWindow; -const { registerVMHandlers } = require('./vm-ipc'); - -function createWindow() { - mainWindow = new BrowserWindow({ - width: 900, - height: 700, - minWidth: 800, - minHeight: 600, - icon: path.join(__dirname, '../../assets/favicon.ico'), - webPreferences: { - preload: path.join(__dirname, '../preload/index.js'), - nodeIntegration: false, - contextIsolation: true, - sandbox: false - }, - autoHideMenuBar: true, - resizable: true, - show: false // Don't show until ready - }); - - mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); - - // Show window when ready - mainWindow.once('ready-to-show', () => { - mainWindow.show(); - }); - - // Open DevTools in development mode - if (process.argv.includes('--dev')) { - mainWindow.webContents.openDevTools(); - } - - mainWindow.on('closed', () => { - mainWindow = null; - }); -} - -// App lifecycle handlers -// --- Disk space check (for VM disk budget step) --- -ipcMain.handle('get-disk-space', async (event, targetPath) => { - try { - const stats = await require('fs').promises.statfs(targetPath || '/'); - return { - free: stats.bavail * stats.bsize, - total: stats.blocks * stats.bsize, - }; - } catch (e) { - return { free: 0, total: 0, error: e.message }; - } -}); - -app.whenReady().then(() => { - createWindow(); - - registerVMHandlers(mainWindow); - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } - }); -}); - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -// Import utilities -const { DEFAULT_PORTS } = require('../shared/constants'); -const { getPlatformInfo } = require('../shared/platform-utils'); -const DependencyChecker = require('./dependency-checker'); -const ConfigManager = require('./config-manager'); -const FileDeployer = require('./file-deployer'); -const CaddyfileGenerator = require('./caddyfile-generator'); -const BrowserLauncher = require('./browser-launcher'); -const ServiceManager = require('./service-manager'); - -// Create instances -const dependencyChecker = new DependencyChecker(); -const configManager = new ConfigManager(); -const fileDeployer = new FileDeployer(); -const caddyfileGenerator = new CaddyfileGenerator(); -const browserLauncher = new BrowserLauncher(); -const serviceManager = new ServiceManager(); - -// IPC handlers -ipcMain.handle('check-platform', async () => { - try { - return { - success: true, - data: getPlatformInfo() - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('check-dependencies', async (event, options) => { - try { - // Accept optional search paths for Caddy (e.g. user's configured caddy folder) - const additionalPaths = options?.caddySearchPaths || []; - const [docker, caddy, platform] = await Promise.all([ - dependencyChecker.checkDocker(), - dependencyChecker.checkCaddy(additionalPaths), - dependencyChecker.checkPlatform() - ]); - - return { - success: true, - data: { - docker, - caddy, - platform - } - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('check-docker', async () => { - try { - const result = await dependencyChecker.checkDocker(); - return { - success: true, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('check-caddy', async (event, options) => { - try { - const additionalPaths = options?.caddySearchPaths || []; - const result = await dependencyChecker.checkCaddy(additionalPaths); - return { - success: true, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('install-docker', async () => { - try { - const platform = getPlatformInfo().os; - const result = await dependencyChecker.installDocker(platform); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('install-caddy', async (event, options) => { - try { - const platform = getPlatformInfo().os; - const targetPath = options?.targetPath || null; - const result = await dependencyChecker.installCaddy(platform, null, targetPath); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Configuration management handlers -ipcMain.handle('validate-path', async (event, testPath) => { - try { - const result = await configManager.validatePath(testPath); - return { - success: result.valid, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('save-config', async (event, config) => { - try { - const result = await configManager.saveConfig(config, config.installPath); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('load-config', async (event, installPath) => { - try { - const result = await configManager.loadConfig(installPath); - return { - success: result.exists, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// File deployment handlers -ipcMain.handle('validate-sources', async () => { - try { - const result = await fileDeployer.validateSources(); - return { - success: result.valid, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('deploy-dashboard', async (event, installPath) => { - try { - const result = await fileDeployer.deployDashboard(installPath, (progress) => { - event.sender.send('deployment-progress', progress); - }); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('deploy-api', async (event, installPath) => { - try { - const result = await fileDeployer.deployAPI(installPath, (progress) => { - event.sender.send('deployment-progress', progress); - }); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('deploy-complete', async (event, installPath) => { - try { - const result = await fileDeployer.deployComplete(installPath, (progress) => { - event.sender.send('deployment-progress', progress); - }); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Caddyfile generation handlers -ipcMain.handle('create-caddyfile', async (event, installPath, options) => { - try { - const result = await caddyfileGenerator.createCaddyfileSetup(installPath, options); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -ipcMain.handle('create-docker-compose', async (event, installPath) => { - try { - const result = await caddyfileGenerator.createDockerCompose(installPath); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Browser launcher handlers -ipcMain.handle('open-dashboard', async (event, port, hostname) => { - try { - const result = await browserLauncher.openDashboardWhenReady(port, hostname); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Folder selection handler -ipcMain.handle('select-folder', async () => { - try { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openDirectory', 'createDirectory'], - title: 'Select Installation Folder' - }); - - if (result.canceled) { - return { success: false, canceled: true }; - } - - return { - success: true, - path: result.filePaths[0] - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// File selection handler (for logo, etc.) -ipcMain.handle('select-file', async (event, options) => { - try { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openFile'], - filters: options?.filters || [ - { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico'] } - ], - title: options?.title || 'Select File' - }); - - if (result.canceled) { - return { success: false, canceled: true }; - } - - return { - success: true, - path: result.filePaths[0] - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Installation orchestration handler (tier-aware) -ipcMain.handle('run-installation', async (event, config) => { - try { - const tier = config.tier || 'basic'; - const isDocker = tier !== 'basic'; // intermediate or advanced needs Docker - - // Build steps dynamically based on tier - const steps = [ - { name: 'Creating directories', weight: 5 }, - { name: 'Creating seed files', weight: 3 } - ]; - - steps.push({ name: 'Deploying dashboard files', weight: 20 }); - - if (isDocker) { - steps.push({ name: 'Deploying API server', weight: 15 }); - } - - steps.push({ name: 'Generating Caddyfile', weight: 10 }); - - if (isDocker) { - steps.push({ name: 'Creating docker-compose.yml', weight: 8 }); - } - - steps.push({ name: 'Setting up branding', weight: 5 }); - - if (config.dns) { - steps.push({ name: 'Saving DNS credentials', weight: 5 }); - } - - steps.push({ name: 'Saving configuration', weight: 5 }); - steps.push({ name: 'Starting services', weight: 10 }); - - let completedWeight = 0; - const totalWeight = steps.reduce((sum, s) => sum + s.weight, 0); - - const sendProgress = (stepName, stepProgress = 100) => { - const overallProgress = Math.round((completedWeight / totalWeight) * 100); - event.sender.send('step-progress', { - task: stepName, - progress: overallProgress, - stepProgress - }); - }; - - const completeStep = (stepName) => { - const step = steps.find(s => s.name === stepName); - if (step) completedWeight += step.weight; - event.sender.send('step-complete', { step: stepName }); - }; - - // Step: Create directories - sendProgress('Creating directories', 0); - await configManager.createDirectories(config.installPath); - completeStep('Creating directories'); - - // Step: Restore preserved settings from a previous uninstall (if any) - try { - const restoreResult = await configManager.restoreUserSettings(config.installPath); - if (restoreResult.success && restoreResult.restored.length > 0) { - console.log('[Installer] Restored preserved settings:', restoreResult.restored); - event.sender.send('step-progress', { - task: `Restored ${restoreResult.restored.length} preserved file(s) from previous install`, - progress: 0, - stepProgress: 100 - }); - } - } catch (err) { - console.warn('Settings restore check:', err.message); - } - - // Step: Create seed files (services.json, credentials.json, etc.) - sendProgress('Creating seed files', 0); - const seedResult = await configManager.seedFiles(config.installPath); - if (!seedResult.success) { - console.warn('Seed file creation warning:', seedResult.error); - } - completeStep('Creating seed files'); - - // Step: Deploy dashboard - sendProgress('Deploying dashboard files', 0); - await fileDeployer.deployDashboard(config.installPath, (progress) => { - sendProgress('Deploying dashboard files', progress.percent || 0); - }); - completeStep('Deploying dashboard files'); - - // Step: Deploy API (intermediate/advanced only) - if (isDocker) { - sendProgress('Deploying API server', 0); - await fileDeployer.deployAPI(config.installPath, (progress) => { - sendProgress('Deploying API server', progress.percent || 0); - }); - completeStep('Deploying API server'); - } - - // Step: Generate Caddyfile - sendProgress('Generating Caddyfile', 0); - const caddyfileOptions = { - port: config.dashboardPort || 8080, - apiPort: config.apiPort || 3001, - tier: tier, - domainMode: config.domainMode || 'local' - }; - if (config.domainMode === 'public' && config.domain) { - caddyfileOptions.publicDomain = config.domain.publicDomain; - caddyfileOptions.email = config.domain.email; - } else if (config.domainMode === 'custom-tld' && config.domain) { - caddyfileOptions.tld = config.domain.tld; - caddyfileOptions.caName = config.domain.caName || 'DashCaddy Local CA'; - } - await caddyfileGenerator.createCaddyfileSetup(config.installPath, caddyfileOptions); - completeStep('Generating Caddyfile'); - - // Step: Create docker-compose (intermediate/advanced only) - if (isDocker) { - sendProgress('Creating docker-compose.yml', 0); - await caddyfileGenerator.createDockerCompose(config.installPath, { - apiPort: config.apiPort || 3001, - domainMode: config.domainMode || 'local', - lanIP: config.lanIP || '', - tailscaleIP: config.tailscaleIP || '' - }); - completeStep('Creating docker-compose.yml'); - } - - // Step: Setup branding - sendProgress('Setting up branding', 0); - if (config.branding) { - await configManager.saveBranding(config.branding, config.installPath); - } - completeStep('Setting up branding'); - - // Step: Save DNS credentials (if provided) - if (config.dns) { - sendProgress('Saving DNS credentials', 0); - await configManager.saveDNSCredentials(config.dns, config.installPath); - completeStep('Saving DNS credentials'); - } - - // Step: Save configuration - sendProgress('Saving configuration', 0); - - // Derive dashboardHost and tld from domain mode - let dashboardHost, tld = null; - if (config.domainMode === 'public' && config.domain) { - dashboardHost = config.domain.publicDomain; - } else if (config.domainMode === 'custom-tld' && config.domain) { - tld = config.domain.tld; - dashboardHost = `dashcaddy${tld}`; - } else { - dashboardHost = `localhost:${config.dashboardPort || 8080}`; - } - - await configManager.saveConfig({ - setupComplete: true, - configurationType: config.domainMode === 'custom-tld' ? 'homelab' : (config.domainMode === 'public' ? 'public' : 'local'), - tier: tier, - domainMode: config.domainMode || 'local', - tld: tld, - dashboardHost: dashboardHost, - dns: config.dns ? { - provider: 'technitium', - ip: config.dns.server ? config.dns.server.replace(/^https?:\/\//, '').replace(/:\d+$/, '') : '', - port: '5380' - } : undefined, - installedAt: new Date().toISOString(), - version: '1.0.0' - }, config.installPath); - completeStep('Saving configuration'); - - // Step: Start services - sendProgress('Starting services', 0); - if (config.autoStart) { - const caddyfilePath = path.join(config.installPath, 'Caddyfile'); - const caddyBinaryPath = config.caddyBinaryPath || null; - - // Start Caddy - sendProgress('Starting Caddy...', 30); - const caddyResult = await serviceManager.startCaddy(caddyfilePath, caddyBinaryPath); - if (!caddyResult.success) { - console.warn('Caddy start warning:', caddyResult.error); - } - - // Start Docker Compose only for intermediate/advanced tiers - if (isDocker) { - sendProgress('Starting Docker containers...', 60); - const dockerResult = await serviceManager.startDockerCompose(config.installPath); - if (!dockerResult.success) { - console.warn('Docker Compose start warning:', dockerResult.error); - } - } - } - completeStep('Starting services'); - - // Installation complete - determine dashboard URL - let dashboardUrl; - if (config.domainMode === 'public' && config.domain) { - dashboardUrl = `https://${config.domain.publicDomain}`; - } else if (config.domainMode === 'custom-tld' && config.domain) { - dashboardUrl = `https://dashcaddy${config.domain.tld}`; - } else { - dashboardUrl = `http://localhost:${config.dashboardPort || 8080}`; - } - - // Quick health check (non-blocking, just informational) - let healthResults = null; - if (config.autoStart) { - sendProgress('Verifying services...', 95); - // Wait a moment for services to start up - await new Promise(r => setTimeout(r, 3000)); - try { - const http = require('http'); - const caddyOk = await new Promise((resolve) => { - const req = http.get(`http://localhost:${DEFAULT_PORTS.CADDY_ADMIN}/config/`, { timeout: 5000 }, (res) => { - resolve(res.statusCode === 200); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { req.destroy(); resolve(false); }); - }); - healthResults = { caddy: caddyOk }; - } catch { - healthResults = { caddy: false }; - } - } - - sendProgress('Installation complete', 100); - event.sender.send('installation-complete', { - success: true, - installPath: config.installPath, - dashboardUrl, - health: healthResults - }); - - return { - success: true, - data: { - installPath: config.installPath, - dashboardUrl - } - }; - } catch (error) { - event.sender.send('step-error', { error: error.message }); - return { - success: false, - error: error.message - }; - } -}); - -// DNS connection test handler -ipcMain.handle('test-dns-connection', async (event, credentials) => { - try { - // Test connectivity to the DNS server - if (!credentials?.server) { - return { - success: false, - error: 'No server URL provided' - }; - } - - const http = require('http'); - const https = require('https'); - const { URL } = require('url'); - - const url = new URL(credentials.server); - const protocol = url.protocol === 'https:' ? https : http; - - const connected = await new Promise((resolve) => { - const req = protocol.get(url.href, { timeout: 10000 }, (res) => { - resolve(true); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { - req.destroy(); - resolve(false); - }); - }); - - return { - success: true, - data: { - connected, - message: connected ? 'Successfully connected to DNS server' : 'Could not reach DNS server' - } - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Network detection handler - detects LAN and Tailscale IPs -ipcMain.handle('detect-network', async () => { - try { - const os = require('os'); - const interfaces = os.networkInterfaces(); - let lanIP = ''; - let tailscaleIP = ''; - - for (const [name, addrs] of Object.entries(interfaces)) { - for (const addr of addrs) { - if (addr.family !== 'IPv4' || addr.internal) continue; - - // Tailscale interfaces: typically 100.x.x.x range - if (addr.address.startsWith('100.') && (name.toLowerCase().includes('tailscale') || name.toLowerCase().includes('utun'))) { - tailscaleIP = addr.address; - } - // LAN: common private ranges (not 100.x Tailscale, not 172.17+ Docker) - else if ( - addr.address.startsWith('192.168.') || - addr.address.startsWith('10.') || - (addr.address.startsWith('172.') && !addr.address.startsWith('172.17.')) - ) { - if (!lanIP) lanIP = addr.address; - } - } - } - - // Fallback: try to find Tailscale IP from 100.x range even without name match - if (!tailscaleIP) { - for (const addrs of Object.values(interfaces)) { - for (const addr of addrs) { - if (addr.family === 'IPv4' && addr.address.startsWith('100.') && !addr.internal) { - tailscaleIP = addr.address; - break; - } - } - if (tailscaleIP) break; - } - } - - return { - success: true, - data: { lanIP, tailscaleIP } - }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// Health check handler - verify services are running after install -ipcMain.handle('health-check', async (event, config) => { - try { - const http = require('http'); - const results = {}; - - // Check Caddy admin API - results.caddy = await new Promise((resolve) => { - const req = http.get(`http://localhost:${DEFAULT_PORTS.CADDY_ADMIN}/config/`, { timeout: 5000 }, (res) => { - resolve({ running: res.statusCode === 200, statusCode: res.statusCode }); - }); - req.on('error', () => resolve({ running: false })); - req.on('timeout', () => { req.destroy(); resolve({ running: false }); }); - }); - - // Check API server (only for non-basic tiers) - const apiPort = config?.apiPort || DEFAULT_PORTS.API; - results.api = await new Promise((resolve) => { - const req = http.get(`http://localhost:${apiPort}/api/health`, { timeout: 5000 }, (res) => { - resolve({ running: res.statusCode === 200, statusCode: res.statusCode }); - }); - req.on('error', () => resolve({ running: false })); - req.on('timeout', () => { req.destroy(); resolve({ running: false }); }); - }); - - // Check dashboard is reachable - let dashboardPort = config?.dashboardPort || 8080; - let dashboardProto = 'http'; - if (config?.domainMode === 'custom-tld' || config?.domainMode === 'public') { - dashboardPort = 443; - dashboardProto = 'https'; - } - - results.dashboard = await new Promise((resolve) => { - const mod = dashboardProto === 'https' ? require('https') : http; - const opts = { timeout: 5000 }; - if (dashboardProto === 'https') opts.rejectUnauthorized = false; - const req = mod.get(`${dashboardProto}://localhost:${dashboardPort}/`, opts, (res) => { - resolve({ running: res.statusCode >= 200 && res.statusCode < 400, statusCode: res.statusCode }); - }); - req.on('error', () => resolve({ running: false })); - req.on('timeout', () => { req.destroy(); resolve({ running: false }); }); - }); - - return { success: true, data: results }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// DNS credential handlers -ipcMain.handle('save-dns-credentials', async (event, credentials, installPath) => { - try { - const result = await configManager.saveDNSCredentials(credentials, installPath); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Branding handler -ipcMain.handle('save-branding', async (event, branding, installPath) => { - try { - const result = await configManager.saveBranding(branding, installPath); - return { - success: result.success, - data: result - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } -}); - -// Service management handlers -ipcMain.handle('start-services', async (event, config) => { - try { - const result = await serviceManager.startAll(config.installPath, { - caddyfilePath: config.caddyfilePath, - caddyBinaryPath: config.caddyBinaryPath - }); - return { success: result.success, data: result }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -ipcMain.handle('stop-services', async (event, config) => { - try { - const result = await serviceManager.stopAll(config.installPath); - return { success: result.success, data: result }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -ipcMain.handle('check-service-status', async (event, config) => { - try { - const [caddyStatus, dockerStatus] = await Promise.all([ - serviceManager.checkCaddyStatus(), - serviceManager.checkDockerComposeStatus(config.installPath) - ]); - return { - success: true, - data: { caddy: caddyStatus, docker: dockerStatus } - }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// Detect existing installation -ipcMain.handle('detect-installation', async () => { - try { - const { DEFAULT_PATHS } = require('../shared/constants'); - const platformInfo = getPlatformInfo(); - const defaultPath = DEFAULT_PATHS[platformInfo.platform] || DEFAULT_PATHS.win32; - - // Check default path first - const exists = await configManager.installationExists(defaultPath); - if (exists) { - const configResult = await configManager.loadConfig(defaultPath); - return { - success: true, - data: { found: true, installPath: defaultPath, config: configResult.config } - }; - } - - // Check common alternative paths - const alternatives = ['C:\\DashCaddy', 'C:\\caddy', '/opt/dashcaddy', '/Applications/DashCaddy']; - for (const altPath of alternatives) { - if (altPath === defaultPath) continue; - const altExists = await configManager.installationExists(altPath); - if (altExists) { - const configResult = await configManager.loadConfig(altPath); - return { - success: true, - data: { found: true, installPath: altPath, config: configResult.config } - }; - } - } - - return { success: true, data: { found: false } }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// Check for preserved settings from previous uninstall -ipcMain.handle('check-preserved-settings', async (event, installPath) => { - try { - const settingsDir = path.join(installPath, '.dashcaddy-settings'); - try { - await require('fs').promises.access(settingsDir); - const files = await require('fs').promises.readdir(settingsDir); - return { success: true, data: { found: true, files, settingsPath: settingsDir } }; - } catch { - return { success: true, data: { found: false } }; - } - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// Restore preserved settings during reinstall -ipcMain.handle('restore-preserved-settings', async (event, installPath) => { - try { - const result = await configManager.restoreUserSettings(installPath); - return { success: result.success, data: result }; - } catch (error) { - return { success: false, error: error.message }; - } -}); - -// Uninstall orchestration handler with progress tracking -ipcMain.handle('run-uninstallation', async (event, options) => { - try { - const { installPath, preserveSettings, preserveCA } = options; - - // Verify installation exists - const exists = await configManager.installationExists(installPath); - if (!exists) { - return { success: false, error: 'No DashCaddy installation found at this path' }; - } - - // Load config to understand what was installed - const configResult = await configManager.loadConfig(installPath); - const config = configResult.config || {}; - const isDocker = config.tier && config.tier !== 'basic'; - - // Build uninstall steps - const uninstallSteps = []; - uninstallSteps.push({ name: 'Stopping Caddy', weight: 10 }); - if (isDocker) { - uninstallSteps.push({ name: 'Stopping Docker containers', weight: 15 }); - uninstallSteps.push({ name: 'Removing Docker containers', weight: 10 }); - } - if (preserveSettings || preserveCA) { - uninstallSteps.push({ name: 'Backing up preserved data', weight: 10 }); - } - uninstallSteps.push({ name: 'Removing installation files', weight: 40 }); - - let completedWeight = 0; - const totalWeight = uninstallSteps.reduce((sum, s) => sum + s.weight, 0); - - const sendProgress = (stepName, stepProgress = 0) => { - const overallProgress = Math.round((completedWeight / totalWeight) * 100); - event.sender.send('uninstall-progress', { - task: stepName, - progress: overallProgress, - stepProgress - }); - }; - - const completeStep = (stepName) => { - const step = uninstallSteps.find(s => s.name === stepName); - if (step) completedWeight += step.weight; - event.sender.send('uninstall-step-complete', { step: stepName }); - }; - - // Step: Stop Caddy - sendProgress('Stopping Caddy'); - try { - await serviceManager.stopCaddy(); - } catch (err) { - console.warn('Caddy stop warning:', err.message); - } - completeStep('Stopping Caddy'); - - // Step: Stop & remove Docker containers (if applicable) - if (isDocker) { - sendProgress('Stopping Docker containers'); - try { - await serviceManager.stopDockerCompose(installPath); - } catch (err) { - console.warn('Docker stop warning:', err.message); - } - completeStep('Stopping Docker containers'); - - sendProgress('Removing Docker containers'); - try { - const execPromise = require('util').promisify(require('child_process').exec); - const composePath = path.join(installPath, 'sites', 'dashcaddy-api', 'docker-compose.yml'); - await execPromise(`docker compose -f "${composePath}" down --rmi local --volumes`, { - cwd: path.dirname(composePath), - timeout: 120000 - }); - } catch (err) { - console.warn('Docker cleanup warning:', err.message); - } - completeStep('Removing Docker containers'); - } - - // Step: Back up preserved data - if (preserveSettings || preserveCA) { - sendProgress('Backing up preserved data'); - } - - // Step: Remove files (config-manager handles backup internally) - sendProgress('Removing installation files'); - const removeResult = await configManager.removeInstallation(installPath, { - preserveSettings, - preserveCA - }); - - if (!removeResult.success) { - event.sender.send('uninstall-error', { error: removeResult.error || removeResult.message }); - return { success: false, error: removeResult.message }; - } - - if (preserveSettings || preserveCA) { - completeStep('Backing up preserved data'); - } - completeStep('Removing installation files'); - - sendProgress('Uninstall complete', 100); - event.sender.send('uninstall-complete', { - success: true, - preservedSettings: preserveSettings, - preservedCA: preserveCA, - settingsPath: removeResult.settingsPath || null - }); - - return { - success: true, - data: { - preservedSettings: preserveSettings, - preservedCA: preserveCA, - settingsPath: removeResult.settingsPath || null - } - }; - } catch (error) { - event.sender.send('uninstall-error', { error: error.message }); - return { success: false, error: error.message }; - } -}); - -module.exports = { mainWindow };