Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
1048 lines
30 KiB
JavaScript
1048 lines
30 KiB
JavaScript
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 };
|