Merge pull request #349 from naturallaw777/arena/019faad3-sovran-systemsos
Port-forward UX: remove tile Step 4 and misleading local port 'ready' status
This commit is contained in:
@@ -319,6 +319,14 @@ _PORTS_ELEMENT_CALLING = [
|
|||||||
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
{"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]] = {
|
SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
|
||||||
# Infrastructure
|
# Infrastructure
|
||||||
"caddy.service": [],
|
"caddy.service": [],
|
||||||
@@ -2895,7 +2903,11 @@ async def api_services():
|
|||||||
else:
|
else:
|
||||||
domain_reachability = "unreachable"
|
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:
|
if needs_domain:
|
||||||
health_port_requirements = [
|
health_port_requirements = [
|
||||||
{"port": "80", "protocol": "TCP"},
|
{"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", [])
|
domain_check_steps = domain_eval.get("domain_check_steps", [])
|
||||||
has_domain_issues = bool(domain_eval.get("has_issues"))
|
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, [])
|
port_requirements = SERVICE_PORT_REQUIREMENTS.get(unit, [])
|
||||||
|
router_forward_only = unit in ROUTER_FORWARD_ONLY_UNITS
|
||||||
port_statuses: list[dict] = []
|
port_statuses: list[dict] = []
|
||||||
if port_requirements:
|
if port_requirements and not router_forward_only:
|
||||||
listening, allowed = await asyncio.gather(
|
listening, allowed = await asyncio.gather(
|
||||||
loop.run_in_executor(None, _get_listening_ports),
|
loop.run_in_executor(None, _get_listening_ports),
|
||||||
loop.run_in_executor(None, _get_firewall_allowed_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,
|
"status": ps,
|
||||||
"description": p.get("description", ""),
|
"description": p.get("description", ""),
|
||||||
})
|
})
|
||||||
extra_ports = port_statuses if unit == "livekit.service" else []
|
# Ports the user must forward in their router (no local status — see above).
|
||||||
|
router_ports = (
|
||||||
if needs_domain and unit == "livekit.service":
|
[
|
||||||
if has_domain_issues:
|
{
|
||||||
domain_check_steps.append({
|
"port": str(p.get("port", "")),
|
||||||
"step": 4,
|
"protocol": str(p.get("protocol", "TCP")),
|
||||||
"label": "Router Setup Needed",
|
"description": p.get("description", ""),
|
||||||
"status": "skipped",
|
}
|
||||||
"detail": "Finish the domain steps first, then forward the Element Call ports in your router.",
|
for p in port_requirements
|
||||||
})
|
]
|
||||||
else:
|
if router_forward_only
|
||||||
# These checks are local-only (listening/firewall state on this computer),
|
else []
|
||||||
# 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."
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
# Compute composite health
|
# Compute composite health
|
||||||
sync_progress: float | None = None
|
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,
|
"domain_check_steps": domain_check_steps,
|
||||||
"port_requirements": port_requirements,
|
"port_requirements": port_requirements,
|
||||||
"port_statuses": port_statuses,
|
"port_statuses": port_statuses,
|
||||||
"extra_ports": extra_ports,
|
"router_ports": router_ports,
|
||||||
"external_ip": external_ip,
|
"external_ip": external_ip,
|
||||||
"internal_ip": internal_ip,
|
"internal_ip": internal_ip,
|
||||||
"feature": feature_entry,
|
"feature": feature_entry,
|
||||||
@@ -3362,6 +3370,11 @@ async def api_ports_health():
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
continue
|
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, [])
|
ports = SERVICE_PORT_REQUIREMENTS.get(unit, [])
|
||||||
if ports:
|
if ports:
|
||||||
enabled_port_requirements.append((entry.get("name", unit), unit, ports))
|
enabled_port_requirements.append((entry.get("name", unit), unit, ports))
|
||||||
|
|||||||
@@ -172,6 +172,58 @@ domain-field-actions {
|
|||||||
font-weight: 600;
|
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 ───────────────────── */
|
/* ── Wallet Connections unique-hostname warning ───────────────────── */
|
||||||
|
|
||||||
.domain-nwc-warning {
|
.domain-nwc-warning {
|
||||||
|
|||||||
@@ -279,24 +279,12 @@ function openPortRequirementsModal(featureName, ports, onContinue) {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
function renderPortRequirements(internalIp) {
|
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 =
|
$portReqBody.innerHTML =
|
||||||
'<p class="port-req-intro">For <strong>' + escHtml(featureName) + '</strong> to work for people outside your home network, ' +
|
renderPortForwardGuideHtml(ports, {
|
||||||
'forward each port below' + ipPart + " in your router's port-forwarding settings. " +
|
internalIp: internalIp,
|
||||||
'Set the internal and external port to the same number.</p>' +
|
serviceName: featureName,
|
||||||
'<table class="port-req-table">' +
|
}) +
|
||||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th></tr></thead>' +
|
'<p class="port-req-hint">💡 This list is always available again on the <strong>' + escHtml(featureName) + '</strong> tile.</p>' +
|
||||||
'<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>' +
|
|
||||||
'<div class="domain-field-actions">' +
|
'<div class="domain-field-actions">' +
|
||||||
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
||||||
continueBtn +
|
continueBtn +
|
||||||
@@ -429,39 +417,10 @@ function handleFeatureToggle(feat, newEnabled) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check which ports are actually closed before showing the modal
|
// Always show the full list of ports to forward. Port forwarding happens on
|
||||||
fetch("/api/ports/status", {
|
// the router, which this computer cannot inspect — a local check would only
|
||||||
method: "POST",
|
// hide ports the user still has to open.
|
||||||
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);
|
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feat.id === "bitcoin-core") {
|
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>');
|
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) {
|
function formatDuration(seconds) {
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
|||||||
@@ -514,85 +514,41 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
domainActionHtml +
|
domainActionHtml +
|
||||||
'</div>';
|
'</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 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">' +
|
html += '<div class="svc-detail-section">' +
|
||||||
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
|
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
|
||||||
'<div class="svc-detail-port-note">' + forwardNote + '</div>' +
|
renderPortForwardGuideHtml(data.router_ports, {
|
||||||
'<table class="svc-detail-port-table">' +
|
internalIp: trimmedInternalIp || null,
|
||||||
'<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
|
tableClass: "svc-detail-port-table",
|
||||||
'<tbody>' + extraRows + '</tbody>' +
|
introClass: "svc-detail-port-note",
|
||||||
'</table>' +
|
noteClass: "svc-detail-port-note",
|
||||||
'<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>' +
|
}) +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
} else if (data.port_statuses && data.port_statuses.length > 0) {
|
} else if (data.port_statuses && data.port_statuses.length > 0) {
|
||||||
// Non-domain services (SSH) keep local single-port checks.
|
// Non-domain services (e.g. SSH): show what to forward, plus a short
|
||||||
var portTableRows = "";
|
// note if the service isn't actually listening on this computer yet.
|
||||||
data.port_statuses.forEach(function(p) {
|
var localInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
|
||||||
var statusIcon, statusClass2;
|
var notListening = data.port_statuses.filter(function(p) {
|
||||||
if (p.status === "listening") {
|
return p.status === "closed";
|
||||||
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>';
|
|
||||||
});
|
});
|
||||||
|
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">' +
|
html += '<div class="svc-detail-section">' +
|
||||||
'<div class="svc-detail-section-title">Port Requirements</div>' +
|
'<div class="svc-detail-section-title">Ports to Forward in Your Router</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>' +
|
'<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>' +
|
||||||
'<table class="svc-detail-port-table">' +
|
renderPortForwardGuideHtml(data.port_statuses, {
|
||||||
'<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
|
internalIp: localInternalIp || null,
|
||||||
'<tbody>' + portTableRows + '</tbody>' +
|
tableClass: "svc-detail-port-table",
|
||||||
'</table>' +
|
introClass: "svc-detail-port-note",
|
||||||
|
noteClass: "svc-detail-port-note",
|
||||||
|
}) +
|
||||||
|
localNote +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -383,10 +383,12 @@ async function loadStep3() {
|
|||||||
? ' to this computer’s internal IP <strong>' + escHtml(internalIp) + '</strong>'
|
? ' to this computer’s internal IP <strong>' + escHtml(internalIp) + '</strong>'
|
||||||
: ' to this computer’s internal IP';
|
: ' to this computer’s internal IP';
|
||||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
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)'
|
+ '🔌 <strong>One router task:</strong> in your router’s <strong>port forwarding</strong> settings, forward '
|
||||||
+ routerIpPart + '. They are required for HTTPS and SSL certificates — without them your services cannot be reached from outside your home network. '
|
+ 'port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>'
|
||||||
+ 'Forward port <strong>22</strong> (TCP) too if you want remote SSH access. '
|
+ routerIpPart + '. Use the <strong>same number for the internal and external port</strong>. '
|
||||||
+ 'Element Call needs a few extra ports, but you’ll be shown those when you enable it.'
|
+ '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>';
|
+ '</div>';
|
||||||
|
|
||||||
relevantDomains.forEach(function(d) {
|
relevantDomains.forEach(function(d) {
|
||||||
|
|||||||
Reference in New Issue
Block a user