Port-forward UX: drop tile Step 4 and misleading local 'ready' status
The Element Call tile still appended a synthetic 'Step 4: Router Setup
Needed' to the domain diagnostic checklist, and every port table carried a
'Sovran_SystemsOS Status' column with Ready / Not ready yet verdicts.
Both were misleading: port forwarding happens on the router, which this
computer cannot inspect. A local ss/firewall probe can neither prove nor
disprove that forwarding works — and the LiveKit TURN relay range binds on
demand, so it reported 'Not ready yet' even on a perfectly working system.
- server.py: add ROUTER_FORWARD_ONLY_UNITS ({livekit.service}); skip the
local probe for those units, drop the step-4 append, replace extra_ports
with router_ports (no status field), and exclude router-only ports from
both tile health and /api/ports/health so they can't raise false alarms.
- helpers.js: new shared renderPortForwardGuideHtml() — one intro naming the
internal IP, explicit instructions (same internal/external port, match the
protocol, use port-range fields for 30000-40000), a colour-coded
TCP / UDP / TCP+UDP badge per row, and a closing note that the only real
test is loading the service from a phone on mobile data.
- features.js: enable-time modal uses the shared guide and now always lists
every port to forward (the old local pre-filter hid ports the user still
had to open).
- service-detail.js: tile port section uses the same guide; the SSH/non-domain
branch keeps a small 'not open on this computer yet' hint, which is a real
local fact, separate from router forwarding.
- onboarding.js: step 3 router note reworded to match (same number for
internal/external, notes that Element Call adds UDP ports).
- domain-setup.css: styles for the protocol badges and instruction list.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This commit is contained in:
co-authored by
arena-agent
parent
0e3cab78f8
commit
419680a847
@@ -319,6 +319,14 @@ _PORTS_ELEMENT_CALLING = [
|
||||
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||
]
|
||||
|
||||
# Units whose port requirements exist purely so the user can forward them in
|
||||
# their router. Whether those ports actually work can only be judged from
|
||||
# OUTSIDE the network, so we never show a local "ready/not ready" verdict for
|
||||
# them and they never affect tile health — we just tell the user what to
|
||||
# forward. (E.g. the LiveKit TURN relay range is bound on demand, so a local
|
||||
# `ss` check reports "closed" even on a perfectly working system.)
|
||||
ROUTER_FORWARD_ONLY_UNITS: set[str] = {"livekit.service"}
|
||||
|
||||
SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
|
||||
# Infrastructure
|
||||
"caddy.service": [],
|
||||
@@ -2895,7 +2903,11 @@ async def api_services():
|
||||
else:
|
||||
domain_reachability = "unreachable"
|
||||
|
||||
health_port_requirements = list(port_requirements)
|
||||
# Router-forwarded ports can only be verified from outside the network,
|
||||
# so they must not drive local health.
|
||||
health_port_requirements = (
|
||||
[] if unit in ROUTER_FORWARD_ONLY_UNITS else list(port_requirements)
|
||||
)
|
||||
if needs_domain:
|
||||
health_port_requirements = [
|
||||
{"port": "80", "protocol": "TCP"},
|
||||
@@ -3136,10 +3148,17 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
domain_check_steps = domain_eval.get("domain_check_steps", [])
|
||||
has_domain_issues = bool(domain_eval.get("has_issues"))
|
||||
|
||||
# Port requirements and statuses
|
||||
# Port requirements and statuses.
|
||||
#
|
||||
# For router-forward-only units we deliberately skip the local
|
||||
# listening/firewall probe: those ports live on the *router*, and a local
|
||||
# probe can neither prove nor disprove that forwarding works. Reporting a
|
||||
# local verdict there confused users more than it helped, so the UI just
|
||||
# lists what to forward.
|
||||
port_requirements = SERVICE_PORT_REQUIREMENTS.get(unit, [])
|
||||
router_forward_only = unit in ROUTER_FORWARD_ONLY_UNITS
|
||||
port_statuses: list[dict] = []
|
||||
if port_requirements:
|
||||
if port_requirements and not router_forward_only:
|
||||
listening, allowed = await asyncio.gather(
|
||||
loop.run_in_executor(None, _get_listening_ports),
|
||||
loop.run_in_executor(None, _get_firewall_allowed_ports),
|
||||
@@ -3154,30 +3173,19 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
"status": ps,
|
||||
"description": p.get("description", ""),
|
||||
})
|
||||
extra_ports = port_statuses if unit == "livekit.service" else []
|
||||
|
||||
if needs_domain and unit == "livekit.service":
|
||||
if has_domain_issues:
|
||||
domain_check_steps.append({
|
||||
"step": 4,
|
||||
"label": "Router Setup Needed",
|
||||
"status": "skipped",
|
||||
"detail": "Finish the domain steps first, then forward the Element Call ports in your router.",
|
||||
})
|
||||
else:
|
||||
# These checks are local-only (listening/firewall state on this computer),
|
||||
# not an outside-in verification of router/NAT forwarding.
|
||||
all_local_ready = all(p["status"] != "closed" for p in extra_ports)
|
||||
domain_check_steps.append({
|
||||
"step": 4,
|
||||
"label": "Router Setup Needed" if all_local_ready else "Sovran_SystemsOS Port Setup Needed",
|
||||
"status": "warning" if all_local_ready else "error",
|
||||
"detail": (
|
||||
"Sovran_SystemsOS is ready to use these ports on this computer. Now forward them in your router so Element Call can work from outside your home network."
|
||||
if all_local_ready
|
||||
else "Sovran_SystemsOS is not ready to use all required Element Call ports on this computer yet. Fix the ports marked “Not ready yet” below, then forward them in your router."
|
||||
),
|
||||
})
|
||||
# Ports the user must forward in their router (no local status — see above).
|
||||
router_ports = (
|
||||
[
|
||||
{
|
||||
"port": str(p.get("port", "")),
|
||||
"protocol": str(p.get("protocol", "TCP")),
|
||||
"description": p.get("description", ""),
|
||||
}
|
||||
for p in port_requirements
|
||||
]
|
||||
if router_forward_only
|
||||
else []
|
||||
)
|
||||
|
||||
# Compute composite health
|
||||
sync_progress: float | None = None
|
||||
@@ -3264,7 +3272,7 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
"domain_check_steps": domain_check_steps,
|
||||
"port_requirements": port_requirements,
|
||||
"port_statuses": port_statuses,
|
||||
"extra_ports": extra_ports,
|
||||
"router_ports": router_ports,
|
||||
"external_ip": external_ip,
|
||||
"internal_ip": internal_ip,
|
||||
"feature": feature_entry,
|
||||
@@ -3362,6 +3370,11 @@ async def api_ports_health():
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
# Router-forwarded ports are not locally verifiable — excluded from
|
||||
# aggregate health so they can't raise a false alarm.
|
||||
if unit in ROUTER_FORWARD_ONLY_UNITS:
|
||||
continue
|
||||
|
||||
ports = SERVICE_PORT_REQUIREMENTS.get(unit, [])
|
||||
if ports:
|
||||
enabled_port_requirements.append((entry.get("name", unit), unit, ports))
|
||||
|
||||
@@ -172,6 +172,58 @@ domain-field-actions {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Port-forwarding instructions ─────────────────────────────────── */
|
||||
|
||||
.port-req-steps {
|
||||
margin: 4px 0 12px;
|
||||
padding-left: 20px;
|
||||
font-size: 0.84rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.port-req-steps li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.port-proto-badge {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 2px 7px;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.port-proto-badge--tcp {
|
||||
color: #6dbf8b;
|
||||
background: rgba(109, 191, 139, 0.12);
|
||||
border: 1px solid rgba(109, 191, 139, 0.4);
|
||||
}
|
||||
|
||||
.port-proto-badge--udp {
|
||||
color: #6aa9e0;
|
||||
background: rgba(106, 169, 224, 0.12);
|
||||
border: 1px solid rgba(106, 169, 224, 0.4);
|
||||
}
|
||||
|
||||
.port-proto-badge--both {
|
||||
color: #e5a50a;
|
||||
background: rgba(229, 165, 10, 0.12);
|
||||
border: 1px solid rgba(229, 165, 10, 0.45);
|
||||
}
|
||||
|
||||
.port-proto-note {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-dim);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* ── Wallet Connections unique-hostname warning ───────────────────── */
|
||||
|
||||
.domain-nwc-warning {
|
||||
|
||||
@@ -279,24 +279,12 @@ function openPortRequirementsModal(featureName, ports, onContinue) {
|
||||
: '';
|
||||
|
||||
function renderPortRequirements(internalIp) {
|
||||
var rows = ports.map(function(p) {
|
||||
return '<tr><td class="port-req-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="port-req-proto">' + escHtml(p.protocol) + '</td>' +
|
||||
'<td class="port-req-desc">' + escHtml(p.description) + '</td></tr>';
|
||||
}).join("");
|
||||
var ipPart = internalIp
|
||||
? ' to this computer’s internal IP <code class="port-req-internal-ip">' + escHtml(internalIp) + '</code>'
|
||||
: " to this computer's internal IP";
|
||||
|
||||
$portReqBody.innerHTML =
|
||||
'<p class="port-req-intro">For <strong>' + escHtml(featureName) + '</strong> to work for people outside your home network, ' +
|
||||
'forward each port below' + ipPart + " in your router's port-forwarding settings. " +
|
||||
'Set the internal and external port to the same number.</p>' +
|
||||
'<table class="port-req-table">' +
|
||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
'<p class="port-req-hint">💡 You can review these ports with live status any time on the <strong>' + escHtml(featureName) + '</strong> tile after enabling.</p>' +
|
||||
renderPortForwardGuideHtml(ports, {
|
||||
internalIp: internalIp,
|
||||
serviceName: featureName,
|
||||
}) +
|
||||
'<p class="port-req-hint">💡 This list is always available again on the <strong>' + escHtml(featureName) + '</strong> tile.</p>' +
|
||||
'<div class="domain-field-actions">' +
|
||||
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
||||
continueBtn +
|
||||
@@ -429,39 +417,10 @@ function handleFeatureToggle(feat, newEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check which ports are actually closed before showing the modal
|
||||
fetch("/api/ports/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ports: ports }),
|
||||
})
|
||||
.then(function(r) {
|
||||
if (!r.ok) throw new Error("Port status request failed: " + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
var portStatuses = {};
|
||||
(data.ports || []).forEach(function(p) {
|
||||
portStatuses[p.port + "/" + p.protocol] = p.status;
|
||||
});
|
||||
|
||||
var closedPorts = ports.filter(function(p) {
|
||||
var key = p.port + "/" + p.protocol;
|
||||
var status = portStatuses[key] || "unknown";
|
||||
return status !== "listening" && status !== "firewall_open";
|
||||
});
|
||||
|
||||
if (closedPorts.length === 0) {
|
||||
proceedAfterPortCheck();
|
||||
} else {
|
||||
openPortRequirementsModal(feat.name, closedPorts, proceedAfterPortCheck);
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.warn("Failed to fetch port status for feature enable flow:", err);
|
||||
// Safe fallback if status check fails
|
||||
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
|
||||
});
|
||||
// Always show the full list of ports to forward. Port forwarding happens on
|
||||
// the router, which this computer cannot inspect — a local check would only
|
||||
// hide ports the user still has to open.
|
||||
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
|
||||
}
|
||||
|
||||
if (feat.id === "bitcoin-core") {
|
||||
|
||||
@@ -40,6 +40,66 @@ function linkify(str) {
|
||||
return escHtml(str).replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer" class="creds-link">$1</a>');
|
||||
}
|
||||
|
||||
// ── Router port-forwarding guide ──────────────────────────────────
|
||||
// Whether a port is truly reachable can only be judged from OUTSIDE the
|
||||
// network, so we never show a local "ready" verdict here. We only tell the
|
||||
// user exactly what to enter in their router.
|
||||
|
||||
// Render the protocol cell so TCP / UDP / both is unmistakable.
|
||||
function portProtocolHtml(protocol) {
|
||||
var p = String(protocol || "TCP").toUpperCase();
|
||||
var isTcp = p.indexOf("TCP") !== -1;
|
||||
var isUdp = p.indexOf("UDP") !== -1;
|
||||
if (isTcp && isUdp) {
|
||||
return '<span class="port-proto-badge port-proto-badge--both">TCP + UDP</span>' +
|
||||
'<span class="port-proto-note">both required</span>';
|
||||
}
|
||||
if (isUdp) return '<span class="port-proto-badge port-proto-badge--udp">UDP</span>';
|
||||
return '<span class="port-proto-badge port-proto-badge--tcp">TCP</span>';
|
||||
}
|
||||
|
||||
// ports: [{ port, protocol, description }]
|
||||
// opts: { internalIp, serviceName, tableClass, introClass, noteClass }
|
||||
function renderPortForwardGuideHtml(ports, opts) {
|
||||
opts = opts || {};
|
||||
var tableClass = opts.tableClass || "port-req-table";
|
||||
var introClass = opts.introClass || "port-req-intro";
|
||||
var noteClass = opts.noteClass || "port-req-hint";
|
||||
var ipHtml = opts.internalIp
|
||||
? '<code class="port-req-internal-ip">' + escHtml(opts.internalIp) + '</code>'
|
||||
: 'this computer’s <strong>internal IP</strong> (shown as “Internal IP” at the top of the Hub dashboard)';
|
||||
|
||||
var rows = (ports || []).map(function(p) {
|
||||
return '<tr>' +
|
||||
'<td class="port-req-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="port-req-proto">' + portProtocolHtml(p.protocol) + '</td>' +
|
||||
'<td class="port-req-desc">' + escHtml(p.description || "") + '</td>' +
|
||||
'</tr>';
|
||||
}).join("");
|
||||
|
||||
var forWhat = opts.serviceName
|
||||
? 'For <strong>' + escHtml(opts.serviceName) + '</strong> to be reachable from outside your home network, open'
|
||||
: 'Open';
|
||||
|
||||
return '<p class="' + introClass + '">' +
|
||||
forWhat + ' the ports below in your router’s <strong>port forwarding</strong> settings ' +
|
||||
'and point them at ' + ipHtml + '.' +
|
||||
'</p>' +
|
||||
'<ul class="port-req-steps">' +
|
||||
'<li>Set the <strong>internal (private) port</strong> and the <strong>external (public) port</strong> to the <strong>same number</strong>.</li>' +
|
||||
'<li>Match the <strong>protocol</strong> exactly — a rule set to TCP will not pass UDP traffic. Where the table says <strong>TCP + UDP</strong>, create both rules (or pick “Both”/“TCP/UDP” if your router offers it).</li>' +
|
||||
'<li>For a range such as <strong>30000-40000</strong>, use your router’s port-range fields — start 30000, end 40000 — rather than one rule per port.</li>' +
|
||||
'</ul>' +
|
||||
'<table class="' + tableClass + '">' +
|
||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Used for</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
'<p class="' + noteClass + '">' +
|
||||
'📱 <strong>How to confirm it worked:</strong> forwarding happens on your router, so it can only be verified from outside your network. ' +
|
||||
'Turn Wi-Fi off on your phone and open the service over mobile data — if it loads, your ports are open.' +
|
||||
'</p>';
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
@@ -514,85 +514,41 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
domainActionHtml +
|
||||
'</div>';
|
||||
|
||||
if (unit === "livekit.service" && data.extra_ports && data.extra_ports.length > 0) {
|
||||
if (data.router_ports && data.router_ports.length > 0) {
|
||||
var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
|
||||
var internalIp = trimmedInternalIp || "";
|
||||
var internalIpHtml = internalIp ? escHtml(internalIp) : "Could not detect";
|
||||
var forwardNote = internalIp
|
||||
? 'Forward each port below in your router to this computer’s internal IP <code class="port-req-internal-ip">' + internalIpHtml + '</code>, using the same internal and external port.'
|
||||
: 'Forward each port below in your router to this computer’s internal IP, using the same internal and external port.';
|
||||
var domainConfigured = !!(data.domain && String(data.domain).trim());
|
||||
var extraRows = "";
|
||||
data.extra_ports.forEach(function(p) {
|
||||
var statusIcon, statusClass2;
|
||||
if (!effectiveEnabled) {
|
||||
statusIcon = "⚠ Configure Element Call first";
|
||||
statusClass2 = "port-status-open";
|
||||
} else if (!domainConfigured) {
|
||||
statusIcon = "⚠ Configure domain first";
|
||||
statusClass2 = "port-status-open";
|
||||
} else if (p.status === "listening") {
|
||||
statusIcon = "✅ Ready";
|
||||
statusClass2 = "port-status-listening";
|
||||
} else if (p.status === "firewall_open") {
|
||||
statusIcon = "✅ Ready";
|
||||
statusClass2 = "port-status-open";
|
||||
} else if (p.status === "closed") {
|
||||
statusIcon = "❌ Not ready yet";
|
||||
statusClass2 = "port-status-closed";
|
||||
} else {
|
||||
statusIcon = "— Could not check";
|
||||
statusClass2 = "port-status-unknown";
|
||||
}
|
||||
extraRows += '<tr>' +
|
||||
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
|
||||
'<td class="svc-detail-port-table-desc">' + escHtml(p.description || "") + '</td>' +
|
||||
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
|
||||
'<div class="svc-detail-port-note">' + forwardNote + '</div>' +
|
||||
'<table class="svc-detail-port-table">' +
|
||||
'<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
|
||||
'<tbody>' + extraRows + '</tbody>' +
|
||||
'</table>' +
|
||||
'<div class="svc-detail-port-note">✅ = Sovran_SystemsOS is ready on this computer. Router-side forwarding itself can only be verified from outside your network (e.g. a phone on mobile data).</div>' +
|
||||
renderPortForwardGuideHtml(data.router_ports, {
|
||||
internalIp: trimmedInternalIp || null,
|
||||
tableClass: "svc-detail-port-table",
|
||||
introClass: "svc-detail-port-note",
|
||||
noteClass: "svc-detail-port-note",
|
||||
}) +
|
||||
'</div>';
|
||||
}
|
||||
} else if (data.port_statuses && data.port_statuses.length > 0) {
|
||||
// Non-domain services (SSH) keep local single-port checks.
|
||||
var portTableRows = "";
|
||||
data.port_statuses.forEach(function(p) {
|
||||
var statusIcon, statusClass2;
|
||||
if (p.status === "listening") {
|
||||
statusIcon = "✅ Ready";
|
||||
statusClass2 = "port-status-listening";
|
||||
} else if (p.status === "firewall_open") {
|
||||
statusIcon = "✅ Ready";
|
||||
statusClass2 = "port-status-open";
|
||||
} else if (p.status === "closed") {
|
||||
statusIcon = "❌ Not ready";
|
||||
statusClass2 = "port-status-closed";
|
||||
} else {
|
||||
statusIcon = "— Could not check";
|
||||
statusClass2 = "port-status-unknown";
|
||||
}
|
||||
portTableRows += '<tr>' +
|
||||
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
|
||||
'<td class="svc-detail-port-table-desc">' + escHtml(p.description || "") + '</td>' +
|
||||
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
|
||||
'</tr>';
|
||||
// Non-domain services (e.g. SSH): show what to forward, plus a short
|
||||
// note if the service isn't actually listening on this computer yet.
|
||||
var localInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
|
||||
var notListening = data.port_statuses.filter(function(p) {
|
||||
return p.status === "closed";
|
||||
});
|
||||
var localNote = notListening.length
|
||||
? '<div class="svc-detail-port-note port-status-closed">' +
|
||||
'⚠ Port ' + escHtml(notListening.map(function(p) { return p.port; }).join(", ")) +
|
||||
' is not open on this computer yet — enable the service below first, then forward it in your router.' +
|
||||
'</div>'
|
||||
: "";
|
||||
html += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Port Requirements</div>' +
|
||||
'<div class="svc-detail-port-note">This shows whether Sovran_SystemsOS is ready to use this port on this computer. If you need access from outside your home network, forward this port in your router.</div>' +
|
||||
'<table class="svc-detail-port-table">' +
|
||||
'<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
|
||||
'<tbody>' + portTableRows + '</tbody>' +
|
||||
'</table>' +
|
||||
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
|
||||
'<div class="svc-detail-port-note">Only needed if you want to reach this service from <strong>outside</strong> your home network. On your local network it already works without any router changes.</div>' +
|
||||
renderPortForwardGuideHtml(data.port_statuses, {
|
||||
internalIp: localInternalIp || null,
|
||||
tableClass: "svc-detail-port-table",
|
||||
introClass: "svc-detail-port-note",
|
||||
noteClass: "svc-detail-port-note",
|
||||
}) +
|
||||
localNote +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -383,10 +383,12 @@ async function loadStep3() {
|
||||
? ' to this computer’s internal IP <strong>' + escHtml(internalIp) + '</strong>'
|
||||
: ' to this computer’s internal IP';
|
||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
||||
+ '🔌 <strong>One router task:</strong> forward ports <strong>80</strong> and <strong>443</strong> (TCP)'
|
||||
+ routerIpPart + '. They are required for HTTPS and SSL certificates — without them your services cannot be reached from outside your home network. '
|
||||
+ 'Forward port <strong>22</strong> (TCP) too if you want remote SSH access. '
|
||||
+ 'Element Call needs a few extra ports, but you’ll be shown those when you enable it.'
|
||||
+ '🔌 <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.'
|
||||
+ '</div>';
|
||||
|
||||
relevantDomains.forEach(function(d) {
|
||||
|
||||
Reference in New Issue
Block a user