/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard Drives the 4-step post-install setup flow. */ "use strict"; // ── Constants ───────────────────────────────────────────────────── const TOTAL_STEPS = 4; // Steps to skip per role (steps 2 and 3 involve domain/port setup) const ROLE_SKIP_STEPS = { "desktop": [2, 3], "node": [2, 3], }; // ── Role state (loaded at init) ─────────────────────────────────── var _onboardingRole = "server_plus_desktop"; // Domains that may need configuration, with service unit mapping for enabled check const DOMAIN_DEFS = [ { name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true }, { name: "haven", label: "Haven Nostr Relay", unit: "haven-relay.service", needsDdns: true }, { name: "element-calling", label: "Element Video/Audio Calling", unit: "livekit.service", needsDdns: true }, { name: "vaultwarden", label: "Vaultwarden (Password Vault)", unit: "vaultwarden.service", needsDdns: true }, { name: "btcpayserver", label: "BTCPay Server", unit: "btcpayserver.service", needsDdns: true }, { name: "nextcloud", label: "Nextcloud", unit: "phpfpm-nextcloud.service", needsDdns: true }, { name: "wordpress", label: "WordPress", unit: "phpfpm-wordpress.service", needsDdns: true }, ]; // ── State ───────────────────────────────────────────────────────── var _currentStep = 1; var _servicesData = null; var _domainsData = null; // ── Helpers ─────────────────────────────────────────────────────── function escHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } async function apiFetch(path, options) { var res = await fetch(path, options || {}); if (!res.ok) { var detail = res.status + " " + res.statusText; try { var body = await res.json(); if (body && body.detail) detail = body.detail; } catch (e) {} throw new Error(detail); } return res.json(); } function setStatus(elId, msg, type) { var el = document.getElementById(elId); if (!el) return; el.textContent = msg; el.className = "onboarding-save-status" + (type ? " onboarding-save-status--" + type : ""); } // ── Progress / step navigation ──────────────────────────────────── function updateProgress(step) { var fill = document.getElementById("onboarding-progress-fill"); if (fill) { fill.style.width = Math.round(((step - 1) / (TOTAL_STEPS - 1)) * 100) + "%"; } var dots = document.querySelectorAll(".onboarding-step-dot"); dots.forEach(function(dot) { var ds = parseInt(dot.dataset.step, 10); dot.classList.remove("active", "completed"); if (ds < step) dot.classList.add("completed"); if (ds === step) dot.classList.add("active"); }); } function showStep(step) { for (var i = 1; i <= TOTAL_STEPS; i++) { var panel = document.getElementById("step-" + i); if (panel) panel.style.display = (i === step) ? "" : "none"; } _currentStep = step; updateProgress(step); // Lazy-load step content if (step === 2) loadStep2(); if (step === 3) loadStep3(); } // Return the next step number, skipping over role-excluded steps function nextStep(current) { var skip = ROLE_SKIP_STEPS[_onboardingRole] || []; var next = current + 1; while (next < TOTAL_STEPS && skip.indexOf(next) !== -1) next++; return next; } // Return the previous step number, skipping over role-excluded steps function prevStep(current) { var skip = ROLE_SKIP_STEPS[_onboardingRole] || []; var prev = current - 1; while (prev > 1 && skip.indexOf(prev) !== -1) prev--; return prev; } // ── Step 1: Welcome ─────────────────────────────────────────────── async function loadStep1() { try { var cfg = await apiFetch("/api/config"); var badge = document.getElementById("onboarding-role-badge"); if (badge && cfg.role_label) badge.textContent = cfg.role_label; } catch (_) {} } // ── Step 2: Domain Configuration ───────────────────────────────── async function loadStep2() { var body = document.getElementById("step-2-body"); if (!body) return; try { // Fetch services, domains, and network info in parallel var results = await Promise.all([ apiFetch("/api/services"), apiFetch("/api/domains/status"), apiFetch("/api/network"), ]); _servicesData = results[0]; _domainsData = results[1]; var networkData = results[2]; } catch (err) { body.innerHTML = '
⚠ Could not load service data: ' + escHtml(err.message) + '
'; return; } var externalIp = (networkData && networkData.external_ip) || "Unknown (could not retrieve)"; // Build set of enabled service units var enabledUnits = new Set(); (_servicesData || []).forEach(function(svc) { if (svc.enabled) enabledUnits.add(svc.unit); }); // Filter domain defs to only those whose service is enabled var relevantDomains = DOMAIN_DEFS.filter(function(d) { return enabledUnits.has(d.unit); }); var html = ""; if (relevantDomains.length === 0) { html += 'No domain-based services are enabled for your role. You can skip this step.
'; } else { html += 'curl "https://njal.la/update/?h=sub.domain.com&k=abc123&auto"Enter each fully-qualified subdomain (e.g. matrix.yourdomain.com) and its Njal.la DDNS curl command.
ℹ Paste the curl URL from your Njal.la dashboard\'s Dynamic record
'; html += 'Let\'s Encrypt uses this for certificate expiry notifications.
'; html += ''; html += 'Checking ports…
'; var networkData = null; try { networkData = await apiFetch("/api/network"); } catch (err) { body.innerHTML = '⚠ Could not load network data: ' + escHtml(err.message) + '
'; return; } var internalIp = (networkData && networkData.internal_ip) || "unknown"; var ip = escHtml(internalIp); var html = '' + '⚠ Each port only needs to be forwarded once — all services share the same ports.' + '
'; html += '| Port | Protocol | Forward to | Purpose |
|---|---|---|---|
| 80 | TCP | ' + ip + ' | HTTP |
| 443 | TCP | ' + ip + ' | HTTPS |
| 22 | TCP | ' + ip + ' | SSH Remote Access |
| 8448 | TCP | ' + ip + ' | Matrix Federation |
| Port | Protocol | Forward to | Purpose |
|---|---|---|---|
| 7881 | TCP | ' + ip + ' | LiveKit WebRTC signalling |
| 7882–7894 | UDP | ' + ip + ' | LiveKit media streams |
| 5349 | TCP | ' + ip + ' | TURN over TLS |
| 3478 | UDP | ' + ip + ' | TURN (STUN/relay) |
| 30000–40000 | TCP/UDP | ' + ip + ' | TURN relay (WebRTC) |
http://192.168.1.1 or http://192.168.0.1