Merge pull request #21 from naturallaw777/copilot/add-port-requirements-notification
[WIP] Add network port requirements notification for installation
This commit is contained in:
@@ -83,6 +83,7 @@ FEATURE_REGISTRY = [
|
|||||||
"needs_ddns": False,
|
"needs_ddns": False,
|
||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": [],
|
"conflicts_with": [],
|
||||||
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "haven",
|
"id": "haven",
|
||||||
@@ -102,6 +103,8 @@ FEATURE_REGISTRY = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"conflicts_with": [],
|
"conflicts_with": [],
|
||||||
|
# Haven uses only 80/443, already covered by the main install alert
|
||||||
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "element-calling",
|
"id": "element-calling",
|
||||||
@@ -114,6 +117,15 @@ FEATURE_REGISTRY = [
|
|||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": [],
|
"conflicts_with": [],
|
||||||
"requires": ["matrix_domain"],
|
"requires": ["matrix_domain"],
|
||||||
|
"port_requirements": [
|
||||||
|
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
||||||
|
{"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"},
|
||||||
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
|
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"},
|
||||||
|
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
||||||
|
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
||||||
|
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mempool",
|
"id": "mempool",
|
||||||
@@ -125,6 +137,7 @@ FEATURE_REGISTRY = [
|
|||||||
"needs_ddns": False,
|
"needs_ddns": False,
|
||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": [],
|
"conflicts_with": [],
|
||||||
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bip110",
|
"id": "bip110",
|
||||||
@@ -136,6 +149,7 @@ FEATURE_REGISTRY = [
|
|||||||
"needs_ddns": False,
|
"needs_ddns": False,
|
||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": ["bitcoin-core"],
|
"conflicts_with": ["bitcoin-core"],
|
||||||
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bitcoin-core",
|
"id": "bitcoin-core",
|
||||||
@@ -147,6 +161,7 @@ FEATURE_REGISTRY = [
|
|||||||
"needs_ddns": False,
|
"needs_ddns": False,
|
||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": ["bip110"],
|
"conflicts_with": ["bip110"],
|
||||||
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -160,6 +175,37 @@ FEATURE_SERVICE_MAP = {
|
|||||||
"bitcoin-core": None,
|
"bitcoin-core": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Port requirements for service tiles (keyed by unit name or icon)
|
||||||
|
# Services using only 80/443 for domain access share the same base list.
|
||||||
|
_PORTS_WEB = [
|
||||||
|
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
||||||
|
{"port": "443", "protocol": "TCP", "description": "HTTPS"},
|
||||||
|
]
|
||||||
|
_PORTS_MATRIX_FEDERATION = _PORTS_WEB + [
|
||||||
|
{"port": "8448", "protocol": "TCP", "description": "Matrix server-to-server federation"},
|
||||||
|
]
|
||||||
|
_PORTS_ELEMENT_CALLING = _PORTS_WEB + [
|
||||||
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
|
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"},
|
||||||
|
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
||||||
|
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
||||||
|
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||||
|
]
|
||||||
|
|
||||||
|
SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
|
||||||
|
# Infrastructure
|
||||||
|
"caddy.service": _PORTS_WEB,
|
||||||
|
# Communication
|
||||||
|
"matrix-synapse.service": _PORTS_MATRIX_FEDERATION,
|
||||||
|
"livekit.service": _PORTS_ELEMENT_CALLING,
|
||||||
|
# Domain-based apps (80/443)
|
||||||
|
"btcpayserver.service": _PORTS_WEB,
|
||||||
|
"vaultwarden.service": _PORTS_WEB,
|
||||||
|
"phpfpm-nextcloud.service": _PORTS_WEB,
|
||||||
|
"phpfpm-wordpress.service": _PORTS_WEB,
|
||||||
|
"haven-relay.service": _PORTS_WEB,
|
||||||
|
}
|
||||||
|
|
||||||
# For features that share a unit, disambiguate by icon field
|
# For features that share a unit, disambiguate by icon field
|
||||||
FEATURE_ICON_MAP = {
|
FEATURE_ICON_MAP = {
|
||||||
"bip110": "bip110",
|
"bip110": "bip110",
|
||||||
@@ -689,6 +735,8 @@ async def api_services():
|
|||||||
creds = entry.get("credentials", [])
|
creds = entry.get("credentials", [])
|
||||||
has_credentials = len(creds) > 0
|
has_credentials = len(creds) > 0
|
||||||
|
|
||||||
|
port_requirements = SERVICE_PORT_REQUIREMENTS.get(unit, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": entry.get("name", ""),
|
"name": entry.get("name", ""),
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
@@ -698,6 +746,7 @@ async def api_services():
|
|||||||
"category": entry.get("category", "other"),
|
"category": entry.get("category", "other"),
|
||||||
"status": status,
|
"status": status,
|
||||||
"has_credentials": has_credentials,
|
"has_credentials": has_credentials,
|
||||||
|
"port_requirements": port_requirements,
|
||||||
}
|
}
|
||||||
|
|
||||||
results = await asyncio.gather(*[get_status(s) for s in services])
|
results = await asyncio.gather(*[get_status(s) for s in services])
|
||||||
@@ -910,6 +959,7 @@ async def api_features():
|
|||||||
"needs_ddns": feat.get("needs_ddns", False),
|
"needs_ddns": feat.get("needs_ddns", False),
|
||||||
"extra_fields": extra_fields,
|
"extra_fields": extra_fields,
|
||||||
"conflicts_with": feat.get("conflicts_with", []),
|
"conflicts_with": feat.get("conflicts_with", []),
|
||||||
|
"port_requirements": feat.get("port_requirements", []),
|
||||||
}
|
}
|
||||||
if "requires" in feat:
|
if "requires" in feat:
|
||||||
entry["requires"] = feat["requires"]
|
entry["requires"] = feat["requires"]
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ const $featureConfirmOk = document.getElementById("feature-confirm-ok-btn")
|
|||||||
const $featureConfirmCancel = document.getElementById("feature-confirm-cancel-btn");
|
const $featureConfirmCancel = document.getElementById("feature-confirm-cancel-btn");
|
||||||
const $featureConfirmClose = document.getElementById("feature-confirm-close-btn");
|
const $featureConfirmClose = document.getElementById("feature-confirm-close-btn");
|
||||||
|
|
||||||
|
// Port Requirements modal
|
||||||
|
const $portReqModal = document.getElementById("port-requirements-modal");
|
||||||
|
const $portReqBody = document.getElementById("port-req-body");
|
||||||
|
const $portReqClose = document.getElementById("port-req-close-btn");
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function tileId(svc) { return svc.unit + "::" + svc.name; }
|
function tileId(svc) { return svc.unit + "::" + svc.name; }
|
||||||
@@ -218,7 +223,16 @@ function buildTile(svc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var infoBtn = hasCreds ? '<button class="tile-info-btn" data-unit="' + escHtml(svc.unit) + '" title="Connection info">i</button>' : "";
|
var infoBtn = hasCreds ? '<button class="tile-info-btn" data-unit="' + escHtml(svc.unit) + '" title="Connection info">i</button>' : "";
|
||||||
tile.innerHTML = infoBtn + '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
|
||||||
|
// Port requirements badge
|
||||||
|
var ports = svc.port_requirements || [];
|
||||||
|
var portsHtml = "";
|
||||||
|
if (ports.length > 0) {
|
||||||
|
var portLabels = ports.map(function(p) { return escHtml(p.port) + ' (' + escHtml(p.protocol) + ')'; });
|
||||||
|
portsHtml = '<div class="tile-ports" title="Click to view required router ports"><span class="tile-ports-icon">🔌</span><span class="tile-ports-label">Ports: ' + portLabels.join(', ') + '</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.innerHTML = infoBtn + '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>' + portsHtml;
|
||||||
|
|
||||||
var infoBtnEl = tile.querySelector(".tile-info-btn");
|
var infoBtnEl = tile.querySelector(".tile-info-btn");
|
||||||
if (infoBtnEl) {
|
if (infoBtnEl) {
|
||||||
@@ -227,6 +241,16 @@ function buildTile(svc) {
|
|||||||
openCredsModal(svc.unit, svc.name);
|
openCredsModal(svc.unit, svc.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var portsEl = tile.querySelector(".tile-ports");
|
||||||
|
if (portsEl) {
|
||||||
|
portsEl.style.cursor = "pointer";
|
||||||
|
portsEl.addEventListener("click", function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
openPortRequirementsModal(svc.name, ports, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,6 +907,58 @@ function closeDomainSetupModal() {
|
|||||||
if ($domainSetupModal) $domainSetupModal.classList.remove("open");
|
if ($domainSetupModal) $domainSetupModal.classList.remove("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Port Requirements modal ───────────────────────────────────────
|
||||||
|
|
||||||
|
function openPortRequirementsModal(featureName, ports, onContinue) {
|
||||||
|
if (!$portReqModal || !$portReqBody) return;
|
||||||
|
|
||||||
|
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 continueBtn = onContinue
|
||||||
|
? '<button class="btn btn-primary" id="port-req-continue-btn">I Understand — Continue</button>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$portReqBody.innerHTML =
|
||||||
|
'<p class="port-req-intro">You have enabled <strong>' + escHtml(featureName) + '</strong>. ' +
|
||||||
|
'For it to work with clients outside your local network you must open the following ports ' +
|
||||||
|
'on your <strong>home router / WAN firewall</strong>:</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">ℹ Consult your router manual or search "<em>how to open ports on [router model]</em>" ' +
|
||||||
|
'for instructions. Features like Element Video Calling will not work for remote users until these ports are open.</p>' +
|
||||||
|
'<div class="domain-field-actions">' +
|
||||||
|
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
||||||
|
continueBtn +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
document.getElementById("port-req-dismiss-btn").addEventListener("click", function() {
|
||||||
|
closePortRequirementsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onContinue) {
|
||||||
|
document.getElementById("port-req-continue-btn").addEventListener("click", function() {
|
||||||
|
closePortRequirementsModal();
|
||||||
|
onContinue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$portReqModal.classList.add("open");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePortRequirementsModal() {
|
||||||
|
if ($portReqModal) $portReqModal.classList.remove("open");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($portReqClose) {
|
||||||
|
$portReqClose.addEventListener("click", closePortRequirementsModal);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Feature toggle logic ──────────────────────────────────────────
|
// ── Feature toggle logic ──────────────────────────────────────────
|
||||||
|
|
||||||
async function performFeatureToggle(featId, enabled, extra) {
|
async function performFeatureToggle(featId, enabled, extra) {
|
||||||
@@ -935,7 +1011,7 @@ function handleFeatureToggle(feat, newEnabled) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function proceedAfterConflictCheck() {
|
function proceedAfterPortCheck() {
|
||||||
// Check SSL email first
|
// Check SSL email first
|
||||||
if (!_featuresData || !_featuresData.ssl_email_configured) {
|
if (!_featuresData || !_featuresData.ssl_email_configured) {
|
||||||
if (feat.needs_domain) {
|
if (feat.needs_domain) {
|
||||||
@@ -967,6 +1043,16 @@ function handleFeatureToggle(feat, newEnabled) {
|
|||||||
performFeatureToggle(feat.id, true, {});
|
performFeatureToggle(feat.id, true, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function proceedAfterConflictCheck() {
|
||||||
|
// Show port requirements notification if the feature has extra port needs
|
||||||
|
var ports = feat.port_requirements || [];
|
||||||
|
if (ports.length > 0) {
|
||||||
|
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
|
||||||
|
} else {
|
||||||
|
proceedAfterPortCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (conflictNames.length > 0) {
|
if (conflictNames.length > 0) {
|
||||||
openFeatureConfirm(
|
openFeatureConfirm(
|
||||||
"This will disable " + conflictNames.join(", ") + ". Continue?",
|
"This will disable " + conflictNames.join(", ") + ". Continue?",
|
||||||
|
|||||||
@@ -1311,3 +1311,82 @@ button.btn-reboot:hover:not(:disabled) {
|
|||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Tile: Port Requirements badge ──────────────────────────────── */
|
||||||
|
|
||||||
|
.tile-ports {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-ports:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-ports-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-ports-label {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Port Requirements Modal ────────────────────────────────────── */
|
||||||
|
|
||||||
|
.port-req-intro {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-table thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-table tbody tr:nth-child(even) {
|
||||||
|
background-color: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-port {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-proto {
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-desc {
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-req-hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -129,6 +129,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Port Requirements Modal -->
|
||||||
|
<div class="modal-overlay" id="port-requirements-modal" role="dialog" aria-modal="true" aria-labelledby="port-req-title">
|
||||||
|
<div class="creds-dialog">
|
||||||
|
<div class="creds-header">
|
||||||
|
<span class="creds-title" id="port-req-title">🔌 Router / Firewall Port Requirements</span>
|
||||||
|
<button class="creds-close-btn" id="port-req-close-btn" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="creds-body" id="port-req-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Rebuild Modal -->
|
<!-- Rebuild Modal -->
|
||||||
<div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title">
|
<div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
105
iso/installer.py
105
iso/installer.py
@@ -341,7 +341,110 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if radio.get_active():
|
if radio.get_active():
|
||||||
self.role = radio.get_name()
|
self.role = radio.get_name()
|
||||||
break
|
break
|
||||||
self.push_disk_confirm()
|
self.push_port_requirements()
|
||||||
|
|
||||||
|
# ── Step 1b: Port Requirements Notice ─────────────────────────────────
|
||||||
|
|
||||||
|
def push_port_requirements(self):
|
||||||
|
"""Inform the user about required router/firewall ports before install."""
|
||||||
|
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
|
||||||
|
# Warning banner
|
||||||
|
banner = Adw.Banner()
|
||||||
|
banner.set_title(
|
||||||
|
"⚠ You must open these ports on your home router / WAN firewall"
|
||||||
|
)
|
||||||
|
banner.set_revealed(True)
|
||||||
|
banner.set_margin_top(16)
|
||||||
|
banner.set_margin_start(40)
|
||||||
|
banner.set_margin_end(40)
|
||||||
|
outer.append(banner)
|
||||||
|
|
||||||
|
intro = Gtk.Label()
|
||||||
|
intro.set_markup(
|
||||||
|
"<span foreground='#a6adc8'>"
|
||||||
|
"Many Sovran_SystemsOS features require specific ports to be forwarded "
|
||||||
|
"through your router for remote access to work correctly. "
|
||||||
|
"Services like Element Video/Audio Calling and Matrix Federation "
|
||||||
|
"<b>will not work for clients outside your LAN</b> unless these ports are open."
|
||||||
|
"</span>"
|
||||||
|
)
|
||||||
|
intro.set_wrap(True)
|
||||||
|
intro.set_justify(Gtk.Justification.FILL)
|
||||||
|
intro.set_margin_top(14)
|
||||||
|
intro.set_margin_start(40)
|
||||||
|
intro.set_margin_end(40)
|
||||||
|
outer.append(intro)
|
||||||
|
|
||||||
|
sw = Gtk.ScrolledWindow()
|
||||||
|
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
sw.set_vexpand(True)
|
||||||
|
sw.set_margin_start(40)
|
||||||
|
sw.set_margin_end(40)
|
||||||
|
sw.set_margin_top(12)
|
||||||
|
sw.set_margin_bottom(8)
|
||||||
|
|
||||||
|
ports_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||||
|
|
||||||
|
port_sections = [
|
||||||
|
(
|
||||||
|
"🌐 Web / HTTPS (all domain-based services)",
|
||||||
|
[("80", "TCP", "HTTP (redirects to HTTPS)"),
|
||||||
|
("443", "TCP", "HTTPS")],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"💬 Matrix Federation (Matrix-Synapse)",
|
||||||
|
[("8448", "TCP", "Server-to-server federation")],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"🎥 Element Video & Audio Calling (LiveKit / Element-call)",
|
||||||
|
[("7881", "TCP", "LiveKit WebRTC signalling"),
|
||||||
|
("7882-7894", "UDP", "LiveKit media streams"),
|
||||||
|
("5349", "TCP", "TURN over TLS"),
|
||||||
|
("3478", "UDP", "TURN (STUN / relay)"),
|
||||||
|
("30000-40000", "TCP/UDP", "TURN relay (WebRTC media)")],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"🖥 Remote SSH (optional — only if you want WAN SSH access)",
|
||||||
|
[("22", "TCP", "SSH")],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for section_title, rows in port_sections:
|
||||||
|
group = Adw.PreferencesGroup()
|
||||||
|
group.set_title(section_title)
|
||||||
|
|
||||||
|
for port, proto, desc in rows:
|
||||||
|
row = Adw.ActionRow()
|
||||||
|
row.set_title(f"Port {port} ({proto})")
|
||||||
|
row.set_subtitle(desc)
|
||||||
|
group.add(row)
|
||||||
|
|
||||||
|
ports_box.append(group)
|
||||||
|
|
||||||
|
note = Gtk.Label()
|
||||||
|
note.set_markup(
|
||||||
|
"<span foreground='#6c7086' size='small'>"
|
||||||
|
"ℹ Search \"<i>how to open ports on [your router model]</i>\" for step-by-step instructions. "
|
||||||
|
"Most home routers have a \"Port Forwarding\" section in their admin panel."
|
||||||
|
"</span>"
|
||||||
|
)
|
||||||
|
note.set_wrap(True)
|
||||||
|
note.set_justify(Gtk.Justification.FILL)
|
||||||
|
note.set_margin_top(8)
|
||||||
|
ports_box.append(note)
|
||||||
|
|
||||||
|
sw.set_child(ports_box)
|
||||||
|
outer.append(sw)
|
||||||
|
|
||||||
|
outer.append(self.nav_row(
|
||||||
|
back_label="← Back",
|
||||||
|
back_cb=lambda b: self.nav.pop(),
|
||||||
|
next_label="I Understand →",
|
||||||
|
next_cb=lambda b: self.push_disk_confirm(),
|
||||||
|
))
|
||||||
|
|
||||||
|
self.push_page("Network Port Requirements", outer, show_back=True)
|
||||||
|
|
||||||
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
|
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user