Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
1415 lines
44 KiB
JavaScript
1415 lines
44 KiB
JavaScript
/**
|
|
* 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 `
|
|
<div class="installer-container">
|
|
<div class="step-content">
|
|
${renderUninstallView()}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="installer-container">
|
|
${renderStepIndicator()}
|
|
<div class="step-content">
|
|
${renderCurrentStep()}
|
|
</div>
|
|
${renderFooter()}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderStepIndicator() {
|
|
return `
|
|
<div class="step-indicator">
|
|
${steps.map((step, index) => `
|
|
<div class="step-item ${index === state.currentStep ? 'active' : ''} ${index < state.currentStep ? 'completed' : ''}">
|
|
<div class="step-circle">${index < state.currentStep ? '\u2713' : index + 1}</div>
|
|
<span class="step-label">${step.title}</span>
|
|
</div>
|
|
${index < steps.length - 1 ? '<div class="step-connector"></div>' : ''}
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<div class="welcome-content">
|
|
<div class="logo-container">
|
|
<img src="../../assets/dashcaddy logo light.png" alt="DashCaddy Logo" />
|
|
</div>
|
|
<h1>Welcome to DashCaddy</h1>
|
|
<p>The unified management platform for your home lab. This wizard will guide you through the installation process.</p>
|
|
|
|
<ul class="feature-list">
|
|
<li>Manage all your Docker containers from one dashboard</li>
|
|
<li>Automatic HTTPS with Caddy reverse proxy</li>
|
|
<li>DNS integration for local domains</li>
|
|
<li>50+ pre-configured app templates</li>
|
|
<li>Real-time service health monitoring</li>
|
|
</ul>
|
|
|
|
<div class="platform-badge">
|
|
<span>Detected Platform:</span>
|
|
<strong>${platformName} ${state.platform?.arch || ''}</strong>
|
|
</div>
|
|
|
|
<div class="uninstall-link">
|
|
<a href="#" onclick="enterUninstallMode(); return false;">Uninstall an existing installation</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderDependencies() {
|
|
const docker = state.dependencies.docker;
|
|
const caddy = state.dependencies.caddy;
|
|
|
|
return `
|
|
<div>
|
|
<h2>System Requirements</h2>
|
|
<p>DashCaddy requires Docker and Caddy to be installed. We'll check and install them for you.</p>
|
|
|
|
<div class="dependency-cards">
|
|
<div class="dependency-card ${docker.checking ? 'checking' : docker.installed ? 'installed' : 'missing'}">
|
|
<div class="dependency-icon">D</div>
|
|
<div class="dependency-info">
|
|
<h3>Docker</h3>
|
|
<p>${docker.checking ? 'Checking...' : docker.installed ? `Version ${docker.version}` : 'Not installed'}</p>
|
|
</div>
|
|
<div class="dependency-status">
|
|
${docker.checking ?
|
|
'<div class="spinner spinner-small"></div>' :
|
|
docker.installed ?
|
|
'<span class="status-icon installed"></span>' :
|
|
`<button class="btn-install" onclick="installDependency('docker')">Install</button>`
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dependency-card ${caddy.checking ? 'checking' : caddy.installed ? 'installed' : 'missing'}">
|
|
<div class="dependency-icon">C</div>
|
|
<div class="dependency-info">
|
|
<h3>Caddy</h3>
|
|
<p>${caddy.checking ? 'Checking...' : caddy.installed ? `Version ${caddy.version}` : 'Not installed'}</p>
|
|
</div>
|
|
<div class="dependency-status">
|
|
${caddy.checking ?
|
|
'<div class="spinner spinner-small"></div>' :
|
|
caddy.installed ?
|
|
'<span class="status-icon installed"></span>' :
|
|
`<button class="btn-install" onclick="installDependency('caddy')">Install</button>`
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${!docker.checking && !caddy.checking && (!docker.installed || !caddy.installed) ? `
|
|
<div class="error-message">
|
|
Please install the missing dependencies before continuing.
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderFolderSelection() {
|
|
return `
|
|
<div>
|
|
<h2>Choose Installation Folders</h2>
|
|
<p>Select where DashCaddy and its components should be installed.</p>
|
|
|
|
<div class="folder-inputs">
|
|
<div class="folder-input">
|
|
<label>DashCaddy Installation Path</label>
|
|
<div class="input-row">
|
|
<input type="text" value="${escapeHtml(state.paths.install)}" readonly placeholder="Select installation folder...">
|
|
<button class="btn-browse" onclick="selectFolder('install')">Browse...</button>
|
|
</div>
|
|
<p class="hint">Main folder where DashCaddy will be installed</p>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Docker Data Path</label>
|
|
<div class="input-row">
|
|
<input type="text" value="${escapeHtml(state.paths.dockerData)}" readonly placeholder="Select Docker data folder...">
|
|
<button class="btn-browse" onclick="selectFolder('dockerData')">Browse...</button>
|
|
</div>
|
|
<p class="hint">Where Docker containers will store their data (volumes)</p>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Caddy Configuration Path</label>
|
|
<div class="input-row">
|
|
<input type="text" value="${escapeHtml(state.paths.caddyConfig)}" readonly placeholder="Select Caddy config folder...">
|
|
<button class="btn-browse" onclick="selectFolder('caddyConfig')">Browse...</button>
|
|
</div>
|
|
<p class="hint">Where Caddyfile and SSL certificates will be stored</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<div>
|
|
<h2>Choose Deployment Tier</h2>
|
|
<p>Select the level of functionality you need. You can upgrade later.</p>
|
|
|
|
<div class="tier-cards">
|
|
${tiers.map(tier => `
|
|
<div class="tier-card ${state.tier === tier.id ? 'selected' : ''}"
|
|
onclick="selectTier('${tier.id}')">
|
|
${tier.recommended ? '<div class="tier-badge">Recommended</div>' : ''}
|
|
<div class="tier-header">
|
|
<h3>${tier.name}</h3>
|
|
</div>
|
|
<p class="tier-description">${tier.description}</p>
|
|
<ul class="tier-features">
|
|
${tier.includes.map(f => `<li>${f}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<div>
|
|
<h2>How will you access DashCaddy?</h2>
|
|
<p>Choose how you want to reach your dashboard and services on the network.</p>
|
|
|
|
<div class="tier-cards">
|
|
${modes.map(mode => `
|
|
<div class="tier-card ${state.domainMode === mode.id ? 'selected' : ''}"
|
|
onclick="selectDomainMode('${mode.id}')">
|
|
${mode.badge ? `<div class="tier-badge">${mode.badge}</div>` : ''}
|
|
<div class="access-icon">${mode.icon}</div>
|
|
<div class="tier-header">
|
|
<h3>${mode.name}</h3>
|
|
</div>
|
|
<p class="tier-description">${mode.description}</p>
|
|
<p class="access-note">${mode.note}</p>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
${state.domainMode === 'public' ? `
|
|
<div class="folder-inputs" style="margin-top: 24px;">
|
|
<div class="folder-input">
|
|
<label>Domain Name</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.domain.publicDomain)}"
|
|
placeholder="dashboard.example.com"
|
|
oninput="updateDomain('publicDomain', this.value)">
|
|
</div>
|
|
<p class="hint">The domain where your dashboard will be accessible</p>
|
|
</div>
|
|
<div class="folder-input">
|
|
<label>Email for Let's Encrypt</label>
|
|
<div class="input-row">
|
|
<input type="email"
|
|
value="${escapeHtml(state.domain.email)}"
|
|
placeholder="you@example.com"
|
|
oninput="updateDomain('email', this.value)">
|
|
</div>
|
|
<p class="hint">Used for certificate expiry notifications (required by Let's Encrypt)</p>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
${state.domainMode === 'custom-tld' ? `
|
|
<div class="folder-inputs" style="margin-top: 24px;">
|
|
<div class="folder-input">
|
|
<label>Top-Level Domain</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.domain.tld)}"
|
|
placeholder=".home"
|
|
oninput="updateDomain('tld', this.value)">
|
|
</div>
|
|
<p class="hint">Your dashboard will be at <strong>dashcaddy${escapeHtml(state.domain.tld)}</strong>, services at <strong><name>${escapeHtml(state.domain.tld)}</strong></p>
|
|
</div>
|
|
<div class="folder-input">
|
|
<label>Certificate Authority Name</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.domain.caName)}"
|
|
placeholder="DashCaddy Local CA"
|
|
oninput="updateDomain('caName', this.value)">
|
|
</div>
|
|
<p class="hint">Name for your internal Certificate Authority</p>
|
|
</div>
|
|
<div class="info-message">
|
|
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.
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
${state.network.detected && (state.network.lanIP || state.network.tailscaleIP) ? `
|
|
<div class="info-message" style="margin-top: 16px;">
|
|
<strong>Detected Network:</strong>
|
|
${state.network.lanIP ? `LAN: ${escapeHtml(state.network.lanIP)}` : ''}
|
|
${state.network.lanIP && state.network.tailscaleIP ? ' | ' : ''}
|
|
${state.network.tailscaleIP ? `Tailscale: ${escapeHtml(state.network.tailscaleIP)}` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderDNSConfiguration() {
|
|
// Local mode: DNS not needed
|
|
if (state.domainMode === 'local') {
|
|
return `
|
|
<div>
|
|
<h2>DNS Configuration</h2>
|
|
<div class="info-message">
|
|
DNS is not needed for local/IP-based access. You can skip this step.
|
|
</div>
|
|
<p>If you add a DNS server later, you can configure it from the DashCaddy dashboard.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Public domain: DNS managed externally
|
|
if (state.domainMode === 'public') {
|
|
return `
|
|
<div>
|
|
<h2>DNS Configuration</h2>
|
|
<div class="info-message">
|
|
Your domain's DNS is managed by your registrar or DNS provider.
|
|
Make sure <strong>${escapeHtml(state.domain.publicDomain)}</strong> points to this server's IP address.
|
|
</div>
|
|
<p>DashCaddy can optionally manage a local DNS server for additional services. You can configure this later.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Custom TLD but not Full Stack tier
|
|
if (state.domainMode === 'custom-tld' && state.tier !== 'advanced') {
|
|
return `
|
|
<div>
|
|
<h2>DNS Configuration</h2>
|
|
<div class="info-message">
|
|
Your custom TLD <strong>${escapeHtml(state.domain.tld)}</strong> requires a DNS server to resolve domain names.
|
|
DNS management is available with the Full Stack tier — you selected ${state.tier === 'basic' ? 'Basic' : 'Standard'}.
|
|
</div>
|
|
<p>You'll need to manually configure your DNS server to resolve <strong>*${escapeHtml(state.domain.tld)}</strong> domains to this machine's IP address.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Custom TLD + Full Stack tier: full DNS form
|
|
return `
|
|
<div>
|
|
<h2>DNS Configuration</h2>
|
|
<p>Configure your Technitium DNS server connection for automatic DNS record management.</p>
|
|
|
|
<div class="form-group">
|
|
<label class="toggle-label">
|
|
<input type="checkbox"
|
|
${state.dns.enabled ? 'checked' : ''}
|
|
onchange="toggleDNS(this.checked)">
|
|
<span>Enable DNS integration</span>
|
|
</label>
|
|
</div>
|
|
|
|
${state.dns.enabled ? `
|
|
<div class="folder-inputs">
|
|
<div class="folder-input">
|
|
<label>DNS Server URL</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.dns.serverUrl)}"
|
|
placeholder="http://192.168.1.1:5380"
|
|
oninput="updateDNS('serverUrl', this.value)">
|
|
</div>
|
|
<p class="hint">Technitium DNS server address including port</p>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Username</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.dns.username)}"
|
|
placeholder="admin"
|
|
oninput="updateDNS('username', this.value)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Password</label>
|
|
<div class="input-row">
|
|
<input type="password"
|
|
value="${escapeHtml(state.dns.password)}"
|
|
placeholder="Enter password"
|
|
oninput="updateDNS('password', this.value)">
|
|
</div>
|
|
<p class="hint">Stored encrypted on disk</p>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>API Token (optional)</label>
|
|
<div class="input-row">
|
|
<input type="password"
|
|
value="${escapeHtml(state.dns.token)}"
|
|
placeholder="Enter API token"
|
|
oninput="updateDNS('token', this.value)">
|
|
</div>
|
|
<p class="hint">Alternative to username/password authentication</p>
|
|
</div>
|
|
|
|
<button class="btn btn-secondary" onclick="testDNSConnection()"
|
|
${!state.dns.serverUrl ? 'disabled' : ''}>
|
|
Test Connection
|
|
</button>
|
|
|
|
${state.dns.testResult !== null ? `
|
|
<div class="${state.dns.testResult.success ? 'success-message' : 'error-message'}">
|
|
${escapeHtml(state.dns.testResult.message)}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderDashboardSetup() {
|
|
return `
|
|
<div>
|
|
<h2>Dashboard Setup</h2>
|
|
<p>Customize your DashCaddy dashboard appearance and ports.</p>
|
|
|
|
<div class="folder-inputs">
|
|
<div class="folder-input">
|
|
<label>Dashboard Name</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.branding.name)}"
|
|
placeholder="DashCaddy"
|
|
oninput="updateBranding('name', this.value)">
|
|
</div>
|
|
<p class="hint">Displayed in the dashboard header</p>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Primary Color</label>
|
|
<div class="input-row">
|
|
<input type="color"
|
|
value="${state.branding.primaryColor}"
|
|
oninput="updateBranding('primaryColor', this.value)"
|
|
style="width: 60px; height: 40px; padding: 2px; cursor: pointer;">
|
|
<input type="text"
|
|
value="${escapeHtml(state.branding.primaryColor)}"
|
|
placeholder="#6366f1"
|
|
oninput="updateBranding('primaryColor', this.value)"
|
|
style="flex: 1;">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Custom Logo (optional)</label>
|
|
<div class="input-row">
|
|
<input type="text"
|
|
value="${escapeHtml(state.branding.logoSourcePath)}"
|
|
readonly
|
|
placeholder="No custom logo selected">
|
|
<button class="btn-browse" onclick="selectLogo()">Browse...</button>
|
|
</div>
|
|
<p class="hint">PNG or SVG, recommended 200x120px. Leave empty for default logo.</p>
|
|
</div>
|
|
|
|
<div class="folder-input">
|
|
<label>Dashboard Port</label>
|
|
<div class="input-row">
|
|
<input type="number"
|
|
value="${state.branding.dashboardPort}"
|
|
min="1024" max="65535"
|
|
oninput="updateBranding('dashboardPort', parseInt(this.value) || 8080)">
|
|
</div>
|
|
<p class="hint">Port where the dashboard will be accessible (default: 8080)</p>
|
|
</div>
|
|
|
|
${state.tier !== 'basic' ? `
|
|
<div class="folder-input">
|
|
<label>API Server Port</label>
|
|
<div class="input-row">
|
|
<input type="number"
|
|
value="${state.branding.apiPort}"
|
|
min="1024" max="65535"
|
|
oninput="updateBranding('apiPort', parseInt(this.value) || 3001)">
|
|
</div>
|
|
<p class="hint">Port for the DashCaddy API server (default: 3001)</p>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderInstallation() {
|
|
const { status, progress, currentTask, completedTasks, error } = state.installation;
|
|
|
|
if (error) {
|
|
return `
|
|
<div class="installation-progress">
|
|
<h2>Installation Failed</h2>
|
|
<div class="error-message">${escapeHtml(error)}</div>
|
|
<button class="btn btn-primary" onclick="startInstallation()">Retry</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="installation-progress">
|
|
<h2>Installing DashCaddy</h2>
|
|
<p>Please wait while we set up DashCaddy on your system.</p>
|
|
|
|
<div class="progress-percentage">${progress}%</div>
|
|
|
|
<div class="progress-container">
|
|
<div class="progress-bar-bg">
|
|
<div class="progress-bar" style="width: ${progress}%"></div>
|
|
</div>
|
|
<p class="progress-text">${currentTask || 'Starting installation...'}</p>
|
|
</div>
|
|
|
|
<div class="task-log">
|
|
${completedTasks.map(task => `
|
|
<div class="task-log-item completed">✓ ${escapeHtml(task)}</div>
|
|
`).join('')}
|
|
${currentTask && !completedTasks.includes(currentTask) ? `
|
|
<div class="task-log-item current">
|
|
<div class="spinner spinner-small"></div>
|
|
${escapeHtml(currentTask)}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<div class="complete-content">
|
|
<div class="success-icon">✓</div>
|
|
<h1>Installation Complete!</h1>
|
|
<p>${escapeHtml(state.branding.name)} has been successfully installed on your system.</p>
|
|
|
|
<div class="install-summary">
|
|
<h3>Installation Details</h3>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Tier</span>
|
|
<span class="summary-value">${tierLabels[state.tier] || state.tier}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Access Mode</span>
|
|
<span class="summary-value">${modeLabels[state.domainMode] || state.domainMode}${state.domainMode === 'custom-tld' ? ' (' + escapeHtml(state.domain.tld) + ')' : ''}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Dashboard URL</span>
|
|
<span class="summary-value">${escapeHtml(state.result.dashboardUrl || 'http://localhost:' + port)}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Install Path</span>
|
|
<span class="summary-value">${escapeHtml(state.result.installPath || state.paths.install)}</span>
|
|
</div>
|
|
${state.tier !== 'basic' ? `
|
|
<div class="summary-item">
|
|
<span class="summary-label">API Server</span>
|
|
<span class="summary-value">http://localhost:${state.branding.apiPort || 3001}</span>
|
|
</div>
|
|
` : ''}
|
|
${state.dns.enabled ? `
|
|
<div class="summary-item">
|
|
<span class="summary-label">DNS Server</span>
|
|
<span class="summary-value">${escapeHtml(state.dns.serverUrl)}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
${state.result.health ? `
|
|
<div class="install-summary" style="margin-top: 16px;">
|
|
<h3>Service Status</h3>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Caddy</span>
|
|
<span class="summary-value">${state.result.health.caddy ? '<span style="color:#22c55e">Running</span>' : '<span style="color:#ef4444">Not responding</span>'}</span>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<button class="btn btn-success" onclick="openDashboard()">
|
|
Open Dashboard
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<div class="step-footer">
|
|
<div></div>
|
|
<button class="btn btn-primary" onclick="window.close()">Close Installer</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (isInstalling) {
|
|
return `
|
|
<div class="step-footer">
|
|
<div></div>
|
|
<button class="btn btn-primary" disabled>Installing...</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Button text
|
|
let nextLabel = 'Next';
|
|
if (state.currentStep === 6) nextLabel = 'Install';
|
|
|
|
return `
|
|
<div class="step-footer">
|
|
${isFirst ? '<div></div>' : '<button class="btn btn-secondary" onclick="prevStep()">Back</button>'}
|
|
<button class="btn btn-primary" onclick="nextStep()" ${!canProceed ? 'disabled' : ''}>
|
|
${nextLabel}
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ── 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 `
|
|
<div class="uninstall-content">
|
|
<h1>Uninstall DashCaddy</h1>
|
|
<p>This will remove DashCaddy from your system. Services will be stopped and files removed.</p>
|
|
|
|
${u.detecting ? `
|
|
<div class="detecting-box">
|
|
<div class="spinner spinner-small"></div>
|
|
<span>Searching for existing installation...</span>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="form-group">
|
|
<label>Installation Path</label>
|
|
<div class="path-input-row">
|
|
<input type="text" class="input" value="${escapeHtml(u.installPath)}"
|
|
onchange="state.uninstall.installPath = this.value" placeholder="Select installation folder..." />
|
|
<button class="btn btn-secondary" onclick="selectUninstallPath()">Browse</button>
|
|
</div>
|
|
${u.detected ? `
|
|
<div class="detected-badge">Installation detected</div>
|
|
` : u.installPath && !u.detecting ? `
|
|
<div class="not-detected-badge">No installation found at this path</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
${u.config ? `
|
|
<div class="install-summary" style="margin-top: 16px;">
|
|
<h3>Detected Installation</h3>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Tier</span>
|
|
<span class="summary-value">${tierLabels[u.config.tier] || u.config.tier || 'Unknown'}</span>
|
|
</div>
|
|
${u.config.domainMode ? `
|
|
<div class="summary-item">
|
|
<span class="summary-label">Domain Mode</span>
|
|
<span class="summary-value">${u.config.domainMode}${u.config.tld ? ' (' + escapeHtml(u.config.tld) + ')' : ''}</span>
|
|
</div>
|
|
` : ''}
|
|
${u.config.installedAt ? `
|
|
<div class="summary-item">
|
|
<span class="summary-label">Installed</span>
|
|
<span class="summary-value">${new Date(u.config.installedAt).toLocaleDateString()}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="preserve-options">
|
|
<h3>Preserve for Reinstall</h3>
|
|
<p class="preserve-description">Keep these files so a fresh install can pick up where you left off.</p>
|
|
|
|
<label class="checkbox-row">
|
|
<input type="checkbox" ${u.preserveSettings ? 'checked' : ''}
|
|
onchange="togglePreserveSettings(this.checked)" />
|
|
<div>
|
|
<strong>User settings</strong>
|
|
<span class="checkbox-hint">Config, credentials, branding, services list, encryption key</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="checkbox-row">
|
|
<input type="checkbox" ${u.preserveCA ? 'checked' : ''}
|
|
onchange="togglePreserveCA(this.checked)" />
|
|
<div>
|
|
<strong>CA certificates</strong>
|
|
<span class="checkbox-hint">Root and intermediate certificates. Without these, every device that trusted your CA would need to re-trust a new one.</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="step-footer">
|
|
<button class="btn btn-secondary" onclick="exitUninstallMode()">Cancel</button>
|
|
<button class="btn btn-danger" onclick="startUninstallation()"
|
|
${!u.detected ? 'disabled' : ''}>
|
|
Uninstall
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderUninstallProgress() {
|
|
const u = state.uninstall;
|
|
|
|
return `
|
|
<div class="uninstall-content">
|
|
<h1>Uninstalling DashCaddy</h1>
|
|
<p>Please wait while DashCaddy is being removed...</p>
|
|
|
|
<div class="progress-container">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${u.progress}%"></div>
|
|
</div>
|
|
<div class="progress-text">${u.progress}%</div>
|
|
</div>
|
|
|
|
<div class="current-task">${escapeHtml(u.currentTask)}</div>
|
|
|
|
${u.completedTasks.length > 0 ? `
|
|
<div class="completed-tasks">
|
|
${u.completedTasks.map(task => `
|
|
<div class="task-item completed">
|
|
<span class="task-check">✓</span>
|
|
<span>${escapeHtml(task)}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
` : ''}
|
|
|
|
${u.status === 'error' ? `
|
|
<div class="error-message">
|
|
<strong>Error:</strong> ${escapeHtml(u.error)}
|
|
</div>
|
|
<div class="step-footer">
|
|
<button class="btn btn-secondary" onclick="exitUninstallMode()">Close</button>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderUninstallComplete() {
|
|
const u = state.uninstall;
|
|
const preserved = [];
|
|
if (u.preserveSettings) preserved.push('user settings');
|
|
if (u.preserveCA) preserved.push('CA certificates');
|
|
|
|
return `
|
|
<div class="uninstall-content">
|
|
<div class="success-icon" style="color: #6366f1;">✓</div>
|
|
<h1>Uninstall Complete</h1>
|
|
<p>DashCaddy has been removed from your system.</p>
|
|
|
|
${preserved.length > 0 ? `
|
|
<div class="install-summary" style="margin-top: 16px;">
|
|
<h3>Preserved Data</h3>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Saved</span>
|
|
<span class="summary-value">${preserved.join(', ')}</span>
|
|
</div>
|
|
${u.settingsPath ? `
|
|
<div class="summary-item">
|
|
<span class="summary-label">Location</span>
|
|
<span class="summary-value">${escapeHtml(u.settingsPath)}</span>
|
|
</div>
|
|
` : ''}
|
|
<p class="preserve-note">These will be automatically restored when you reinstall to the same path.</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="step-footer">
|
|
<button class="btn btn-secondary" onclick="exitUninstallMode()">Back to Installer</button>
|
|
<button class="btn btn-primary" onclick="window.close()">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Utility functions
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// Initialize on DOM ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initWizard);
|
|
} else {
|
|
initWizard();
|
|
}
|