Files
dashcaddy/status/js/setup-wizard.js
T
Hermes 1a736c89cc [grade=B urn:ump:wokpmecjfauc7jvnxyyyu4fq3zkkvwge455a5mxlmi3pdzkfdw3a] feat(DC-171): DNS provider plug-in parity phase 1 — BIND 9 AXFR listing + capability honesty
RFC2136/BIND9 adapter gains listRecords via AXFR zone transfer (dig with
the same TSIG identity as nsupdate; temp 0600 keyfile, always cleaned up;
transfer-denial errors carry the exact allow-transfer fix). TSIG fields
validated (name charset, HMAC algorithm whitelist, base64 secret) before
keyfile interpolation.

Capability honesty at every surface: GET /dns/records 501s name the active
provider + its supported capabilities + zone-transfer pointer; MCP
dashcaddy_list_dns attaches actionable guidance for AI clients; setup
wizard shows per-provider capability hints at choice time (parity-pinned
by tests against the adapters).

Also fixed a latent rfc2136 bug: this._log instance property shadowed the
prototype logging method — every nsupdate call threw since the DC-170-era
refactor. CHANGELOG stale 'protocol-impossible' DC-171 entry superseded.

Judge: grade B, zero blockers (urn:ump:wokpmecjfauc7jvnxyyyu4fq3zkkvwge455a5mxlmi3pdzkfdw3a).
Polish applied post-grade: stale changelog entry corrected, TSIG field
validation + 4 extra failure-path tests (exact dig argv, ENOENT install
hint, non-denial failure passthrough, hostile-field rejection),
dns-records 501 test strengthened to pin provider name + capabilities.
Suite: backend 174 suites / 3754 tests, frontend 68/68.
2026-10-29 03:21:48 -07:00

485 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Shared timezone utility — used by setup wizard and settings modal
// errorHandler is declared globally in globals.js (core.js bundle); reusing it
// here so we don't get "redeclaration of const errorHandler" at script scope.
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 {
errorHandler.logError('[SetupWizard] Save Config', new Error(`Server returned ${response.status}`), { function: 'saveConfigToServer' });
return false;
}
} catch (error) {
errorHandler.logError('[SetupWizard] Save Config', error, { function: 'saveConfigToServer' });
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() || '';
const selectedProvider = document.getElementById('setup-dns-provider')?.value || 'technitium';
config.dns = {
provider: selectedProvider,
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');
}
};
}
// Summary "Continue →" — advance to the disk-safety warning step
const summaryNext = document.getElementById('setup-summary-next');
if (summaryNext) {
summaryNext.onclick = function(e) {
e.preventDefault();
showStep('setup-step-disk-safety');
};
}
// Disk-safety step navigation
const diskSafetyBack = document.getElementById('setup-disk-safety-back');
if (diskSafetyBack) {
diskSafetyBack.onclick = function(e) {
e.preventDefault();
showStep('setup-step-summary');
};
}
// Finish setup button (now on the disk-safety step)
const finishBtn = document.getElementById('setup-disk-safety-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();
}
};
// ── DC-171: capability reporting at DNS provider CHOICE time ────────────
// Per-provider honest capability hints. Keep in sync with the adapters'
// getCapabilities() in dashcaddy-api/src/dns/dns-providers/ — the parity
// test (dns-provider-parity.test.js) asserts every <option> value here has
// a hint entry.
const DNS_PROVIDER_HINTS = {
technitium: '✅ Full management — records, zones and health checks via the Technitium API. DashCaddy can create, delete, list and resolve records.',
cloudflare: '✅ Full management — records via the Cloudflare API token. DashCaddy can create, delete, list and resolve records.',
rfc2136: '⚠️ Partial (push-only protocol) — records can be created, deleted and resolved, and listed via AXFR zone transfer if the server permits transfers for your key (BIND: allow-transfer { key <name>; };). Listing requires that server-side setting.',
manual: 'ℹ️ No API control — DashCaddy shows the records you create yourself with your DNS provider\'s tools. Everything else (reverse proxy, SSL) works normally.',
};
function updateDnsProviderHints() {
const select = document.getElementById('setup-dns-provider');
const hints = document.getElementById('setup-dns-provider-hints');
if (!select || !hints) return;
hints.innerHTML = DNS_PROVIDER_HINTS[select.value]
? `<strong>${escapeHint(select.selectedOptions[0]?.text || select.value)}</strong><br>${DNS_PROVIDER_HINTS[select.value]}`
: '';
}
function escapeHint(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
const dnsProviderSelect = document.getElementById('setup-dns-provider');
if (dnsProviderSelect) {
dnsProviderSelect.addEventListener('change', updateDnsProviderHints);
updateDnsProviderHints();
}
})();