Files
dashcaddy/status/js/setup-wizard.js
Sami 77030931b7 Add subdirectory routing mode for public domain deployments
Apps can now be served at domain.com/appname/ instead of requiring
subdomain DNS records (appname.domain.com). Supports three subpath
modes per template: native (URL base env var), strip (handle_path),
and none (incompatible warning). Tested on Linux with deploy/removal
lifecycle verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:03:17 -08:00

434 lines
15 KiB
JavaScript

// Shared timezone utility — used by setup wizard and settings modal
window.populateTimezoneSelect = function(selectEl, selectedTz) {
const timezones = Intl.supportedValuesOf('timeZone');
const detected = selectedTz || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
selectEl.innerHTML = '';
for (const tz of timezones) {
const opt = document.createElement('option');
opt.value = tz;
opt.textContent = tz.replace(/_/g, ' ');
if (tz === detected) opt.selected = true;
selectEl.appendChild(opt);
}
};
// Setup Wizard System - Server-side config storage
(function () {
let currentConfigType = 'homelab';
let serverConfig = null;
// Check server for existing config on page load
async function checkServerConfig() {
try {
const response = await fetch('/api/v1/config');
if (response.ok) {
serverConfig = await response.json();
if (serverConfig && serverConfig.setupComplete) {
// Config exists on server - don't show wizard
document.getElementById('setup-wizard').style.display = 'none';
return true;
}
}
} catch (error) {
console.warn('Could not fetch server config, checking localStorage fallback:', error.message);
}
// Fallback: check localStorage for backwards compatibility
const localSetup = safeGet('dashcaddy-setup');
if (localSetup) {
document.getElementById('setup-wizard').style.display = 'none';
return true;
}
// No config found - show wizard
document.getElementById('setup-wizard').style.display = 'flex';
return false;
}
// Initialize on page load
checkServerConfig();
// Populate timezone dropdown with auto-detection
const setupTzSelect = document.getElementById('setup-timezone');
if (setupTzSelect) window.populateTimezoneSelect(setupTzSelect);
// Step navigation
function showStep(stepId) {
document.querySelectorAll('.setup-step').forEach(step => {
step.style.display = 'none';
});
const targetStep = document.getElementById(stepId);
if (targetStep) {
targetStep.style.display = 'block';
}
}
// Show summary
function showSummary() {
const summaryContent = document.getElementById('setup-summary-content');
if (!summaryContent) return;
let html = '<div style="display: grid; gap: 20px;">';
if (currentConfigType === 'homelab') {
const tld = document.getElementById('setup-tld')?.value?.trim() || '.home';
const caName = document.getElementById('setup-ca-name')?.value?.trim() || '';
const dnsIP = document.getElementById('setup-dns-ip')?.value?.trim() || '';
const dnsPort = document.getElementById('setup-dns-port')?.value?.trim() || DC.DEFAULTS.DNS_PORT;
html += `
<div>
<h3 style="margin: 0 0 12px; color: var(--accent);">Home Lab Configuration</h3>
<div style="display: grid; gap: 12px; font-size: 0.95rem;">
<div><strong>TLD:</strong> ${tld}</div>
<div><strong>Certificate Authority:</strong> ${caName}</div>
<div><strong>DNS Server:</strong> ${dnsIP}:${dnsPort}</div>
<div><strong>Example URLs:</strong> https://uptime${tld}, https://nextcloud${tld}</div>
</div>
</div>
`;
} else if (currentConfigType === 'simple') {
const ip = document.getElementById('setup-simple-ip')?.value?.trim() || 'localhost';
html += `
<div>
<h3 style="margin: 0 0 12px; color: var(--accent);">Simple Setup</h3>
<div style="display: grid; gap: 12px; font-size: 0.95rem;">
<div><strong>Access Method:</strong> IP:Port only</div>
<div><strong>Default IP:</strong> ${ip}</div>
<div><strong>SSL:</strong> None (HTTP only)</div>
<div><strong>Example URLs:</strong> http://${ip}:8080, http://${ip}:3000</div>
</div>
</div>
`;
} else if (currentConfigType === 'public') {
const domain = document.getElementById('setup-public-domain')?.value?.trim() || '';
const email = document.getElementById('setup-public-email')?.value?.trim() || '';
const routingMode = document.querySelector('input[name="routing-mode"]:checked')?.value || 'subdirectory';
const exampleUrls = routingMode === 'subdirectory'
? `https://${domain}/sonarr, https://${domain}/grafana`
: `https://sonarr.${domain}, https://grafana.${domain}`;
const routingLabel = routingMode === 'subdirectory' ? 'Subdirectory (domain.com/app)' : 'Subdomain (app.domain.com)';
html += `
<div>
<h3 style="margin: 0 0 12px; color: var(--accent);">Public Server</h3>
<div style="display: grid; gap: 12px; font-size: 0.95rem;">
<div><strong>Domain:</strong> ${domain}</div>
<div><strong>SSL:</strong> Let's Encrypt</div>
<div><strong>Email:</strong> ${email}</div>
<div><strong>Routing:</strong> ${routingLabel}</div>
<div><strong>Example URLs:</strong> ${exampleUrls}</div>
</div>
</div>
`;
}
// Timezone (universal across all config types)
const tz = document.getElementById('setup-timezone')?.value || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
html += `
<div style="margin-top: 8px; padding-top: 12px; border-top: 1px solid var(--border);">
<div style="font-size: 0.95rem;"><strong>Timezone:</strong> ${tz.replace(/_/g, ' ')}</div>
</div>
`;
html += '</div>';
summaryContent.innerHTML = html;
showStep('setup-step-summary');
}
// Save config to server
async function saveConfigToServer(config) {
try {
const response = await secureFetch('/api/v1/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (response.ok) {
await response.json();
return true;
} else {
console.error('Failed to save config to server:', response.status);
return false;
}
} catch (error) {
console.error('Error saving config to server:', error);
return false;
}
}
// Finish setup handler
async function finishSetup() {
const config = {
setupComplete: true,
configurationType: currentConfigType,
timestamp: new Date().toISOString(),
timezone: document.getElementById('setup-timezone')?.value || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
};
if (currentConfigType === 'homelab') {
config.tld = document.getElementById('setup-tld')?.value?.trim() || '.home';
config.caName = document.getElementById('setup-ca-name')?.value?.trim() || '';
config.dns = {
provider: 'technitium',
ip: document.getElementById('setup-dns-ip')?.value?.trim() || '',
port: document.getElementById('setup-dns-port')?.value?.trim() || DC.DEFAULTS.DNS_PORT,
token: document.getElementById('setup-dns-token')?.value?.trim() || ''
};
config.defaults = {
dnsType: 'private',
sslType: 'internal',
targetIP: 'localhost'
};
} else if (currentConfigType === 'simple') {
config.defaultIP = document.getElementById('setup-simple-ip')?.value?.trim() || 'localhost';
config.defaults = {
dnsType: 'none',
sslType: 'none',
targetIP: config.defaultIP
};
} else if (currentConfigType === 'public') {
config.domain = document.getElementById('setup-public-domain')?.value?.trim() || '';
config.email = document.getElementById('setup-public-email')?.value?.trim() || '';
config.routingMode = document.querySelector('input[name="routing-mode"]:checked')?.value || 'subdirectory';
config.defaults = {
dnsType: config.routingMode === 'subdirectory' ? 'none' : 'public',
sslType: 'letsencrypt',
targetIP: 'localhost'
};
}
// Save to server (primary) and localStorage (fallback)
const savedToServer = await saveConfigToServer(config);
safeSet('dashcaddy-config', JSON.stringify(config));
safeSet('dashcaddy-setup', 'completed');
// Hide wizard
document.getElementById('setup-wizard').style.display = 'none';
// Show success notification
const configName = currentConfigType === 'homelab' ? 'Professional Home Lab' :
currentConfigType === 'simple' ? 'Simple Setup' : 'Public Server';
const saveLocation = savedToServer ? 'server (shared across all devices)' : 'locally (this browser only)';
showNotification(`Setup Complete! Configured for: ${configName}. Settings saved to: ${saveLocation}`, 'success', 5000);
// Reload page to apply configuration
setTimeout(() => location.reload(), 500);
}
// ===== Event Handlers using direct onclick for reliability =====
// Step 1: Continue button
const step1Next = document.getElementById('setup-step-1-next');
if (step1Next) {
step1Next.onclick = function(e) {
e.preventDefault();
const selected = document.querySelector('input[name="config-type"]:checked');
if (selected) {
currentConfigType = selected.value;
}
if (currentConfigType === 'homelab') {
showStep('setup-step-homelab');
} else if (currentConfigType === 'simple') {
showStep('setup-step-simple');
} else if (currentConfigType === 'public') {
showStep('setup-step-public');
} else {
showStep('setup-step-homelab');
}
};
}
// Skip setup button
const skipBtn = document.getElementById('setup-skip');
if (skipBtn) {
skipBtn.onclick = async function(e) {
e.preventDefault();
if (confirm('Skip setup? You can run it later from Settings.')) {
// Save skip status to server
await saveConfigToServer({ setupComplete: true, skipped: true, timestamp: new Date().toISOString() });
safeSet('dashcaddy-setup', 'skipped');
document.getElementById('setup-wizard').style.display = 'none';
}
};
}
// Home Lab: TLD Preview
const tldInput = document.getElementById('setup-tld');
if (tldInput) {
tldInput.oninput = function(e) {
const tld = e.target.value || '.home';
const preview1 = document.getElementById('tld-preview');
const preview2 = document.getElementById('tld-preview-2');
if (preview1) preview1.textContent = tld;
if (preview2) preview2.textContent = tld;
};
}
// Home Lab navigation
const homelabBack = document.getElementById('setup-homelab-back');
if (homelabBack) {
homelabBack.onclick = function(e) {
e.preventDefault();
showStep('setup-step-1');
};
}
const homelabNext = document.getElementById('setup-homelab-next');
if (homelabNext) {
homelabNext.onclick = function(e) {
e.preventDefault();
const tld = document.getElementById('setup-tld')?.value?.trim() || '';
const caName = document.getElementById('setup-ca-name')?.value?.trim() || '';
const dnsIP = document.getElementById('setup-dns-ip')?.value?.trim() || '';
if (!tld || !tld.startsWith('.')) {
showNotification('Please enter a valid TLD starting with a dot (e.g., .home)', 'warning');
return;
}
if (!caName) {
showNotification('Please enter a Certificate Authority name', 'warning');
return;
}
if (!dnsIP) {
showNotification('Please enter your DNS server IP address', 'warning');
return;
}
showSummary();
};
}
// Simple navigation
const simpleBack = document.getElementById('setup-simple-back');
if (simpleBack) {
simpleBack.onclick = function(e) {
e.preventDefault();
showStep('setup-step-1');
};
}
const simpleNext = document.getElementById('setup-simple-next');
if (simpleNext) {
simpleNext.onclick = function(e) {
e.preventDefault();
showSummary();
};
}
// Public routing mode toggle — update requirement text
document.querySelectorAll('input[name="routing-mode"]').forEach(function(radio) {
radio.onchange = function() {
var note = document.getElementById('dns-requirement-note');
if (note) {
note.textContent = this.value === 'subdirectory'
? 'Only one DNS record needed (for the main domain)'
: 'You\'ll need to configure DNS manually for each subdomain';
}
};
});
// Public navigation
const publicBack = document.getElementById('setup-public-back');
if (publicBack) {
publicBack.onclick = function(e) {
e.preventDefault();
showStep('setup-step-1');
};
}
const publicNext = document.getElementById('setup-public-next');
if (publicNext) {
publicNext.onclick = function(e) {
e.preventDefault();
const domain = document.getElementById('setup-public-domain')?.value?.trim() || '';
const email = document.getElementById('setup-public-email')?.value?.trim() || '';
if (!domain) {
showNotification('Please enter your domain name', 'warning');
return;
}
if (!email || !email.includes('@')) {
showNotification('Please enter a valid email address', 'warning');
return;
}
showSummary();
};
}
// Summary navigation
const summaryBack = document.getElementById('setup-summary-back');
if (summaryBack) {
summaryBack.onclick = function(e) {
e.preventDefault();
if (currentConfigType === 'homelab') {
showStep('setup-step-homelab');
} else if (currentConfigType === 'simple') {
showStep('setup-step-simple');
} else if (currentConfigType === 'public') {
showStep('setup-step-public');
}
};
}
// Finish setup button
const finishBtn = document.getElementById('setup-finish');
if (finishBtn) {
finishBtn.onclick = function(e) {
e.preventDefault();
finishSetup();
};
}
// Expose function to get global config (from server or localStorage)
window.getGlobalConfig = async function() {
// Try server first
try {
const response = await fetch('/api/v1/config');
if (response.ok) {
const config = await response.json();
if (config && config.setupComplete) {
return config;
}
}
} catch (e) {
console.warn('Could not fetch config from server');
}
// Fallback to localStorage
const configStr = safeGet('dashcaddy-config');
if (configStr) {
return JSON.parse(configStr);
}
// Return default config if not set
return {
setupComplete: false,
configurationType: 'homelab',
tld: '.home',
caName: '',
defaults: {
dnsType: 'private',
sslType: 'internal',
targetIP: 'localhost'
}
};
};
// Expose reset function for settings
window.resetSetupWizard = async function() {
if (confirm('Reset DashCaddy configuration? This will show the setup wizard again.')) {
try {
await secureFetch('/api/v1/config', { method: 'DELETE' });
} catch (e) {
console.warn('Could not delete server config');
}
safeRemove('dashcaddy-setup');
safeRemove('dashcaddy-config');
location.reload();
}
};
})();