15 Commits
Author SHA1 Message Date
naturallaw777 8345c97664 updated nixpkgs and btc clients 2026-06-25 16:26:43 -05:00
Sovran SystemsandGitHub 1f61eb8c7e Merge pull request #317 from naturallaw777/copilot/update-hub-router-port-forwarding-ui
Clarify Hub router forwarding copy and surface internal IP in Element Call flows
2026-06-24 17:26:11 -05:00
Sovran SystemsandGitHub 625a307a8d Merge pull request #316 from naturallaw777/copilot/fix-legacy-ssh-key-handling
Repair legacy factory SSH keys when Hub passphrase changes
2026-06-24 17:25:57 -05:00
copilot-swe-agent[bot]andGitHub c2f3f048b9 fix: simplify internal IP copy handling 2026-06-24 22:19:14 +00:00
copilot-swe-agent[bot]andGitHub bd3dbcb057 fix: clarify router forwarding IP guidance 2026-06-24 22:17:13 +00:00
copilot-swe-agent[bot]andGitHub 7f975bc4f1 chore: use flock for ssh bootstrap repair 2026-06-24 22:16:44 +00:00
copilot-swe-agent[bot]andGitHub 31abf40722 chore: serialize ssh bootstrap key repairs 2026-06-24 22:16:05 +00:00
copilot-swe-agent[bot]andGitHub 439021f798 chore: harden ssh bootstrap script 2026-06-24 22:15:24 +00:00
copilot-swe-agent[bot]andGitHub 181465c376 chore: log legacy ssh key regeneration 2026-06-24 22:14:42 +00:00
copilot-swe-agent[bot]andGitHub 6ec28b1ad7 Initial plan 2026-06-24 22:14:35 +00:00
copilot-swe-agent[bot]andGitHub db1a88ab2e fix: repair legacy factory ssh key passphrases 2026-06-24 22:14:06 +00:00
copilot-swe-agent[bot]andGitHub aa148fe435 Initial plan 2026-06-24 22:12:39 +00:00
Sovran SystemsandGitHub f4590ff653 Merge pull request #315 from naturallaw777/copilot/update-sovran-systemsos-port-forwarding-ui
Align port-forwarding UX to local-readiness semantics across service detail, Step 4 checklist, and onboarding
2026-06-24 16:55:07 -05:00
copilot-swe-agent[bot]andGitHub 22402cb4fd Align router setup wording and local port statuses 2026-06-24 19:06:12 +00:00
copilot-swe-agent[bot]andGitHub 1868a58ee0 Initial plan 2026-06-24 19:03:36 +00:00
7 changed files with 295 additions and 53 deletions
+10 -6
View File
@@ -2986,18 +2986,22 @@ async def api_service_detail(unit: str, icon: str | None = None):
if has_domain_issues: if has_domain_issues:
domain_check_steps.append({ domain_check_steps.append({
"step": 4, "step": 4,
"label": "Additional Ports Required", "label": "Router Setup Needed",
"status": "skipped", "status": "skipped",
"detail": "Skipped until Steps 1-3 are complete", "detail": "Finish the domain steps first, then forward the Element Call ports in your router.",
}) })
else: else:
extra_open = all(p["status"] != "closed" for p in extra_ports) # These checks are local-only (listening/firewall state on this computer),
# 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({ domain_check_steps.append({
"step": 4, "step": 4,
"label": "Additional Ports Required", "label": "Router Setup Needed" if all_local_ready else "Sovran_SystemsOS Port Setup Needed",
"status": "ok" if extra_open else "error", "status": "warning" if all_local_ready else "error",
"detail": ( "detail": (
"Element-Call/LiveKit requires additional forwarded ports for WebRTC and TURN traffic." "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."
), ),
}) })
@@ -154,20 +154,36 @@ async function openServiceDetailModal(unit, name, icon) {
'</div>'; '</div>';
if (unit === "livekit.service" && data.extra_ports && data.extra_ports.length > 0) { if (unit === "livekit.service" && data.extra_ports && data.extra_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 routerIpHelp = internalIp
? "Use this IP address as the destination/internal IP when creating each router forwarding rule."
: "Use this computers internal IP as the destination/internal IP when creating each router forwarding rule.";
var routerNextStep = internalIp
? 'Next step: Log in to your router and create forwarding rules for the ports above. Set the destination/internal IP to <strong>' + internalIpHtml + '</strong>.'
: 'Next step: Log in to your router and create forwarding rules for the ports above. Use this computers internal IP as the destination/internal IP.';
var domainConfigured = !!(data.domain && String(data.domain).trim());
var extraRows = ""; var extraRows = "";
data.extra_ports.forEach(function(p) { data.extra_ports.forEach(function(p) {
var statusIcon, statusClass2; var statusIcon, statusClass2;
if (p.status === "listening") { if (!effectiveEnabled) {
statusIcon = "✅ Open"; 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"; statusClass2 = "port-status-listening";
} else if (p.status === "firewall_open") { } else if (p.status === "firewall_open") {
statusIcon = "🟡 Firewall open"; statusIcon = "✅ Ready";
statusClass2 = "port-status-open"; statusClass2 = "port-status-open";
} else if (p.status === "closed") { } else if (p.status === "closed") {
statusIcon = "❌ Closed"; statusIcon = "❌ Not ready yet";
statusClass2 = "port-status-closed"; statusClass2 = "port-status-closed";
} else { } else {
statusIcon = "— Unknown"; statusIcon = "— Could not check";
statusClass2 = "port-status-unknown"; statusClass2 = "port-status-unknown";
} }
extraRows += '<tr>' + extraRows += '<tr>' +
@@ -178,11 +194,16 @@ async function openServiceDetailModal(unit, name, icon) {
'</tr>'; '</tr>';
}); });
html += '<div class="svc-detail-section">' + html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Step 4: Additional Ports</div>' + '<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
'<div class="svc-detail-port-note">Forward these ports in your router to this Sovran_SystemsOS computer.</div>' +
'<div class="svc-detail-port-note"><strong>Router Forward-To IP:</strong> ' + internalIpHtml + '</div>' +
'<div class="svc-detail-port-note">' + routerIpHelp + '</div>' +
'<table class="svc-detail-port-table">' + '<table class="svc-detail-port-table">' +
'<thead><tr><th>Port</th><th>Protocol</th><th>Description</th><th>Status</th></tr></thead>' + '<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
'<tbody>' + extraRows + '</tbody>' + '<tbody>' + extraRows + '</tbody>' +
'</table>' + '</table>' +
'<div class="svc-detail-port-note">The Hub can check whether Sovran_SystemsOS is ready on this computer, but full public port verification requires an outside internet check.</div>' +
'<div class="svc-detail-port-note">' + routerNextStep + '</div>' +
'</div>'; '</div>';
} }
} else if (data.port_statuses && data.port_statuses.length > 0) { } else if (data.port_statuses && data.port_statuses.length > 0) {
@@ -191,16 +212,16 @@ async function openServiceDetailModal(unit, name, icon) {
data.port_statuses.forEach(function(p) { data.port_statuses.forEach(function(p) {
var statusIcon, statusClass2; var statusIcon, statusClass2;
if (p.status === "listening") { if (p.status === "listening") {
statusIcon = "✅ Open"; statusIcon = "✅ Ready";
statusClass2 = "port-status-listening"; statusClass2 = "port-status-listening";
} else if (p.status === "firewall_open") { } else if (p.status === "firewall_open") {
statusIcon = "🟡 Firewall open"; statusIcon = "✅ Ready";
statusClass2 = "port-status-open"; statusClass2 = "port-status-open";
} else if (p.status === "closed") { } else if (p.status === "closed") {
statusIcon = "🔴 Closed"; statusIcon = "❌ Not ready";
statusClass2 = "port-status-closed"; statusClass2 = "port-status-closed";
} else { } else {
statusIcon = "— Unknown"; statusIcon = "— Could not check";
statusClass2 = "port-status-unknown"; statusClass2 = "port-status-unknown";
} }
portTableRows += '<tr>' + portTableRows += '<tr>' +
@@ -211,9 +232,10 @@ async function openServiceDetailModal(unit, name, icon) {
'</tr>'; '</tr>';
}); });
html += '<div class="svc-detail-section">' + html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Port Status</div>' + '<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">' + '<table class="svc-detail-port-table">' +
'<thead><tr><th>Port</th><th>Protocol</th><th>Description</th><th>Status</th></tr></thead>' + '<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
'<tbody>' + portTableRows + '</tbody>' + '<tbody>' + portTableRows + '</tbody>' +
'</table>' + '</table>' +
'</div>'; '</div>';
+29 -17
View File
@@ -516,7 +516,7 @@ async function saveStep3() {
async function loadStep4() { async function loadStep4() {
var body = document.getElementById("step-4-body"); var body = document.getElementById("step-4-body");
if (!body) return; if (!body) return;
body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>'; body.innerHTML = '<p class="onboarding-loading">Loading router setup…</p>';
var networkData = null; var networkData = null;
@@ -527,51 +527,59 @@ async function loadStep4() {
return; return;
} }
var internalIp = (networkData && networkData.internal_ip) || "unknown"; var trimmedInternalIp = (networkData && networkData.internal_ip) ? String(networkData.internal_ip).trim() : "";
var internalIp = trimmedInternalIp || "";
var ip = escHtml(internalIp); var hasInternalIp = !!internalIp;
var ip = escHtml(internalIp || "Could not detect");
var routerIpHelp = hasInternalIp
? "Use this IP address as the destination/internal IP when creating each router forwarding rule."
: "Use this computers internal IP as the destination/internal IP when creating each router forwarding rule.";
var destinationInstruction = hasInternalIp
? 'Set the destination/internal IP to <strong>' + ip + '</strong>'
: 'Use this computers internal IP as the destination/internal IP';
var html = '<p class="onboarding-port-note" style="margin-bottom:14px;">' var html = '<p class="onboarding-port-note" style="margin-bottom:14px;">'
+ '⚠ <strong>Each port only needs to be forwarded once — all services share the same ports.</strong>' + '⚠ <strong>Each port only needs to be forwarded once — all services share the same ports.</strong>'
+ '</p>'; + '</p>';
html += '<div class="onboarding-port-ip">'; html += '<div class="onboarding-port-ip">';
html += ' <span class="onboarding-port-ip-label">Forward ports to this machine\'s internal IP:</span>'; html += ' <span class="onboarding-port-ip-label">Forward router traffic to this Sovran_SystemsOS computer:</span>';
html += ' <span class="port-req-internal-ip">' + ip + '</span>'; html += ' <span class="port-req-internal-ip">' + ip + '</span>';
html += '</div>'; html += '</div>';
html += '<div class="onboarding-port-note" style="margin:8px 0 16px;">' + routerIpHelp + '</div>';
// Required ports table // Required ports table
html += '<div class="onboarding-port-section" style="margin-bottom:20px;">'; html += '<div class="onboarding-port-section" style="margin-bottom:20px;">';
html += '<div class="onboarding-port-section-title" style="font-weight:700;margin-bottom:8px;">Required Ports — open these on your router:</div>'; html += '<div class="onboarding-port-section-title" style="font-weight:700;margin-bottom:8px;">Required Router Rules</div>';
html += '<table class="onboarding-port-table">'; html += '<table class="onboarding-port-table">';
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;to</th><th>Purpose</th></tr></thead>'; html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;To</th><th>Used For</th></tr></thead>';
html += '<tbody>'; html += '<tbody>';
html += '<tr><td class="port-req-port">80</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTP</td></tr>'; html += '<tr><td class="port-req-port">80</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTP / SSL setup</td></tr>';
html += '<tr><td class="port-req-port">443</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTPS</td></tr>'; html += '<tr><td class="port-req-port">443</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTPS</td></tr>';
html += '<tr><td class="port-req-port">22</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">SSH Remote Access</td></tr>'; html += '<tr><td class="port-req-port">22</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">Remote SSH access</td></tr>';
html += '</tbody></table>'; html += '</tbody></table>';
html += '</div>'; html += '</div>';
// Optional ports table // Optional ports table
html += '<div class="onboarding-port-section" style="margin-bottom:20px;">'; html += '<div class="onboarding-port-section" style="margin-bottom:20px;">';
html += '<div class="onboarding-port-section-title" style="font-weight:700;margin-bottom:4px;">Optional — Only needed if you enable Element Calling:</div>'; html += '<div class="onboarding-port-section-title" style="font-weight:700;margin-bottom:4px;">Element Call Router Rules</div>';
html += '<div style="font-size:0.88em;margin-bottom:8px;color:var(--color-text-muted,#888);">These 5 additional port openings are required on top of the 3 required ports above.</div>'; html += '<div style="font-size:0.88em;margin-bottom:8px;color:var(--color-text-muted,#888);">Only add these if you enable Element Call. These ports help video and audio calls connect reliably.</div>';
html += '<table class="onboarding-port-table">'; html += '<table class="onboarding-port-table">';
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;to</th><th>Purpose</th></tr></thead>'; html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;To</th><th>Used For</th></tr></thead>';
html += '<tbody>'; html += '<tbody>';
html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>'; html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>';
html += '<tr><td class="port-req-port">7882</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media (UDP mux)</td></tr>'; html += '<tr><td class="port-req-port">7882</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media (UDP mux)</td></tr>';
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>'; html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>'; html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
html += '<tr><td class="port-req-port">3000040000</td><td class="port-req-proto">TCP &amp; UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>'; html += '<tr><td class="port-req-port">30000-40000</td><td class="port-req-proto">TCP &amp; UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
html += '</tbody></table>'; html += '</tbody></table>';
html += '<div style="font-size:0.85em;margin-top:6px;color:var(--color-text-muted,#888);"> The <strong>3000040000</strong> range is a single forwarding rule — just set its protocol to <strong>both TCP and UDP</strong> (often shown as "Both" or "TCP/UDP" on your router).</div>'; html += '<div style="font-size:0.85em;margin-top:6px;color:var(--color-text-muted,#888);"> The <strong>30000-40000</strong> range is a single forwarding rule — just set its protocol to <strong>both TCP and UDP</strong> (often shown as "Both" or "TCP/UDP" on your router).</div>';
html += '</div>'; html += '</div>';
// Totals // Totals
html += '<div class="onboarding-port-totals">'; html += '<div class="onboarding-port-totals">';
html += '<strong>Total port openings: 3</strong> (without Element Calling)<br>'; html += '<strong>Total port openings: 3</strong> (without Element Call)<br>';
html += '<strong>Total port openings: 8</strong> (with Element Calling — 3 required + 5 optional)'; html += '<strong>Total port openings: 8</strong> (with Element Call — 3 required + 5 optional)';
html += '</div>'; html += '</div>';
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">' html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
@@ -586,12 +594,16 @@ async function loadStep4() {
+ '<li>Open your router\'s admin panel — usually <code>http://192.168.1.1</code> or <code>http://192.168.0.1</code></li>' + '<li>Open your router\'s admin panel — usually <code>http://192.168.1.1</code> or <code>http://192.168.0.1</code></li>'
+ '<li>Look for <strong>"Port Forwarding"</strong>, <strong>"NAT"</strong>, or <strong>"Virtual Server"</strong> in the settings</li>' + '<li>Look for <strong>"Port Forwarding"</strong>, <strong>"NAT"</strong>, or <strong>"Virtual Server"</strong> in the settings</li>'
+ '<li>Create a new rule for each port listed above</li>' + '<li>Create a new rule for each port listed above</li>'
+ '<li>Set the destination/internal IP to <strong>' + ip + '</strong></li>' + '<li>' + destinationInstruction + '</li>'
+ '<li>Set both internal and external port to the same number</li>' + '<li>Set both internal and external port to the same number</li>'
+ '<li>Save and apply changes</li>' + '<li>Save and apply changes</li>'
+ '</ol>' + '</ol>'
+ '</details>'; + '</details>';
html += '<div class="onboarding-port-note" style="margin-top:12px;">'
+ '<strong>Important:</strong> The Hub can show which ports Sovran_SystemsOS needs, but it cannot fully confirm router forwarding from inside your home network. Full public port verification requires an outside internet check.'
+ '</div>';
body.innerHTML = html; body.innerHTML = html;
} }
@@ -148,14 +148,14 @@
<div class="onboarding-panel" id="step-4" style="display:none"> <div class="onboarding-panel" id="step-4" style="display:none">
<div class="onboarding-step-header"> <div class="onboarding-step-header">
<span class="onboarding-step-icon">🔌</span> <span class="onboarding-step-icon">🔌</span>
<h2 class="onboarding-step-title">Port Forwarding Check</h2> <h2 class="onboarding-step-title">Router Setup</h2>
<p class="onboarding-step-desc"> <p class="onboarding-step-desc">
Forward these ports on your router to this machine. Each port only needs to be opened once — they are shared across all your services. Forward these ports in your router to this Sovran_SystemsOS computer. These rules let people reach your services from outside your home network.
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong> <strong>Ports 80 and 443 are required for HTTPS and SSL certificates.</strong>
</p> </p>
</div> </div>
<div class="onboarding-card" id="step-4-body"> <div class="onboarding-card" id="step-4-body">
<p class="onboarding-loading">Checking ports</p> <p class="onboarding-loading">Loading router setup</p>
</div> </div>
<div class="onboarding-footer"> <div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button> <button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
@@ -0,0 +1,171 @@
import unittest
from pathlib import Path
from unittest.mock import mock_open, patch
import sys
import types
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
def _install_web_stubs():
if "fastapi" in sys.modules:
return
class _HTTPException(Exception):
def __init__(self, status_code=None, detail=None):
super().__init__(detail)
self.status_code = status_code
self.detail = detail
class _FastAPI:
def __init__(self, *args, **kwargs):
pass
def mount(self, *args, **kwargs):
return None
def add_middleware(self, *args, **kwargs):
return None
def __getattr__(self, _name):
def _decorator_factory(*args, **kwargs):
def _decorator(func):
return func
return _decorator
return _decorator_factory
class _BaseModel:
pass
class _StaticFiles:
def __init__(self, *args, **kwargs):
pass
class _Jinja2Templates:
def __init__(self, *args, **kwargs):
pass
class _BaseHTTPMiddleware:
pass
fastapi_module = types.ModuleType("fastapi")
fastapi_module.FastAPI = _FastAPI
fastapi_module.HTTPException = _HTTPException
sys.modules["fastapi"] = fastapi_module
responses_module = types.ModuleType("fastapi.responses")
responses_module.HTMLResponse = object
responses_module.JSONResponse = object
responses_module.RedirectResponse = object
sys.modules["fastapi.responses"] = responses_module
staticfiles_module = types.ModuleType("fastapi.staticfiles")
staticfiles_module.StaticFiles = _StaticFiles
sys.modules["fastapi.staticfiles"] = staticfiles_module
templating_module = types.ModuleType("fastapi.templating")
templating_module.Jinja2Templates = _Jinja2Templates
sys.modules["fastapi.templating"] = templating_module
requests_module = types.ModuleType("fastapi.requests")
requests_module.Request = object
sys.modules["fastapi.requests"] = requests_module
pydantic_module = types.ModuleType("pydantic")
pydantic_module.BaseModel = _BaseModel
sys.modules["pydantic"] = pydantic_module
starlette_base_module = types.ModuleType("starlette.middleware.base")
starlette_base_module.BaseHTTPMiddleware = _BaseHTTPMiddleware
sys.modules["starlette.middleware.base"] = starlette_base_module
starlette_middleware_module = types.ModuleType("starlette.middleware")
starlette_middleware_module.base = starlette_base_module
sys.modules["starlette.middleware"] = starlette_middleware_module
starlette_module = types.ModuleType("starlette")
starlette_module.middleware = starlette_middleware_module
sys.modules["starlette"] = starlette_module
_install_web_stubs()
from sovran_systemsos_web import server
class ServiceDetailRouterWordingTests(unittest.IsolatedAsyncioTestCase):
async def test_livekit_service_detail_includes_internal_ip(self):
service_cfg = {
"services": [
{"unit": "livekit.service", "icon": "element-call", "enabled": True, "type": "system"}
]
}
domain_eval = {
"domain_status": {"status": "ok"},
"domain_reachable": {"reachable": True},
"domain_check_steps": [],
"has_issues": False,
}
with (
patch.object(server, "load_config", return_value=service_cfg),
patch.object(server, "_read_hub_overrides", return_value=({}, None, None)),
patch.object(server.sysctl, "is_active", return_value="active"),
patch.dict(server.SERVICE_DOMAIN_MAP, {"livekit.service": "element-call"}, clear=False),
patch.dict(
server.SERVICE_PORT_REQUIREMENTS,
{"livekit.service": [{"port": "7881", "protocol": "TCP", "description": "LiveKit"}]},
clear=False,
),
patch("builtins.open", mock_open(read_data="call.example.com\n")),
patch.object(server, "_evaluate_domain_checklist", return_value=domain_eval),
patch.object(server, "_get_internal_ip", return_value="192.168.1.44"),
patch.object(server, "_save_internal_ip"),
patch.object(server, "_get_listening_ports", return_value={"tcp": {7881}, "udp": set()}),
patch.object(server, "_get_firewall_allowed_ports", return_value={"tcp": set(), "udp": set()}),
):
result = await server.api_service_detail("livekit.service")
self.assertEqual(result["internal_ip"], "192.168.1.44")
self.assertEqual(result["extra_ports"][0]["status"], "listening")
self.assertEqual(result["domain_check_steps"][-1]["label"], "Router Setup Needed")
async def test_livekit_router_step_uses_not_ready_yet_wording(self):
service_cfg = {
"services": [
{"unit": "livekit.service", "icon": "element-call", "enabled": True, "type": "system"}
]
}
domain_eval = {
"domain_status": {"status": "ok"},
"domain_reachable": {"reachable": True},
"domain_check_steps": [],
"has_issues": False,
}
with (
patch.object(server, "load_config", return_value=service_cfg),
patch.object(server, "_read_hub_overrides", return_value=({}, None, None)),
patch.object(server.sysctl, "is_active", return_value="active"),
patch.dict(server.SERVICE_DOMAIN_MAP, {"livekit.service": "element-call"}, clear=False),
patch.dict(
server.SERVICE_PORT_REQUIREMENTS,
{"livekit.service": [{"port": "7881", "protocol": "TCP", "description": "LiveKit"}]},
clear=False,
),
patch("builtins.open", mock_open(read_data="call.example.com\n")),
patch.object(server, "_evaluate_domain_checklist", return_value=domain_eval),
patch.object(server, "_get_internal_ip", return_value="192.168.1.44"),
patch.object(server, "_save_internal_ip"),
patch.object(server, "_get_listening_ports", return_value={"tcp": set(), "udp": set()}),
patch.object(server, "_get_firewall_allowed_ports", return_value={"tcp": set(), "udp": set()}),
):
result = await server.api_service_detail("livekit.service")
self.assertEqual(result["extra_ports"][0]["status"], "closed")
self.assertIn("Not ready yet", result["domain_check_steps"][-1]["detail"])
if __name__ == "__main__":
unittest.main()
Generated
+9 -9
View File
@@ -5,11 +5,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1781789880, "lastModified": 1781968640,
"narHash": "sha256-HU/J4pFFkC2XXsYO8B3QFneTV2NWEXFuNi4QmV/4ZA8=", "narHash": "sha256-ySgKs6YZRJRsBVoMnmOBGQiNRaX7M9FxMnHA29kovqA=",
"owner": "emmanuelrosa", "owner": "emmanuelrosa",
"repo": "btc-clients-nix", "repo": "btc-clients-nix",
"rev": "10f0300231075e6c7417030fbcbf9f056d0a7c21", "rev": "3959faadf6c9d3148e1d819ae85b7f32da5b005b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -139,11 +139,11 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1781216227, "lastModified": 1782233679,
"narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=", "narHash": "sha256-QyuGP5+QOtmXpy4i2X4DhBVBaySBdDKQEhqKcphcp34=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0", "rev": "667d5cf1c59585031d743c78b394b0a647537c35",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -224,11 +224,11 @@
"systems": "systems_2" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1781713417, "lastModified": 1782254890,
"narHash": "sha256-Kaj44jTNmnaFhKrcADx8nXmUYPa7l2HYfb7m6lEPy7Q=", "narHash": "sha256-kjsEECqhpPnJWqhooXp6tWh2qGQftCPAo2G1GvZtKdw=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixvim", "repo": "nixvim",
"rev": "caee4e5d4161778815f522d9ea1c9e3dc42462b7", "rev": "dbf9550dba8448b03e11d58e5695d6c44a464554",
"type": "github" "type": "github"
}, },
"original": { "original": {
+37 -4
View File
@@ -31,7 +31,7 @@ lib.mkIf userExists {
}; };
systemd.services.factory-ssh-keygen = { systemd.services.factory-ssh-keygen = {
description = "Generate factory SSH key for ${userName} if missing"; description = "Generate or repair factory SSH key for ${userName}";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "ssh-passphrase-setup.service" ]; after = [ "ssh-passphrase-setup.service" ];
requires = [ "ssh-passphrase-setup.service" ]; requires = [ "ssh-passphrase-setup.service" ];
@@ -39,14 +39,47 @@ lib.mkIf userExists {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
}; };
path = [ pkgs.openssh pkgs.coreutils ]; path = [ pkgs.openssh pkgs.coreutils pkgs.util-linux ];
script = '' script = ''
if [ ! -f "${keyPath}" ]; then set -eu
PASSPHRASE=$(cat /var/lib/secrets/ssh-passphrase)
PASSPHRASE=$(cat /var/lib/secrets/ssh-passphrase)
lock_file="${keyPath}.lock"
exec 9>"$lock_file"
if ! flock -n 9; then
echo "Factory SSH key setup is already running." >&2
exit 1
fi
generate_factory_key() {
ssh-keygen -q -N "$PASSPHRASE" -t ed25519 -f "${keyPath}" ssh-keygen -q -N "$PASSPHRASE" -t ed25519 -f "${keyPath}"
chown ${userName}:users "${keyPath}" "${keyPath}.pub" chown ${userName}:users "${keyPath}" "${keyPath}.pub"
chmod 600 "${keyPath}" chmod 600 "${keyPath}"
chmod 644 "${keyPath}.pub" chmod 644 "${keyPath}.pub"
}
if [ ! -f "${keyPath}" ]; then
generate_factory_key
elif ! ssh-keygen -y -P "$PASSPHRASE" -f "${keyPath}" >/dev/null 2>&1; then
backup_suffix="$(date -u +%Y%m%d_%H%M%S)-$$"
backup_path="${keyPath}.bak-$backup_suffix"
backup_index=0
while [ -e "$backup_path" ] || [ -e "$backup_path.pub" ]; do
backup_index=$((backup_index + 1))
backup_path="${keyPath}.bak-$backup_suffix-$backup_index"
done
echo "Existing factory SSH key does not match current passphrase; backing it up to $backup_path and generating a replacement."
mv "${keyPath}" "$backup_path"
if [ -f "${keyPath}.pub" ]; then
mv "${keyPath}.pub" "$backup_path.pub"
fi
generate_factory_key
fi fi
''; '';
}; };