Unify Njal.la + router port-forwarding guidance across onboarding and feature enable modals
Single source of truth (static/js/domain-prereqs.js) for the domain
prerequisite instructions so all three surfaces read identically:
- Server + Desktop first-boot onboarding wizard (step 3)
- Lightning Wallet Connections (NWC) enable modal (Node-only mode)
- BTCPay Server (web) enable modal (Node-only mode)
Both feature-enable flows share openDomainSetupModal(), which now drops
its role-branched intro and 'Option A/B' blocks in favor of the shared
renderers. All surfaces now consistently cover:
1. A domain from Njal.la (account, subdomain-vs-separate-domain,
Dynamic record with host-part-only Name field, auto-filled IP,
DDNS curl command)
2. Router access — forward ports 80 & 443 (TCP) to this computer's
internal IP, once, for HTTPS/SSL (with CGNAT note)
3. How to get Njal.la working, step by step
The reconfigure/troubleshooting modal gains the same router reminder,
since 'domain not reachable' is often the port forwarding rather than
DNS. domain-prereqs.js is loaded via asset_version cache busting and
ships automatically (installPhase copies the package wholesale).
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This commit is contained in:
co-authored by
arena-agent
parent
d0ec6dd760
commit
2622223033
@@ -0,0 +1,146 @@
|
||||
/* Sovran_SystemsOS Hub — Shared domain-prerequisite instructions.
|
||||
|
||||
SINGLE SOURCE OF TRUTH for the "you need a Njal.la domain + router port
|
||||
forwarding" guidance that must read identically everywhere it appears:
|
||||
|
||||
• First-boot onboarding wizard (Server + Desktop role)
|
||||
— onboarding.js step 3
|
||||
• Feature-enable domain modal (Node role, and any role)
|
||||
— features.js openDomainSetupModal()
|
||||
(Lightning Wallet Connections / NWC, BTCPay Server, Haven, …)
|
||||
• Domain reconfigure / troubleshooting modal
|
||||
— features.js openDomainReconfigureModal()
|
||||
|
||||
Keep these three surfaces word-for-word consistent: always edit this
|
||||
file, never fork the wording inline. Plain classic script (no modules) —
|
||||
both templates load it with a plain <script> tag. */
|
||||
"use strict";
|
||||
|
||||
/* Escape helper local to this file so it is self-contained on both pages. */
|
||||
function dpEsc(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/* ── "What you'll need" intro ──────────────────────────────────────
|
||||
opts.serviceName — e.g. "Lightning Wallet Connections", or null for
|
||||
the multi-service (onboarding) wording.
|
||||
opts.hostExample — e.g. "lightning" → lightning.yourdomain.com */
|
||||
function renderDomainNeedsHtml(opts) {
|
||||
var serviceName = opts.serviceName || null;
|
||||
var hostExample = dpEsc(opts.hostExample || "myservice");
|
||||
var html = "";
|
||||
|
||||
if (serviceName) {
|
||||
html += "<p>To enable <strong>" + dpEsc(serviceName) + "</strong>, you'll need two things first:</p>";
|
||||
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
|
||||
html += "<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
|
||||
+ "— privacy-friendly, no personal details required, accepts Bitcoin. "
|
||||
+ dpEsc(serviceName) + " gets its own hostname: a subdomain (e.g. <code>" + hostExample + ".yourdomain.com</code>) "
|
||||
+ "or a separate domain — your choice. Subdomains are free, and one domain can have many.</li>";
|
||||
html += "<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
|
||||
+ "to this computer once. All domain-based services share these two ports; "
|
||||
+ "they're required for HTTPS and SSL certificates.</li>";
|
||||
html += "</ol>";
|
||||
} else {
|
||||
html += "<p><strong>Each service below needs two things set up:</strong></p>";
|
||||
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
|
||||
html += "<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
|
||||
+ "— privacy-friendly, no personal details required, accepts Bitcoin. "
|
||||
+ "Each service gets its own hostname: its own subdomain (e.g. <code>" + hostExample + ".yourdomain.com</code>) "
|
||||
+ "or a separate domain — your choice. Subdomains are free, and one domain can have many, "
|
||||
+ "so a single domain can serve every service.</li>";
|
||||
html += "<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
|
||||
+ "to this computer once. All domain-based services share these two ports; "
|
||||
+ "they're required for HTTPS and SSL certificates.</li>";
|
||||
html += "</ol>";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ── "How to set it up at Njal.la" steps ───────────────────────────
|
||||
opts.hostExample — host part used in the examples (default "call").
|
||||
opts.pasteHint — where the curl command goes:
|
||||
"below" (single service) or "next to its service below"
|
||||
(onboarding, many services). */
|
||||
function renderNjallaStepsHtml(opts) {
|
||||
var hostExample = dpEsc((opts && opts.hostExample) || "call");
|
||||
var pasteHint = (opts && opts.pasteHint) || "below";
|
||||
var html = "";
|
||||
|
||||
html += "<p style=\"margin-top:12px;\"><strong>How to set it up at Njal.la:</strong></p>";
|
||||
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
|
||||
html += "<li>Create an account at <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">https://njal.la</a> and buy a domain.</li>";
|
||||
html += "<li>Add a <strong>Dynamic</strong> record for the hostname:"
|
||||
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
|
||||
+ "<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>"
|
||||
+ "(For "" + hostExample + ".yourdomain.com" you'd type just: <code>" + hostExample + "</code>.)<br>"
|
||||
+ "⚠ Do NOT type the full domain here — Njal.la adds it automatically.</li>"
|
||||
+ "<li>Dedicating a whole separate domain to this service? Leave the Name field blank or use <code>@</code>.</li>"
|
||||
+ "</ul>"
|
||||
+ "</li>";
|
||||
html += "<li>A Dynamic record has <strong>NO IP field</strong> — you don't enter an IP anywhere. "
|
||||
+ "It auto-fills once Sovran_SystemsOS runs the update command (on save, and again after every reboot).</li>";
|
||||
html += "<li>Njal.la gives you a curl command, e.g.:<br>"
|
||||
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=' + hostExample + '.yourdomain.com&k=abc123&auto"</code><br>'
|
||||
+ "Copy it and paste it " + pasteHint + ".</li>";
|
||||
html += "</ol>";
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ── "One router task" port-forwarding box ─────────────────────────
|
||||
opts.internalIp — LAN IP string, or empty/null for generic wording.
|
||||
opts.plural — true for multi-service (onboarding) wording.
|
||||
opts.includeSsh — also mention port 22 for SSH (onboarding).
|
||||
opts.extraNote — extra sentence appended at the end (optional). */
|
||||
function renderRouterPortsHtml(opts) {
|
||||
opts = opts || {};
|
||||
var ipPart = opts.internalIp
|
||||
? " to this computer’s internal IP <strong>" + dpEsc(opts.internalIp) + "</strong>"
|
||||
: " to this computer’s internal IP";
|
||||
var serviceWord = opts.plural ? "services" : "service";
|
||||
|
||||
var html = "";
|
||||
html += "🔌 <strong>One router task:</strong> in your router’s <strong>port forwarding</strong> settings, "
|
||||
+ "forward port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>"
|
||||
+ ipPart + ". Use the <strong>same number for the internal and external port</strong>. "
|
||||
+ "This only needs to be done once — all domain services share these ports — "
|
||||
+ "but HTTPS and SSL certificates won’t work without them, so your " + serviceWord + " "
|
||||
+ "can’t be reached from outside your home network. "
|
||||
+ "You’ll need normal access to your router’s settings with working port forwarding — "
|
||||
+ "if your ISP blocks it (e.g. CGNAT), domain-based services can’t be reached from the internet. ";
|
||||
if (opts.includeSsh) {
|
||||
html += "Add port <strong>22 (TCP)</strong> as well if you want remote SSH access. ";
|
||||
}
|
||||
if (opts.extraNote) {
|
||||
html += dpEsc(opts.extraNote);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ── Async helper: fill a router-box container with the internal IP ──
|
||||
Renders generic text immediately, then upgrades to the concrete LAN IP
|
||||
if /api/network provides one. Best-effort — never blocks the UI. */
|
||||
function renderRouterPortsBox(elId, opts) {
|
||||
var el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
el.innerHTML = renderRouterPortsHtml(opts || {});
|
||||
fetch("/api/network")
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var target = document.getElementById(elId);
|
||||
if (!target) return;
|
||||
var ip = data && data.internal_ip;
|
||||
if (ip && ip !== "unavailable") {
|
||||
var next = {};
|
||||
for (var k in (opts || {})) next[k] = opts[k];
|
||||
next.internalIp = String(ip).trim();
|
||||
target.innerHTML = renderRouterPortsHtml(next);
|
||||
}
|
||||
})
|
||||
.catch(function() { /* generic wording already shown — fine */ });
|
||||
}
|
||||
@@ -86,45 +86,17 @@ function openDomainSetupModal(feat, onSaved) {
|
||||
var domainPlaceholder = isWalletConnections ? "lightning.yourdomain.com" : "myservice.example.com";
|
||||
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : "call.yourdomain.com";
|
||||
|
||||
var introHtml;
|
||||
if (_currentRole === "node") {
|
||||
introHtml =
|
||||
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la.</p>' +
|
||||
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">njal.la</a>.</li>' +
|
||||
'<li>Set up a domain for it — either a free subdomain or a separate domain. Pick one option:</li>' +
|
||||
'</ol>';
|
||||
} else {
|
||||
introHtml =
|
||||
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la. ' +
|
||||
'In your Njal.la account, set up a domain for it — either a free subdomain or a separate domain. Pick one option:</p>';
|
||||
}
|
||||
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
|
||||
// identical wording to the Server + Desktop onboarding wizard and every other
|
||||
// domain-based feature, regardless of role (Node / Desktop / Server+Desktop).
|
||||
var hostExample = isWalletConnections ? "lightning" : "call";
|
||||
|
||||
$domainSetupBody.innerHTML =
|
||||
'<div class="domain-setup-intro">' +
|
||||
nwcWarning +
|
||||
introHtml +
|
||||
'<details style="margin-top:10px;">' +
|
||||
'<summary style="cursor:pointer;font-weight:600;">Option A — Free subdomain (recommended)</summary>' +
|
||||
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||
'<li>In Njal.la, open a domain you own and click "Add record".</li>' +
|
||||
'<li>Set record type to <strong>Dynamic</strong>.</li>' +
|
||||
'<li>In the <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>' +
|
||||
'(Example only, your choice — for "' + domainLabelExample + '" you'd type just: <code>' + (isWalletConnections ? 'lightning' : 'call') + '</code>)<br>' +
|
||||
'⚠ Do NOT type the full domain here — Njal.la adds it automatically.</li>' +
|
||||
'<li>A Dynamic record has NO IP field — the IP auto-fills after the rebuild/reboot.</li>' +
|
||||
'<li>Copy the curl command Njal.la gives you, e.g.:<br>' +
|
||||
'<code style="font-size:0.8em;">curl "https://njal.la/update/?h=' + domainLabelExample + '&k=abc123&auto"</code></li>' +
|
||||
'</ol>' +
|
||||
'</details>' +
|
||||
'<details style="margin-top:6px;">' +
|
||||
'<summary style="cursor:pointer;font-weight:600;">Option B — Separate / new domain</summary>' +
|
||||
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||
'<li>In Njal.la, buy the domain you want.</li>' +
|
||||
'<li>Add a Dynamic record as in Option A. If this domain is dedicated to the service, leave the Name field blank or use <code>@</code>.</li>' +
|
||||
'<li>Copy the curl command Njal.la gives you.</li>' +
|
||||
'</ol>' +
|
||||
'</details>' +
|
||||
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample }) +
|
||||
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
|
||||
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
||||
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. ' + domainLabelExample + ') or a separate domain — and paste its curl command.</p>' +
|
||||
'</div>' +
|
||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' +
|
||||
@@ -170,6 +142,9 @@ function openDomainSetupModal(feat, onSaved) {
|
||||
});
|
||||
|
||||
$domainSetupModal.classList.add("open");
|
||||
|
||||
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
||||
renderRouterPortsBox("domain-router-box");
|
||||
}
|
||||
|
||||
function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
@@ -218,7 +193,9 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
||||
'<li>If the IP is wrong or the record is missing, update it</li>' +
|
||||
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
|
||||
'<li>Confirm ports <strong>80</strong> and <strong>443</strong> (TCP) are still forwarded on your router to this computer — see the reminder below:</li>' +
|
||||
'</ol>' +
|
||||
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
||||
'</div>' +
|
||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' +
|
||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=' + domainPlaceholder + '&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
||||
@@ -263,6 +240,9 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
});
|
||||
|
||||
$domainSetupModal.classList.add("open");
|
||||
|
||||
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
||||
renderRouterPortsBox("domain-router-box");
|
||||
}
|
||||
|
||||
function closeDomainSetupModal() {
|
||||
|
||||
@@ -357,38 +357,24 @@ async function loadStep3() {
|
||||
if (relevantDomains.length === 0) {
|
||||
html += '<p class="onboarding-body-text">No domain-based services are enabled for your role. You can skip this step.</p>';
|
||||
} else {
|
||||
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
|
||||
// identical wording to the feature-enable domain modal shown for NWC,
|
||||
// BTCPay Server, and every other domain-based feature, in every role.
|
||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
||||
+ '<p style="margin:0 0 8px;"><strong>Sovran_SystemsOS uses Njal.la for domains and Dynamic DNS.</strong></p>'
|
||||
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
|
||||
+ '<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a>.</li>'
|
||||
+ '<li>Buy at least one domain. Each service below needs its own domain — you can either give each service its own subdomain of a single domain you buy (subdomains are free, and one domain can have many), OR use a separate domain for each. Your choice.</li>'
|
||||
+ '<li>For each service, add a <strong>Dynamic</strong> record in Njal.la:'
|
||||
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
|
||||
+ '<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>'
|
||||
+ '(Example only, your choice — for "call.yourdomain.com" you'd type just: <code>call</code>.)<br>'
|
||||
+ 'If you bought a whole separate domain just for this service, leave Name blank or use <code>@</code>.<br>'
|
||||
+ '⚠ Do NOT type the full domain in the Name field — Njal.la adds it automatically.</li>'
|
||||
+ '<li>A Dynamic record has NO IP field. You don't enter an IP anywhere — it auto-fills once Sovran_SystemsOS updates it (on save, and again after reboot).</li>'
|
||||
+ '</ul>'
|
||||
+ '</li>'
|
||||
+ '<li>Njal.la gives you a curl command like:<br>'
|
||||
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=call.yourdomain.com&k=abc123&auto"</code></li>'
|
||||
+ '</ol>'
|
||||
+ renderDomainNeedsHtml({ serviceName: null, hostExample: "call" })
|
||||
+ renderNjallaStepsHtml({ hostExample: "call", pasteHint: "next to its service below" })
|
||||
+ '</div>';
|
||||
html += '<p class="onboarding-hint">Enter each service\'s full domain — a subdomain (e.g. <code>call.yourdomain.com</code>) or a separate domain (e.g. <code>call.com</code>) — and its Njal.la DDNS curl command.</p>';
|
||||
|
||||
// Compact router note (full port guidance is shown when a feature is
|
||||
// enabled, and lives on each service tile afterwards)
|
||||
var routerIpPart = internalIp
|
||||
? ' to this computer’s internal IP <strong>' + escHtml(internalIp) + '</strong>'
|
||||
: ' to this computer’s internal IP';
|
||||
// Router note (the same wording is shown again, per-service, whenever a
|
||||
// domain-based feature is enabled, and lives on each service tile afterwards)
|
||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
||||
+ '🔌 <strong>One router task:</strong> in your router’s <strong>port forwarding</strong> settings, forward '
|
||||
+ 'port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>'
|
||||
+ routerIpPart + '. Use the <strong>same number for the internal and external port</strong>. '
|
||||
+ 'These are required for HTTPS and SSL certificates — without them your services cannot be reached from outside your home network. '
|
||||
+ 'Add port <strong>22 (TCP)</strong> as well if you want remote SSH access. '
|
||||
+ 'Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.'
|
||||
+ renderRouterPortsHtml({
|
||||
internalIp: internalIp,
|
||||
plural: true,
|
||||
includeSsh: true,
|
||||
extraNote: "Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.",
|
||||
})
|
||||
+ '</div>';
|
||||
|
||||
relevantDomains.forEach(function(d) {
|
||||
|
||||
Reference in New Issue
Block a user