/** * DashCaddy Installer Wizard * A step-by-step installation wizard for DashCaddy */ // Wizard State const state = { currentStep: 0, platform: null, paths: { install: '', dockerData: '', caddyConfig: '' }, dependencies: { docker: { installed: false, version: null, checking: true }, caddy: { installed: false, version: null, checking: true } }, // Tier selection tier: 'intermediate', // DNS configuration dns: { enabled: false, serverUrl: '', username: '', password: '', token: '', testResult: null }, // Dashboard branding branding: { name: 'DashCaddy', title: 'DashCaddy Dashboard', primaryColor: '#6366f1', logoSourcePath: '', dashboardPort: 8080, apiPort: 3001 }, // Access mode domainMode: 'local', // 'local' | 'public' | 'custom-tld' domain: { publicDomain: '', tld: '.home', email: '', caName: 'DashCaddy Local CA' }, // Detected network IPs network: { lanIP: '', tailscaleIP: '', detected: false }, installation: { status: 'pending', // pending, running, complete, error progress: 0, currentTask: '', completedTasks: [], error: null }, result: { dashboardUrl: '', installPath: '', health: null }, // Uninstall mode uninstallMode: false, uninstall: { step: 'confirm', // 'confirm' | 'progress' | 'complete' installPath: '', config: null, detected: false, detecting: false, preserveSettings: true, preserveCA: true, status: 'pending', // pending, running, complete, error progress: 0, currentTask: '', completedTasks: [], error: null, settingsPath: null } }; // Step definitions (8 steps) const steps = [ { id: 'welcome', title: 'Welcome' }, { id: 'dependencies', title: 'Dependencies' }, { id: 'folder', title: 'Install Path' }, { id: 'tier', title: 'Tier' }, { id: 'access', title: 'Access' }, { id: 'dns', title: 'DNS' }, { id: 'dashboard', title: 'Dashboard' }, { id: 'install', title: 'Install' }, { id: 'complete', title: 'Complete' } ]; // DOM Elements let root; // Initialize wizard async function initWizard() { root = document.getElementById('root'); // Check platform try { const result = await window.electronAPI.checkPlatform(); if (result.success) { state.platform = result.data; setDefaultPaths(); } } catch (err) { console.error('Platform detection failed:', err); } // Detect network IPs try { const netResult = await window.electronAPI.detectNetwork(); if (netResult.success) { state.network.lanIP = netResult.data.lanIP || ''; state.network.tailscaleIP = netResult.data.tailscaleIP || ''; state.network.detected = true; } } catch (err) { console.warn('Network detection failed:', err); } // Setup event listeners for installation progress setupEventListeners(); // Render initial state render(); } // Set default paths based on platform function setDefaultPaths() { const os = state.platform?.os || 'windows'; if (os === 'windows') { state.paths.install = 'C:\\DashCaddy'; state.paths.dockerData = 'C:\\DashCaddy\\docker-data'; state.paths.caddyConfig = 'C:\\DashCaddy\\caddy'; } else if (os === 'macos') { state.paths.install = '/Applications/DashCaddy'; state.paths.dockerData = '/Applications/DashCaddy/docker-data'; state.paths.caddyConfig = '/Applications/DashCaddy/caddy'; } else { state.paths.install = '/opt/dashcaddy'; state.paths.dockerData = '/opt/dashcaddy/docker-data'; state.paths.caddyConfig = '/opt/dashcaddy/caddy'; } } // Setup IPC event listeners function setupEventListeners() { window.electronAPI.onStepProgress((data) => { state.installation.progress = data.progress; state.installation.currentTask = data.task; render(); }); window.electronAPI.onStepComplete((data) => { state.installation.completedTasks.push(data.step); render(); }); window.electronAPI.onStepError((data) => { state.installation.status = 'error'; state.installation.error = data.error; render(); }); window.electronAPI.onInstallationComplete((data) => { state.installation.status = 'complete'; state.result.dashboardUrl = data.dashboardUrl; state.result.installPath = data.installPath; state.result.health = data.health || null; state.currentStep = 8; // Move to complete step render(); }); // Uninstall progress listeners window.electronAPI.onUninstallProgress((data) => { state.uninstall.progress = data.progress; state.uninstall.currentTask = data.task; render(); }); window.electronAPI.onUninstallStepComplete((data) => { state.uninstall.completedTasks.push(data.step); render(); }); window.electronAPI.onUninstallComplete((data) => { state.uninstall.status = 'complete'; state.uninstall.step = 'complete'; state.uninstall.settingsPath = data.settingsPath || null; render(); }); window.electronAPI.onUninstallError((data) => { state.uninstall.status = 'error'; state.uninstall.error = data.error; render(); }); } // Navigation function nextStep() { if (state.currentStep < steps.length - 1) { state.currentStep++; // Trigger actions on step enter switch (state.currentStep) { case 1: // Dependencies checkDependencies(); break; case 7: // Installation startInstallation(); break; } render(); } } function prevStep() { if (state.currentStep > 0) { state.currentStep--; render(); } } // Check dependencies async function checkDependencies() { state.dependencies.docker.checking = true; state.dependencies.caddy.checking = true; render(); try { // Pass user-configured caddy path so the checker can find caddy.exe there const caddySearchPaths = []; if (state.paths.caddyConfig) { caddySearchPaths.push(state.paths.caddyConfig); } if (state.paths.install) { const sep = state.platform?.os === 'windows' ? '\\' : '/'; caddySearchPaths.push(state.paths.install + sep + 'caddy'); caddySearchPaths.push(state.paths.install); } const result = await window.electronAPI.checkDependencies({ caddySearchPaths }); if (result.success) { state.dependencies.docker = { installed: result.data.docker.installed, version: result.data.docker.version, checking: false }; state.dependencies.caddy = { installed: result.data.caddy.installed, version: result.data.caddy.version, checking: false }; } } catch (err) { console.error('Dependency check failed:', err); state.dependencies.docker.checking = false; state.dependencies.caddy.checking = false; } render(); } // Install dependency async function installDependency(type) { state.dependencies[type].checking = true; render(); try { if (type === 'docker') { await window.electronAPI.installDocker(); } else { await window.electronAPI.installCaddy({ targetPath: state.paths.caddyConfig || null }); } await checkDependencies(); } catch (err) { console.error(`${type} installation failed:`, err); state.dependencies[type].checking = false; render(); } } // Select folder async function selectFolder(pathKey) { try { const result = await window.electronAPI.selectFolder(); if (result.success && result.path) { state.paths[pathKey] = result.path; // Auto-set child paths if main install path changes if (pathKey === 'install') { const sep = state.platform?.os === 'windows' ? '\\' : '/'; state.paths.dockerData = result.path + sep + 'docker-data'; state.paths.caddyConfig = result.path + sep + 'caddy'; } render(); } } catch (err) { console.error('Folder selection failed:', err); } } // Tier selection function selectTier(tierId) { state.tier = tierId; // Reset DNS if downgrading from advanced if (tierId !== 'advanced') { state.dns.enabled = false; } render(); } // Access mode functions function selectDomainMode(mode) { state.domainMode = mode; render(); } function updateDomain(field, value) { state.domain[field] = value; if (field === 'tld' && value && !value.startsWith('.')) { state.domain.tld = '.' + value; } } // DNS functions function toggleDNS(enabled) { state.dns.enabled = enabled; render(); } function updateDNS(field, value) { state.dns[field] = value; } async function testDNSConnection() { state.dns.testResult = null; render(); try { const result = await window.electronAPI.testDNSConnection({ server: state.dns.serverUrl, username: state.dns.username, password: state.dns.password, token: state.dns.token }); state.dns.testResult = { success: result.success && result.data?.connected, message: result.data?.message || (result.success ? 'Connected' : result.error) }; } catch (err) { state.dns.testResult = { success: false, message: err.message }; } render(); } // Branding functions function updateBranding(field, value) { state.branding[field] = value; // Re-render for color to sync both inputs if (field === 'primaryColor') render(); } async function selectLogo() { try { const result = await window.electronAPI.selectFile({ title: 'Select Dashboard Logo', filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'svg'] }] }); if (result.success && result.path) { state.branding.logoSourcePath = result.path; render(); } } catch (err) { console.error('Logo selection failed:', err); } } // Start installation async function startInstallation() { state.installation.status = 'running'; state.installation.progress = 0; state.installation.completedTasks = []; state.installation.error = null; render(); try { await window.electronAPI.runInstallation({ installPath: state.paths.install, dockerDataPath: state.paths.dockerData, caddyConfigPath: state.paths.caddyConfig, tier: state.tier, dashboardPort: state.branding.dashboardPort, apiPort: state.branding.apiPort, domainMode: state.domainMode, domain: state.domain, lanIP: state.network.lanIP, tailscaleIP: state.network.tailscaleIP, branding: { name: state.branding.name, title: state.branding.title, primaryColor: state.branding.primaryColor, logoSourcePath: state.branding.logoSourcePath || null }, dns: state.dns.enabled ? { server: state.dns.serverUrl, username: state.dns.username, password: state.dns.password, token: state.dns.token } : null, autoStart: true }); } catch (err) { state.installation.status = 'error'; state.installation.error = err.message; render(); } } // Open dashboard async function openDashboard() { try { await window.electronAPI.openDashboard(state.branding.dashboardPort || 8080); } catch (err) { console.error('Failed to open dashboard:', err); } } // Render functions function render() { root.innerHTML = renderContainer(); } function renderContainer() { if (state.uninstallMode) { return `
${renderUninstallView()}
`; } return `
${renderStepIndicator()}
${renderCurrentStep()}
${renderFooter()}
`; } function renderStepIndicator() { return `
${steps.map((step, index) => `
${index < state.currentStep ? '\u2713' : index + 1}
${step.title}
${index < steps.length - 1 ? '
' : ''} `).join('')}
`; } function renderCurrentStep() { switch (state.currentStep) { case 0: return renderWelcome(); case 1: return renderDependencies(); case 2: return renderFolderSelection(); case 3: return renderTierSelection(); case 4: return renderAccessMode(); case 5: return renderDNSConfiguration(); case 6: return renderDashboardSetup(); case 7: return renderInstallation(); case 8: return renderComplete(); default: return ''; } } function renderWelcome() { const platformName = { 'windows': 'Windows', 'macos': 'macOS', 'linux': 'Linux' }[state.platform?.os] || 'Unknown'; return `
DashCaddy Logo

Welcome to DashCaddy

The unified management platform for your home lab. This wizard will guide you through the installation process.

Detected Platform: ${platformName} ${state.platform?.arch || ''}
`; } function renderDependencies() { const docker = state.dependencies.docker; const caddy = state.dependencies.caddy; return `

System Requirements

DashCaddy requires Docker and Caddy to be installed. We'll check and install them for you.

D

Docker

${docker.checking ? 'Checking...' : docker.installed ? `Version ${docker.version}` : 'Not installed'}

${docker.checking ? '
' : docker.installed ? '' : `` }
C

Caddy

${caddy.checking ? 'Checking...' : caddy.installed ? `Version ${caddy.version}` : 'Not installed'}

${caddy.checking ? '
' : caddy.installed ? '' : `` }
${!docker.checking && !caddy.checking && (!docker.installed || !caddy.installed) ? `
Please install the missing dependencies before continuing.
` : ''}
`; } function renderFolderSelection() { return `

Choose Installation Folders

Select where DashCaddy and its components should be installed.

Main folder where DashCaddy will be installed

Where Docker containers will store their data (volumes)

Where Caddyfile and SSL certificates will be stored

`; } function renderTierSelection() { const tiers = [ { id: 'basic', name: 'Basic', description: 'Caddy serves the DashCaddy dashboard as a static site.', includes: ['Caddy reverse proxy', 'Dashboard UI', 'Static file serving'] }, { id: 'intermediate', name: 'Standard', description: 'Adds the DashCaddy API server running in Docker for container management.', includes: ['Everything in Basic', 'Docker API server', 'Container management', '50+ app templates'], recommended: true }, { id: 'advanced', name: 'Full Stack', description: 'Complete setup with DNS management via Technitium DNS integration.', includes: ['Everything in Standard', 'DNS management', 'Automatic DNS records', 'Local domain support'] } ]; return `

Choose Deployment Tier

Select the level of functionality you need. You can upgrade later.

${tiers.map(tier => `
${tier.recommended ? '
Recommended
' : ''}

${tier.name}

${tier.description}

    ${tier.includes.map(f => `
  • ${f}
  • `).join('')}
`).join('')}
`; } function renderAccessMode() { const modes = [ { id: 'local', name: 'Local Only', icon: '127', description: 'Access your dashboard via IP address or localhost. No domain names or HTTPS required.', note: 'Best for: Testing, single-machine use, simple setups' }, { id: 'public', name: 'Public Domain', icon: 'WWW', description: 'Use a real domain name with automatic Let\'s Encrypt HTTPS certificates.', note: 'Requires: Ports 80 + 443 open, DNS pointing to this server' }, { id: 'custom-tld', name: 'Custom TLD', icon: 'CA', description: 'Use a custom top-level domain like .home or .lab with an internal Certificate Authority.', note: 'Requires: A local DNS server (configured in the next step)', badge: 'Expert' } ]; return `

How will you access DashCaddy?

Choose how you want to reach your dashboard and services on the network.

${modes.map(mode => `
${mode.badge ? `
${mode.badge}
` : ''}
${mode.icon}

${mode.name}

${mode.description}

${mode.note}

`).join('')}
${state.domainMode === 'public' ? `

The domain where your dashboard will be accessible

Used for certificate expiry notifications (required by Let's Encrypt)

` : ''} ${state.domainMode === 'custom-tld' ? `

Your dashboard will be at dashcaddy${escapeHtml(state.domain.tld)}, services at <name>${escapeHtml(state.domain.tld)}

Name for your internal Certificate Authority

Caddy will run an internal Certificate Authority to issue HTTPS certificates for your ${escapeHtml(state.domain.tld)} domains. You'll need a local DNS server to resolve these names — configure it in the next step.
` : ''} ${state.network.detected && (state.network.lanIP || state.network.tailscaleIP) ? `
Detected Network: ${state.network.lanIP ? `LAN: ${escapeHtml(state.network.lanIP)}` : ''} ${state.network.lanIP && state.network.tailscaleIP ? ' | ' : ''} ${state.network.tailscaleIP ? `Tailscale: ${escapeHtml(state.network.tailscaleIP)}` : ''}
` : ''}
`; } function renderDNSConfiguration() { // Local mode: DNS not needed if (state.domainMode === 'local') { return `

DNS Configuration

DNS is not needed for local/IP-based access. You can skip this step.

If you add a DNS server later, you can configure it from the DashCaddy dashboard.

`; } // Public domain: DNS managed externally if (state.domainMode === 'public') { return `

DNS Configuration

Your domain's DNS is managed by your registrar or DNS provider. Make sure ${escapeHtml(state.domain.publicDomain)} points to this server's IP address.

DashCaddy can optionally manage a local DNS server for additional services. You can configure this later.

`; } // Custom TLD but not Full Stack tier if (state.domainMode === 'custom-tld' && state.tier !== 'advanced') { return `

DNS Configuration

Your custom TLD ${escapeHtml(state.domain.tld)} requires a DNS server to resolve domain names. DNS management is available with the Full Stack tier — you selected ${state.tier === 'basic' ? 'Basic' : 'Standard'}.

You'll need to manually configure your DNS server to resolve *${escapeHtml(state.domain.tld)} domains to this machine's IP address.

`; } // Custom TLD + Full Stack tier: full DNS form return `

DNS Configuration

Configure your Technitium DNS server connection for automatic DNS record management.

${state.dns.enabled ? `

Technitium DNS server address including port

Stored encrypted on disk

Alternative to username/password authentication

${state.dns.testResult !== null ? `
${escapeHtml(state.dns.testResult.message)}
` : ''}
` : ''}
`; } function renderDashboardSetup() { return `

Dashboard Setup

Customize your DashCaddy dashboard appearance and ports.

Displayed in the dashboard header

PNG or SVG, recommended 200x120px. Leave empty for default logo.

Port where the dashboard will be accessible (default: 8080)

${state.tier !== 'basic' ? `

Port for the DashCaddy API server (default: 3001)

` : ''}
`; } function renderInstallation() { const { status, progress, currentTask, completedTasks, error } = state.installation; if (error) { return `

Installation Failed

${escapeHtml(error)}
`; } return `

Installing DashCaddy

Please wait while we set up DashCaddy on your system.

${progress}%

${currentTask || 'Starting installation...'}

${completedTasks.map(task => `
✓ ${escapeHtml(task)}
`).join('')} ${currentTask && !completedTasks.includes(currentTask) ? `
${escapeHtml(currentTask)}
` : ''}
`; } function renderComplete() { const port = state.branding.dashboardPort || 8080; const tierLabels = { basic: 'Basic', intermediate: 'Standard', advanced: 'Full Stack' }; const modeLabels = { 'local': 'Local/IP', 'public': 'Public Domain', 'custom-tld': 'Custom TLD' }; return `

Installation Complete!

${escapeHtml(state.branding.name)} has been successfully installed on your system.

Installation Details

Tier ${tierLabels[state.tier] || state.tier}
Access Mode ${modeLabels[state.domainMode] || state.domainMode}${state.domainMode === 'custom-tld' ? ' (' + escapeHtml(state.domain.tld) + ')' : ''}
Dashboard URL ${escapeHtml(state.result.dashboardUrl || 'http://localhost:' + port)}
Install Path ${escapeHtml(state.result.installPath || state.paths.install)}
${state.tier !== 'basic' ? `
API Server http://localhost:${state.branding.apiPort || 3001}
` : ''} ${state.dns.enabled ? `
DNS Server ${escapeHtml(state.dns.serverUrl)}
` : ''}
${state.result.health ? `

Service Status

Caddy ${state.result.health.caddy ? 'Running' : 'Not responding'}
` : ''}
`; } function renderFooter() { const isFirst = state.currentStep === 0; const isLast = state.currentStep === steps.length - 1; const isInstalling = state.currentStep === 7 && state.installation.status === 'running'; // Determine if user can proceed let canProceed = true; switch (state.currentStep) { case 1: // Dependencies canProceed = state.dependencies.docker.installed && state.dependencies.caddy.installed; break; case 2: // Folder canProceed = !!state.paths.install; break; case 4: // Access mode if (state.domainMode === 'public') { canProceed = !!state.domain.publicDomain && !!state.domain.email; } else if (state.domainMode === 'custom-tld') { canProceed = !!state.domain.tld; } break; } if (isLast) { return ` `; } if (isInstalling) { return ` `; } // Button text let nextLabel = 'Next'; if (state.currentStep === 6) nextLabel = 'Install'; return ` `; } // ── Uninstall Mode ──────────────────────────────────────────────────────── async function enterUninstallMode() { state.uninstallMode = true; state.uninstall.detecting = true; state.uninstall.step = 'confirm'; render(); // Try to auto-detect existing installation try { const result = await window.electronAPI.detectInstallation(); if (result.success && result.data.found) { state.uninstall.installPath = result.data.installPath; state.uninstall.config = result.data.config; state.uninstall.detected = true; } } catch (err) { console.warn('Installation detection failed:', err); } state.uninstall.detecting = false; render(); } function exitUninstallMode() { state.uninstallMode = false; // Reset uninstall state state.uninstall = { step: 'confirm', installPath: '', config: null, detected: false, detecting: false, preserveSettings: true, preserveCA: true, status: 'pending', progress: 0, currentTask: '', completedTasks: [], error: null, settingsPath: null }; render(); } async function selectUninstallPath() { try { const result = await window.electronAPI.selectFolder(); if (result.success && result.path) { state.uninstall.installPath = result.path; state.uninstall.detected = false; // Try to load config from selected path const configResult = await window.electronAPI.loadConfig(result.path); if (configResult.success && configResult.data?.config) { state.uninstall.config = configResult.data.config; state.uninstall.detected = true; } else { state.uninstall.config = null; } render(); } } catch (err) { console.error('Folder selection failed:', err); } } function togglePreserveSettings(checked) { state.uninstall.preserveSettings = checked; render(); } function togglePreserveCA(checked) { state.uninstall.preserveCA = checked; render(); } async function startUninstallation() { state.uninstall.status = 'running'; state.uninstall.step = 'progress'; state.uninstall.progress = 0; state.uninstall.completedTasks = []; state.uninstall.error = null; render(); try { await window.electronAPI.runUninstallation({ installPath: state.uninstall.installPath, preserveSettings: state.uninstall.preserveSettings, preserveCA: state.uninstall.preserveCA }); } catch (err) { state.uninstall.status = 'error'; state.uninstall.error = err.message; render(); } } function renderUninstallView() { switch (state.uninstall.step) { case 'confirm': return renderUninstallConfirm(); case 'progress': return renderUninstallProgress(); case 'complete': return renderUninstallComplete(); default: return ''; } } function renderUninstallConfirm() { const u = state.uninstall; const tierLabels = { basic: 'Basic', intermediate: 'Standard', advanced: 'Full Stack' }; return `

Uninstall DashCaddy

This will remove DashCaddy from your system. Services will be stopped and files removed.

${u.detecting ? `
Searching for existing installation...
` : ''}
${u.detected ? `
Installation detected
` : u.installPath && !u.detecting ? `
No installation found at this path
` : ''}
${u.config ? `

Detected Installation

Tier ${tierLabels[u.config.tier] || u.config.tier || 'Unknown'}
${u.config.domainMode ? `
Domain Mode ${u.config.domainMode}${u.config.tld ? ' (' + escapeHtml(u.config.tld) + ')' : ''}
` : ''} ${u.config.installedAt ? `
Installed ${new Date(u.config.installedAt).toLocaleDateString()}
` : ''}
` : ''}

Preserve for Reinstall

Keep these files so a fresh install can pick up where you left off.

`; } function renderUninstallProgress() { const u = state.uninstall; return `

Uninstalling DashCaddy

Please wait while DashCaddy is being removed...

${u.progress}%
${escapeHtml(u.currentTask)}
${u.completedTasks.length > 0 ? `
${u.completedTasks.map(task => `
${escapeHtml(task)}
`).join('')}
` : ''} ${u.status === 'error' ? `
Error: ${escapeHtml(u.error)}
` : ''}
`; } function renderUninstallComplete() { const u = state.uninstall; const preserved = []; if (u.preserveSettings) preserved.push('user settings'); if (u.preserveCA) preserved.push('CA certificates'); return `

Uninstall Complete

DashCaddy has been removed from your system.

${preserved.length > 0 ? `

Preserved Data

Saved ${preserved.join(', ')}
${u.settingsPath ? `
Location ${escapeHtml(u.settingsPath)}
` : ''}

These will be automatically restored when you reinstall to the same path.

` : ''}
`; } // Utility functions function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initWizard); } else { initWizard(); }