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
@@ -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&rsquo;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&rsquo;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>';
}