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; 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 app.whenReady().then(() => { createWindow(); 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 };