diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 6844370..da23ac3 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -83,6 +83,7 @@ FEATURE_REGISTRY = [ "needs_ddns": False, "extra_fields": [], "conflicts_with": [], + "port_requirements": [], }, { "id": "haven", @@ -102,6 +103,8 @@ FEATURE_REGISTRY = [ }, ], "conflicts_with": [], + # Haven uses only 80/443, already covered by the main install alert + "port_requirements": [], }, { "id": "element-calling", @@ -114,6 +117,15 @@ FEATURE_REGISTRY = [ "extra_fields": [], "conflicts_with": [], "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", @@ -125,6 +137,7 @@ FEATURE_REGISTRY = [ "needs_ddns": False, "extra_fields": [], "conflicts_with": [], + "port_requirements": [], }, { "id": "bip110", @@ -136,6 +149,7 @@ FEATURE_REGISTRY = [ "needs_ddns": False, "extra_fields": [], "conflicts_with": ["bitcoin-core"], + "port_requirements": [], }, { "id": "bitcoin-core", @@ -147,6 +161,7 @@ FEATURE_REGISTRY = [ "needs_ddns": False, "extra_fields": [], "conflicts_with": ["bip110"], + "port_requirements": [], }, ] @@ -160,6 +175,37 @@ FEATURE_SERVICE_MAP = { "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 FEATURE_ICON_MAP = { "bip110": "bip110", @@ -689,6 +735,8 @@ async def api_services(): creds = entry.get("credentials", []) has_credentials = len(creds) > 0 + port_requirements = SERVICE_PORT_REQUIREMENTS.get(unit, []) + return { "name": entry.get("name", ""), "unit": unit, @@ -698,6 +746,7 @@ async def api_services(): "category": entry.get("category", "other"), "status": status, "has_credentials": has_credentials, + "port_requirements": port_requirements, } 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), "extra_fields": extra_fields, "conflicts_with": feat.get("conflicts_with", []), + "port_requirements": feat.get("port_requirements", []), } if "requires" in feat: entry["requires"] = feat["requires"] diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js index 09e6f89..7308f57 100644 --- a/app/sovran_systemsos_web/static/app.js +++ b/app/sovran_systemsos_web/static/app.js @@ -113,6 +113,11 @@ const $featureConfirmOk = document.getElementById("feature-confirm-ok-btn") const $featureConfirmCancel = document.getElementById("feature-confirm-cancel-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 ─────────────────────────────────────────────────────── function tileId(svc) { return svc.unit + "::" + svc.name; } @@ -218,7 +223,16 @@ function buildTile(svc) { } var infoBtn = hasCreds ? '' : ""; - tile.innerHTML = infoBtn + '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + st + '
'; + + // 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 = '
🔌Ports: ' + portLabels.join(', ') + '
'; + } + + tile.innerHTML = infoBtn + '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + st + '
' + portsHtml; var infoBtnEl = tile.querySelector(".tile-info-btn"); if (infoBtnEl) { @@ -227,6 +241,16 @@ function buildTile(svc) { 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; } @@ -883,6 +907,58 @@ function closeDomainSetupModal() { 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 '' + escHtml(p.port) + '' + + '' + escHtml(p.protocol) + '' + + '' + escHtml(p.description) + ''; + }).join(""); + + var continueBtn = onContinue + ? '' + : ''; + + $portReqBody.innerHTML = + '

You have enabled ' + escHtml(featureName) + '. ' + + 'For it to work with clients outside your local network you must open the following ports ' + + 'on your home router / WAN firewall:

' + + '' + + '' + + '' + rows + '' + + '
Port(s)ProtocolPurpose
' + + '

ℹ Consult your router manual or search "how to open ports on [router model]" ' + + 'for instructions. Features like Element Video Calling will not work for remote users until these ports are open.

' + + '
' + + '' + + continueBtn + + '
'; + + 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 ────────────────────────────────────────── async function performFeatureToggle(featId, enabled, extra) { @@ -935,7 +1011,7 @@ function handleFeatureToggle(feat, newEnabled) { }); } - function proceedAfterConflictCheck() { + function proceedAfterPortCheck() { // Check SSL email first if (!_featuresData || !_featuresData.ssl_email_configured) { if (feat.needs_domain) { @@ -967,6 +1043,16 @@ function handleFeatureToggle(feat, newEnabled) { 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) { openFeatureConfirm( "This will disable " + conflictNames.join(", ") + ". Continue?", diff --git a/app/sovran_systemsos_web/static/style.css b/app/sovran_systemsos_web/static/style.css index a65ffa4..05f4c23 100644 --- a/app/sovran_systemsos_web/static/style.css +++ b/app/sovran_systemsos_web/static/style.css @@ -1311,3 +1311,82 @@ button.btn-reboot:hover:not(:disabled) { 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; +} diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index f0e9bd0..618528e 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -129,6 +129,17 @@ + + +