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:
naturallaw777
2026-07-28 22:34:39 +00:00
co-authored by arena-agent
parent 0e3cab78f8
commit 419680a847
6 changed files with 195 additions and 153 deletions
+9 -50
View File
@@ -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&rsquo;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") {