Compare commits
56
Commits
v1.0.0
..
ff70668f38
@@ -10,6 +10,14 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="assets/desktop-screenshot.png" alt="Sovran_SystemsOS desktop showing application dock and PRIVACY. SOVEREIGNTY. BITCOIN. tagline" width="800" />
|
||||||
|
|
||||||
|
*The Sovran_SystemsOS desktop — "Privacy. Sovereignty. Bitcoin."*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|||||||
@@ -281,9 +281,6 @@ FEATURE_SERVICE_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Port requirements for service tiles (keyed by unit name or icon)
|
# Port requirements for service tiles (keyed by unit name or icon)
|
||||||
_PORTS_MATRIX_FEDERATION = [
|
|
||||||
{"port": "8448", "protocol": "TCP", "description": "Matrix server-to-server federation"},
|
|
||||||
]
|
|
||||||
_PORTS_ELEMENT_CALLING = [
|
_PORTS_ELEMENT_CALLING = [
|
||||||
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
|
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
|
||||||
@@ -296,7 +293,7 @@ SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
|
|||||||
# Infrastructure
|
# Infrastructure
|
||||||
"caddy.service": [],
|
"caddy.service": [],
|
||||||
# Communication
|
# Communication
|
||||||
"matrix-synapse.service": _PORTS_MATRIX_FEDERATION,
|
"matrix-synapse.service": [],
|
||||||
"livekit.service": _PORTS_ELEMENT_CALLING,
|
"livekit.service": _PORTS_ELEMENT_CALLING,
|
||||||
# Domain-based apps (80/443 handled by end-to-end domain reachability checks)
|
# Domain-based apps (80/443 handled by end-to-end domain reachability checks)
|
||||||
"btcpayserver.service": [],
|
"btcpayserver.service": [],
|
||||||
@@ -1222,7 +1219,7 @@ def _generate_qr_base64(data: str) -> str | None:
|
|||||||
# ── Update helpers (file-based, no systemctl) ────────────────────
|
# ── Update helpers (file-based, no systemctl) ────────────────────
|
||||||
|
|
||||||
def _read_update_status() -> str:
|
def _read_update_status() -> str:
|
||||||
"""Read the status file. Returns RUNNING, SUCCESS, FAILED, or IDLE."""
|
"""Read the status file. Returns RUNNING, SUCCESS, REBOOT_REQUIRED, FAILED, or IDLE."""
|
||||||
try:
|
try:
|
||||||
with open(UPDATE_STATUS, "r") as f:
|
with open(UPDATE_STATUS, "r") as f:
|
||||||
return f.read().strip()
|
return f.read().strip()
|
||||||
@@ -1953,10 +1950,13 @@ def _verify_support_removed() -> bool:
|
|||||||
|
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request):
|
||||||
return templates.TemplateResponse("login.html", {
|
return templates.TemplateResponse(
|
||||||
"request": request,
|
request=request,
|
||||||
|
name="login.html",
|
||||||
|
context={
|
||||||
"asset_version": ASSET_VERSION,
|
"asset_version": ASSET_VERSION,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/auto-login")
|
@app.get("/auto-login")
|
||||||
@@ -2021,20 +2021,26 @@ async def api_logout(request: Request):
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse(
|
||||||
"request": request,
|
request=request,
|
||||||
|
name="index.html",
|
||||||
|
context={
|
||||||
"asset_version": ASSET_VERSION,
|
"asset_version": ASSET_VERSION,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/onboarding", response_class=HTMLResponse)
|
@app.get("/onboarding", response_class=HTMLResponse)
|
||||||
async def onboarding(request: Request):
|
async def onboarding(request: Request):
|
||||||
_ensure_onboarding_reopened_for_migration()
|
_ensure_onboarding_reopened_for_migration()
|
||||||
return templates.TemplateResponse("onboarding.html", {
|
return templates.TemplateResponse(
|
||||||
"request": request,
|
request=request,
|
||||||
|
name="onboarding.html",
|
||||||
|
context={
|
||||||
"asset_version": ASSET_VERSION,
|
"asset_version": ASSET_VERSION,
|
||||||
"onboarding_js_hash": _ONBOARDING_JS_HASH,
|
"onboarding_js_hash": _ONBOARDING_JS_HASH,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/status")
|
@app.get("/api/onboarding/status")
|
||||||
@@ -2983,36 +2989,28 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
|||||||
"status": ps,
|
"status": ps,
|
||||||
"description": p.get("description", ""),
|
"description": p.get("description", ""),
|
||||||
})
|
})
|
||||||
extra_ports = port_statuses if unit in ("matrix-synapse.service", "livekit.service") else []
|
extra_ports = port_statuses if unit == "livekit.service" else []
|
||||||
|
|
||||||
if needs_domain and unit in ("matrix-synapse.service", "livekit.service"):
|
if needs_domain and unit == "livekit.service":
|
||||||
if has_domain_issues:
|
if has_domain_issues:
|
||||||
domain_check_steps.append({
|
domain_check_steps.append({
|
||||||
"step": 4,
|
"step": 4,
|
||||||
"label": "Federation Port" if unit == "matrix-synapse.service" else "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.",
|
||||||
})
|
|
||||||
elif unit == "matrix-synapse.service":
|
|
||||||
if extra_ports:
|
|
||||||
matrix_open = extra_ports[0]["status"] != "closed"
|
|
||||||
domain_check_steps.append({
|
|
||||||
"step": 4,
|
|
||||||
"label": "Federation Port",
|
|
||||||
"status": "ok" if matrix_open else "error",
|
|
||||||
"detail": (
|
|
||||||
f"Matrix federation port 8448 (TCP) is {'open' if matrix_open else 'closed'}.\n"
|
|
||||||
f"Matrix federation requires port 8448 (TCP) forwarded to {internal_ip}"
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
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."
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -4757,17 +4755,21 @@ def _recover_stale_status(status_file: str, log_file: str, unit_name: str) -> bo
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
new_status = "SUCCESS" if unit_result == "success" else "FAILED"
|
if unit_result == "success":
|
||||||
|
new_status = "REBOOT_REQUIRED" if unit_name == UPDATE_UNIT else "SUCCESS"
|
||||||
|
else:
|
||||||
|
new_status = "FAILED"
|
||||||
try:
|
try:
|
||||||
with open(status_file, "w") as f:
|
with open(status_file, "w") as f:
|
||||||
f.write(new_status)
|
f.write(new_status)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
msg = (
|
if new_status == "REBOOT_REQUIRED":
|
||||||
"\n[Update completed successfully while the server was restarting.]\n"
|
msg = "\n[Update staged successfully while the server was restarting. Reboot required.]\n"
|
||||||
if new_status == "SUCCESS"
|
elif new_status == "SUCCESS":
|
||||||
else "\n[Update encountered an error. See log above for details.]\n"
|
msg = "\n[Update completed successfully while the server was restarting.]\n"
|
||||||
)
|
else:
|
||||||
|
msg = "\n[Update encountered an error. See log above for details.]\n"
|
||||||
try:
|
try:
|
||||||
with open(log_file, "a") as f:
|
with open(log_file, "a") as f:
|
||||||
f.write(msg)
|
f.write(msg)
|
||||||
|
|||||||
@@ -73,22 +73,47 @@ function openDomainSetupModal(feat, onSaved) {
|
|||||||
npubField = '<div class="domain-field-group"><label class="domain-field-label" for="domain-npub-input">Nostr Public Key (npub1...):</label><input class="domain-field-input" type="text" id="domain-npub-input" placeholder="npub1..." value="' + escHtml(currentNpub) + '" /></div>';
|
npubField = '<div class="domain-field-group"><label class="domain-field-label" for="domain-npub-input">Nostr Public Key (npub1...):</label><input class="domain-field-input" type="text" id="domain-npub-input" placeholder="npub1..." value="' + escHtml(currentNpub) + '" /></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var externalIp = _cachedExternalIp || "your external IP";
|
var introHtml;
|
||||||
|
if (_currentRole === "node") {
|
||||||
|
introHtml =
|
||||||
|
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la.</p>' +
|
||||||
|
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||||
|
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">njal.la</a>.</li>' +
|
||||||
|
'<li>Set up a domain for it — either a free subdomain or a separate domain. Pick one option:</li>' +
|
||||||
|
'</ol>';
|
||||||
|
} else {
|
||||||
|
introHtml =
|
||||||
|
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la. ' +
|
||||||
|
'In your Njal.la account, set up a domain for it — either a free subdomain or a separate domain. Pick one option:</p>';
|
||||||
|
}
|
||||||
|
|
||||||
$domainSetupBody.innerHTML =
|
$domainSetupBody.innerHTML =
|
||||||
'<div class="domain-setup-intro">' +
|
'<div class="domain-setup-intro">' +
|
||||||
'<p><strong>Before continuing:</strong></p>' +
|
introHtml +
|
||||||
'<ol>' +
|
'<details style="margin-top:10px;">' +
|
||||||
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">https://njal.la</a></li>' +
|
'<summary style="cursor:pointer;font-weight:600;">Option A — Free subdomain (recommended)</summary>' +
|
||||||
'<li>Purchase a new domain on Njal.la, or create a subdomain from a domain you already own. Tip: Subdomains are free to create — you only need to purchase one domain, and you can add as many subdomains as you need at no extra cost.</li>' +
|
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||||
'<li>In the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine\'s external IP address:<br>' +
|
'<li>In Njal.la, open a domain you own and click "Add record".</li>' +
|
||||||
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
'<li>Set record type to <strong>Dynamic</strong>.</li>' +
|
||||||
'<li>Njal.la will give you a curl command like:<br>' +
|
'<li>In the <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>' +
|
||||||
'<code style="font-size:0.8em;">curl "https://njal.la/update/?h=sub.domain.com&k=abc123&auto"</code></li>' +
|
'(Example only, your choice — for "call.yourdomain.com" you'd type just: <code>call</code>)<br>' +
|
||||||
'<li>Enter the subdomain and paste that curl command below</li>' +
|
'⚠ Do NOT type the full domain here — Njal.la adds it automatically.</li>' +
|
||||||
|
'<li>A Dynamic record has NO IP field — the IP auto-fills after the rebuild/reboot.</li>' +
|
||||||
|
'<li>Copy the curl command Njal.la gives you, e.g.:<br>' +
|
||||||
|
'<code style="font-size:0.8em;">curl "https://njal.la/update/?h=call.yourdomain.com&k=abc123&auto"</code></li>' +
|
||||||
'</ol>' +
|
'</ol>' +
|
||||||
|
'</details>' +
|
||||||
|
'<details style="margin-top:6px;">' +
|
||||||
|
'<summary style="cursor:pointer;font-weight:600;">Option B — Separate / new domain</summary>' +
|
||||||
|
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||||
|
'<li>In Njal.la, buy the domain you want.</li>' +
|
||||||
|
'<li>Add a Dynamic record as in Option A. If this domain is dedicated to the service, leave the Name field blank or use <code>@</code>.</li>' +
|
||||||
|
'<li>Copy the curl command Njal.la gives you.</li>' +
|
||||||
|
'</ol>' +
|
||||||
|
'</details>' +
|
||||||
|
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. call.yourdomain.com) or a separate domain (e.g. call.com) — and paste its curl command.</p>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Subdomain (e.g. myservice.example.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" /></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. call.yourdomain.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" /></div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=myservice.example.com&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=myservice.example.com&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
||||||
npubField +
|
npubField +
|
||||||
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save & Enable</button></div>';
|
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save & Enable</button></div>';
|
||||||
@@ -103,7 +128,7 @@ function openDomainSetupModal(feat, onSaved) {
|
|||||||
ddnsUrl = ddnsUrl.trim();
|
ddnsUrl = ddnsUrl.trim();
|
||||||
npub = npub.trim();
|
npub = npub.trim();
|
||||||
|
|
||||||
if (!subdomain) { alert("Please enter a subdomain."); return; }
|
if (!subdomain) { alert("Please enter a domain."); return; }
|
||||||
if (feat.id === "haven" && !npub) { alert("Please enter your Nostr public key."); return; }
|
if (feat.id === "haven" && !npub) { alert("Please enter your Nostr public key."); return; }
|
||||||
|
|
||||||
var saveBtn = document.getElementById("domain-setup-save-btn");
|
var saveBtn = document.getElementById("domain-setup-save-btn");
|
||||||
@@ -159,14 +184,14 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
|||||||
'<p><strong>Troubleshooting steps:</strong></p>' +
|
'<p><strong>Troubleshooting steps:</strong></p>' +
|
||||||
'<ol>' +
|
'<ol>' +
|
||||||
'<li>Log into your Njal.la dashboard at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">https://njal.la</a></li>' +
|
'<li>Log into your Njal.la dashboard at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">https://njal.la</a></li>' +
|
||||||
'<li>Find the DNS record for <strong>' + escHtml(currentDomain || "your domain") + '</strong></li>' +
|
'<li>Find the DNS record for <strong>' + escHtml(currentDomain || "your domain") + '</strong>. In Njal.la\'s Name field, note that only the host part is stored (the word before the domain) — not the full domain.</li>' +
|
||||||
'<li>Verify it has a <strong>Dynamic</strong> record pointing to your current external IP:<br>' +
|
'<li>Verify it has a <strong>Dynamic</strong> record pointing to your current external IP:<br>' +
|
||||||
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
||||||
'<li>If the IP is wrong or the record is missing, update it</li>' +
|
'<li>If the IP is wrong or the record is missing, update it</li>' +
|
||||||
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
|
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
|
||||||
'</ol>' +
|
'</ol>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Subdomain (e.g. myservice.example.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" value="' + escHtml(currentDomain) + '" /></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. call.yourdomain.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" value="' + escHtml(currentDomain) + '" /></div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=myservice.example.com&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=myservice.example.com&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
||||||
npubField +
|
npubField +
|
||||||
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save & Update</button></div>';
|
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save & Update</button></div>';
|
||||||
|
|||||||
@@ -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 computer’s 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 computer’s 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>';
|
||||||
|
|||||||
@@ -333,8 +333,6 @@ async function loadStep3() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var externalIp = (networkData && networkData.external_ip) || "Unknown (could not retrieve)";
|
|
||||||
|
|
||||||
// Build set of enabled service units
|
// Build set of enabled service units
|
||||||
var enabledUnits = new Set();
|
var enabledUnits = new Set();
|
||||||
(_servicesData || []).forEach(function(svc) {
|
(_servicesData || []).forEach(function(svc) {
|
||||||
@@ -352,25 +350,31 @@ async function loadStep3() {
|
|||||||
html += '<p class="onboarding-body-text">No domain-based services are enabled for your role. You can skip this step.</p>';
|
html += '<p class="onboarding-body-text">No domain-based services are enabled for your role. You can skip this step.</p>';
|
||||||
} else {
|
} else {
|
||||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
||||||
+ '<strong>Before you continue:</strong>'
|
+ '<p style="margin:0 0 8px;"><strong>Sovran_SystemsOS uses Njal.la for domains and Dynamic DNS.</strong></p>'
|
||||||
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
|
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
|
||||||
+ '<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a></li>'
|
+ '<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a>.</li>'
|
||||||
+ '<li>Purchase a new domain on Njal.la, or create a subdomain from a domain you already own. Tip: Subdomains are free to create — you only need to purchase one domain, and you can add as many subdomains as you need at no extra cost.</li>'
|
+ '<li>Buy at least one domain. Each service below needs its own domain — you can either give each service its own subdomain of a single domain you buy (subdomains are free, and one domain can have many), OR use a separate domain for each. Your choice.</li>'
|
||||||
+ '<li>In the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine\'s external IP address:<br>'
|
+ '<li>For each service, add a <strong>Dynamic</strong> record in Njal.la:'
|
||||||
+ '<span style="display:inline-block;margin-top:4px;padding:4px 12px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1.1em;font-weight:700;letter-spacing:0.03em;">' + escHtml(externalIp) + '</span></li>'
|
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
|
||||||
+ '<li>Njal.la will give you a curl command like:<br>'
|
+ '<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>'
|
||||||
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=sub.domain.com&k=abc123&auto"</code></li>'
|
+ '(Example only, your choice — for "call.yourdomain.com" you'd type just: <code>call</code>.)<br>'
|
||||||
+ '<li>Enter the subdomain and paste that curl command below for each service</li>'
|
+ 'If you bought a whole separate domain just for this service, leave Name blank or use <code>@</code>.<br>'
|
||||||
|
+ '⚠ Do NOT type the full domain in the Name field — Njal.la adds it automatically.</li>'
|
||||||
|
+ '<li>A Dynamic record has NO IP field. You don't enter an IP anywhere — it auto-fills once Sovran_SystemsOS updates it (on save, and again after reboot).</li>'
|
||||||
|
+ '</ul>'
|
||||||
|
+ '</li>'
|
||||||
|
+ '<li>Njal.la gives you a curl command like:<br>'
|
||||||
|
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=call.yourdomain.com&k=abc123&auto"</code></li>'
|
||||||
+ '</ol>'
|
+ '</ol>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
html += '<p class="onboarding-hint">Enter each fully-qualified subdomain (e.g. <code>matrix.yourdomain.com</code>) and its Njal.la DDNS curl command.</p>';
|
html += '<p class="onboarding-hint">Enter each service\'s full domain — a subdomain (e.g. <code>call.yourdomain.com</code>) or a separate domain (e.g. <code>call.com</code>) — and its Njal.la DDNS curl command.</p>';
|
||||||
relevantDomains.forEach(function(d) {
|
relevantDomains.forEach(function(d) {
|
||||||
var currentVal = (_domainsData && _domainsData[d.name]) || "";
|
var currentVal = (_domainsData && _domainsData[d.name]) || "";
|
||||||
html += '<div class="onboarding-domain-group">';
|
html += '<div class="onboarding-domain-group">';
|
||||||
html += '<label class="onboarding-domain-label">' + escHtml(d.label) + '</label>';
|
html += '<label class="onboarding-domain-label">' + escHtml(d.label) + '</label>';
|
||||||
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="domain-input-' + escHtml(d.name) + '" data-domain="' + escHtml(d.name) + '" placeholder="e.g. ' + escHtml(d.name) + '.yourdomain.com" value="' + escHtml(currentVal) + '" />';
|
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="domain-input-' + escHtml(d.name) + '" data-domain="' + escHtml(d.name) + '" placeholder="e.g. ' + escHtml(d.name) + '.yourdomain.com" value="' + escHtml(currentVal) + '" />';
|
||||||
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la DDNS Curl Command</label>';
|
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la DDNS Curl Command</label>';
|
||||||
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl "https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&k=abc123&auto"" />';
|
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl "https://njal.la/update/?h=...&k=...&auto"" />';
|
||||||
html += '<p class="onboarding-hint" style="margin-top:4px;">ℹ Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p>';
|
html += '<p class="onboarding-hint" style="margin-top:4px;">ℹ Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p>';
|
||||||
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
|
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
|
||||||
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
|
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
|
||||||
@@ -512,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;
|
||||||
|
|
||||||
@@ -523,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 computer’s 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 computer’s 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 to</th><th>Purpose</th></tr></thead>';
|
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward 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 += '<tr><td class="port-req-port">8448</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">Matrix Federation</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 4 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 to</th><th>Purpose</th></tr></thead>';
|
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward 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">30000–40000</td><td class="port-req-proto">TCP/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 & 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>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: 4</strong> (without Element Calling)<br>';
|
html += '<strong>Total port openings: 3</strong> (without Element Call)<br>';
|
||||||
html += '<strong>Total port openings: 9</strong> (with Element Calling — 4 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;">'
|
||||||
@@ -582,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,9 +128,8 @@
|
|||||||
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
||||||
<p class="onboarding-step-desc">
|
<p class="onboarding-step-desc">
|
||||||
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
|
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
|
||||||
First, create an account at <strong>Njal.la</strong> and purchase a new domain, or create a subdomain from a domain you already own. Tip: Subdomains are free to create — you only need to purchase one domain, and you can add as many subdomains as you need at no extra cost.
|
Create an account at Njal.la, then for each service below, add a <strong>Dynamic</strong> record — no IP needed, it auto-populates once the DDNS curl command runs.
|
||||||
Then, in the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine's external IP address (shown below).
|
Paste the curl command from your Njal.la dashboard for each service.
|
||||||
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding-card" id="step-3-body">
|
<div class="onboarding-card" id="step-3-body">
|
||||||
@@ -149,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,44 @@
|
|||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
HUB_NIX = Path(__file__).resolve().parents[2] / "modules" / "core" / "sovran-hub.nix"
|
||||||
|
|
||||||
|
|
||||||
|
def _section(source: str, start: str, end: str) -> str:
|
||||||
|
start_idx = source.find(start)
|
||||||
|
if start_idx == -1:
|
||||||
|
raise AssertionError(f"Expected section start not found: {start!r}")
|
||||||
|
end_idx = source.find(end, start_idx)
|
||||||
|
if end_idx == -1:
|
||||||
|
raise AssertionError(f"Expected section end not found: {end!r}")
|
||||||
|
return source[start_idx:end_idx]
|
||||||
|
|
||||||
|
|
||||||
|
class HubUpdateBootStagingTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.source = HUB_NIX.read_text()
|
||||||
|
self.update_section = _section(
|
||||||
|
self.source,
|
||||||
|
'update-script = pkgs.writeShellScript "sovran-hub-update.sh" \'\'',
|
||||||
|
"# ── Rebuild wrapper script",
|
||||||
|
)
|
||||||
|
self.rebuild_section = _section(
|
||||||
|
self.source,
|
||||||
|
'rebuild-script = pkgs.writeShellScript "sovran-hub-rebuild.sh" \'\'',
|
||||||
|
"# ── Brave launcher wrapper",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_full_update_uses_boot_not_switch(self):
|
||||||
|
self.assertIn("nixos-rebuild boot --flake /etc/nixos", self.update_section)
|
||||||
|
self.assertNotIn("nixos-rebuild switch --flake /etc/nixos", self.update_section)
|
||||||
|
|
||||||
|
def test_full_update_marks_reboot_required(self):
|
||||||
|
self.assertIn('echo "REBOOT_REQUIRED" > "$STATUS"', self.update_section)
|
||||||
|
|
||||||
|
def test_rebuild_path_keeps_switch_semantics(self):
|
||||||
|
self.assertIn("nixos-rebuild switch --flake /etc/nixos", self.rebuild_section)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Regression test for Starlette 1.1.0+ TemplateResponse keyword-argument style.
|
||||||
|
|
||||||
|
Prior to this fix, the three HTML routes called:
|
||||||
|
templates.TemplateResponse("name.html", {"request": request, ...})
|
||||||
|
which passes the context dict as the second positional argument. With the
|
||||||
|
updated Starlette/FastAPI versions shipped in NixOS unstable (Starlette 1.1.0,
|
||||||
|
FastAPI 0.136.3) that positional argument is the template name, causing Jinja2
|
||||||
|
to receive a dict as a cache key and raise:
|
||||||
|
TypeError: unhashable type: 'dict'
|
||||||
|
|
||||||
|
The fix updates every call to use keyword arguments:
|
||||||
|
templates.TemplateResponse(request=request, name="name.html", context={...})
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SERVER_PY = Path(__file__).resolve().parents[1] / "sovran_systemsos_web" / "server.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _template_response_calls(source: str):
|
||||||
|
"""Return a list of ast.Call nodes that are TemplateResponse calls."""
|
||||||
|
tree = ast.parse(source)
|
||||||
|
calls = []
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.Call):
|
||||||
|
continue
|
||||||
|
func = node.func
|
||||||
|
if isinstance(func, ast.Attribute) and func.attr == "TemplateResponse":
|
||||||
|
calls.append(node)
|
||||||
|
return calls
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateResponseSignatureTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.source = SERVER_PY.read_text()
|
||||||
|
self.calls = _template_response_calls(self.source)
|
||||||
|
|
||||||
|
def test_at_least_one_template_response_call_found(self):
|
||||||
|
self.assertGreater(len(self.calls), 0, "No TemplateResponse calls found in server.py")
|
||||||
|
|
||||||
|
def test_no_old_style_positional_dict_context(self):
|
||||||
|
"""No TemplateResponse call should pass a dict literal as its second positional arg.
|
||||||
|
|
||||||
|
The old style was:
|
||||||
|
templates.TemplateResponse("name.html", {"request": request, ...})
|
||||||
|
where args[0] is a string and args[1] is a Dict node. That pattern
|
||||||
|
triggers the Starlette 1.1.0 bug.
|
||||||
|
"""
|
||||||
|
for call in self.calls:
|
||||||
|
positional = call.args
|
||||||
|
if len(positional) >= 2 and isinstance(positional[1], ast.Dict):
|
||||||
|
self.fail(
|
||||||
|
f"Found old-style TemplateResponse call at line {call.lineno}: "
|
||||||
|
"second positional argument is a dict literal. "
|
||||||
|
"Use keyword arguments (request=, name=, context=) instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_request_not_duplicated_in_context(self):
|
||||||
|
"""The 'request' key must not appear inside the context= dict when
|
||||||
|
request= is already passed as a dedicated keyword argument."""
|
||||||
|
for call in self.calls:
|
||||||
|
kw_dict = {kw.arg: kw.value for kw in call.keywords if isinstance(kw, ast.keyword)}
|
||||||
|
|
||||||
|
if "request" not in kw_dict:
|
||||||
|
continue # no request= kwarg, nothing to check
|
||||||
|
|
||||||
|
context_node = kw_dict.get("context")
|
||||||
|
if not isinstance(context_node, ast.Dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for key_node in context_node.keys:
|
||||||
|
if isinstance(key_node, ast.Constant) and key_node.value == "request":
|
||||||
|
self.fail(
|
||||||
|
f"TemplateResponse at line {call.lineno} passes 'request' both as "
|
||||||
|
"request= keyword argument and inside the context dict."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_all_calls_use_keyword_arguments(self):
|
||||||
|
"""Every TemplateResponse call should use keyword arguments for request, name,
|
||||||
|
and context rather than relying on positional ordering."""
|
||||||
|
for call in self.calls:
|
||||||
|
kw_args = {kw.arg for kw in call.keywords if isinstance(kw, ast.keyword)}
|
||||||
|
self.assertIn(
|
||||||
|
"request",
|
||||||
|
kw_args,
|
||||||
|
f"TemplateResponse at line {call.lineno} is missing keyword argument 'request='.",
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"name",
|
||||||
|
kw_args,
|
||||||
|
f"TemplateResponse at line {call.lineno} is missing keyword argument 'name='.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
+2
-2
@@ -149,8 +149,8 @@
|
|||||||
gparted pv unzip parted screen zenity
|
gparted pv unzip parted screen zenity
|
||||||
libargon2 gnome-terminal libreoffice-fresh
|
libargon2 gnome-terminal libreoffice-fresh
|
||||||
dig firefox wp-cli axel
|
dig firefox wp-cli axel
|
||||||
lk-jwt-service livekit-libwebrtc livekit-cli livekit
|
lk-jwt-service livekit-libwebrtc livekit
|
||||||
matrix-synapse age
|
matrix-synapse age onlyoffice-desktopeditors
|
||||||
];
|
];
|
||||||
|
|
||||||
# ── Shell ──────────────────────────────────────────────────
|
# ── Shell ──────────────────────────────────────────────────
|
||||||
|
|||||||
Generated
+21
-21
@@ -5,11 +5,11 @@
|
|||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780397635,
|
"lastModified": 1783086783,
|
||||||
"narHash": "sha256-6WH7LKD6i91VLWoz4mEpoULtqVinCEZxG7ZjJPMSi3k=",
|
"narHash": "sha256-NxXpNF/9tq2nI+SxFHUxjro3u11SF3l4vs7bawdMKkQ=",
|
||||||
"owner": "emmanuelrosa",
|
"owner": "emmanuelrosa",
|
||||||
"repo": "btc-clients-nix",
|
"repo": "btc-clients-nix",
|
||||||
"rev": "feacd7684dc6bfcd49c57764944a2049bbd71924",
|
"rev": "4f6d07cae877ef58f0fbc9e731c99800ddb80859",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -52,11 +52,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778716662,
|
"lastModified": 1782949081,
|
||||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -108,11 +108,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780218263,
|
"lastModified": 1782911660,
|
||||||
"narHash": "sha256-T/f0pPDrH3Qc1VXyQXbK7yfHWRn90l3xwplc/nsxin4=",
|
"narHash": "sha256-PbR+tJ5E/Ux+01UtdFKqblccVA4/FgWbkym4ev3VHHQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7fc393d1b46fa000d48ff14e8b6a3c9985f03af0",
|
"rev": "cf720c15e108d432d29041cc5a185630809acefb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -139,11 +139,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780453794,
|
"lastModified": 1782999065,
|
||||||
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
|
"narHash": "sha256-5Dgj5+pIQYZKrXUGaLCk7CKfN3MmpwIhO94++WVxvng=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
|
"rev": "80d591ed473cfc46329932c2aadac9b435342c7c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -187,11 +187,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_3": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780243769,
|
"lastModified": 1782959384,
|
||||||
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
|
"narHash": "sha256-xnJJk+ct+D2+wdRxj1wk36w5zV9RVESwRqcklPdt3fM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
|
"rev": "65179426c83bb3f6bc14898b42ea1c6f01d374b0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -203,11 +203,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_4": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780336545,
|
"lastModified": 1782948114,
|
||||||
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
|
"narHash": "sha256-AXmz9ho4Lud5CsbrZsuSVwpQZ4o5FgZ1chxBn5cJ8+0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
|
"rev": "9e92285f211dad236540fd617d7e30e0b99bc0e1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -224,11 +224,11 @@
|
|||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780646548,
|
"lastModified": 1783173302,
|
||||||
"narHash": "sha256-Ckyl/l1XBmEwnaHcHD8PvBZk1uph0NqwbJ//CAvB7iE=",
|
"narHash": "sha256-nlnOw/zsD2H2NHSZ5oNWwcjuM17vipyAapfXsO78GjY=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixvim",
|
"repo": "nixvim",
|
||||||
"rev": "816a15282e58678dde831477964987d0262d4293",
|
"rev": "a402fdf2a1ef297d8ea7c95b90d6af0dbe90ab11",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ in
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
|
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
|
||||||
./branding.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.caddy = {
|
services.caddy = {
|
||||||
enable = true;
|
# Only enable Caddy when at least one domain-based service needs it or
|
||||||
|
# the operator has defined custom vhosts. This prevents Caddy from
|
||||||
|
# running on Desktop Only installs that have no web services configured.
|
||||||
|
enable = needsHttpsPorts || extraVhosts != "";
|
||||||
user = "caddy";
|
user = "caddy";
|
||||||
group = "root";
|
group = "root";
|
||||||
};
|
};
|
||||||
@@ -94,10 +97,10 @@ EOF
|
|||||||
$MATRIX {
|
$MATRIX {
|
||||||
reverse_proxy /_matrix/* http://localhost:8008
|
reverse_proxy /_matrix/* http://localhost:8008
|
||||||
reverse_proxy /_synapse/client/* http://localhost:8008
|
reverse_proxy /_synapse/client/* http://localhost:8008
|
||||||
|
handle /.well-known/matrix/server {
|
||||||
|
header Content-Type application/json
|
||||||
|
respond \`{"m.server":"$MATRIX:443"}\` 200
|
||||||
}
|
}
|
||||||
|
|
||||||
$MATRIX:8448 {
|
|
||||||
reverse_proxy http://localhost:8008
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
{
|
{
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
|
|
||||||
|
# nix-bitcoin is globally imported by the flake (nixosModules.Sovran_SystemsOS).
|
||||||
|
# This default satisfies nix-bitcoin's generateSecrets assertion so that Desktop
|
||||||
|
# Only systems can evaluate without enabling any Bitcoin services.
|
||||||
|
{
|
||||||
|
nix-bitcoin.generateSecrets = lib.mkDefault true;
|
||||||
|
}
|
||||||
|
|
||||||
# ── Server+Desktop Role (default) ─────────────────────────
|
# ── Server+Desktop Role (default) ─────────────────────────
|
||||||
(lib.mkIf config.sovran_systemsOS.roles.server_plus_desktop {
|
(lib.mkIf config.sovran_systemsOS.roles.server_plus_desktop {
|
||||||
sovran_systemsOS.web.btcpayserver = lib.mkDefault true;
|
sovran_systemsOS.web.btcpayserver = lib.mkDefault true;
|
||||||
@@ -12,15 +19,24 @@
|
|||||||
(lib.mkIf config.sovran_systemsOS.roles.desktop {
|
(lib.mkIf config.sovran_systemsOS.roles.desktop {
|
||||||
services.desktopManager.gnome.enable = true;
|
services.desktopManager.gnome.enable = true;
|
||||||
|
|
||||||
|
# Force all server/node services and features off so they cannot be
|
||||||
|
# accidentally enabled via custom.nix or option defaults on Desktop Only.
|
||||||
sovran_systemsOS.services = {
|
sovran_systemsOS.services = {
|
||||||
synapse = lib.mkDefault false;
|
synapse = lib.mkForce false;
|
||||||
bitcoin = lib.mkDefault false;
|
bitcoin = lib.mkForce false;
|
||||||
vaultwarden = lib.mkDefault false;
|
vaultwarden = lib.mkForce false;
|
||||||
wordpress = lib.mkDefault false;
|
wordpress = lib.mkForce false;
|
||||||
nextcloud = lib.mkDefault false;
|
nextcloud = lib.mkForce false;
|
||||||
};
|
};
|
||||||
|
|
||||||
sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
|
sovran_systemsOS.features = {
|
||||||
|
haven = lib.mkForce false;
|
||||||
|
mempool = lib.mkForce false;
|
||||||
|
element-calling = lib.mkForce false;
|
||||||
|
bitcoin-core = lib.mkForce false;
|
||||||
|
};
|
||||||
|
|
||||||
|
sovran_systemsOS.web.btcpayserver = lib.mkForce false;
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Bitcoin Node Only Role ────────────────────────────────
|
# ── Bitcoin Node Only Role ────────────────────────────────
|
||||||
|
|||||||
@@ -146,33 +146,16 @@ let
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$RC" -eq 0 ]; then
|
if [ "$RC" -eq 0 ]; then
|
||||||
echo "── Step 2/3: nixos-rebuild ──────────────────────────"
|
echo "── Step 2/3: nixos-rebuild boot (stage next reboot) ──"
|
||||||
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
BOOT_OUT=$(nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
--option connect-timeout 10 \
|
--option connect-timeout 10 \
|
||||||
--option stalled-download-timeout 90 \
|
--option stalled-download-timeout 90 \
|
||||||
--option download-attempts 7 \
|
--option download-attempts 7 \
|
||||||
--option fallback true 2>&1)
|
--option fallback true 2>&1)
|
||||||
SWITCH_RC=$?
|
BOOT_RC=$?
|
||||||
echo "$SWITCH_OUT"
|
echo "$BOOT_OUT"
|
||||||
if [ "$SWITCH_RC" -eq 0 ]; then
|
if [ "$BOOT_RC" -ne 0 ]; then
|
||||||
echo "[OK] switch succeeded"
|
echo "[ERROR] nixos-rebuild boot failed"
|
||||||
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
|
|
||||||
echo ""
|
|
||||||
echo " ✓ Build succeeded — a reboot is required to apply this update"
|
|
||||||
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
|
||||||
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
|
||||||
--option connect-timeout 10 \
|
|
||||||
--option stalled-download-timeout 90 \
|
|
||||||
--option download-attempts 7 \
|
|
||||||
--option fallback true 2>&1; then
|
|
||||||
echo "REBOOT_REQUIRED" > "$STATUS"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "[ERROR] nixos-rebuild boot also failed"
|
|
||||||
RC=1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[ERROR] nixos-rebuild switch failed"
|
|
||||||
RC=1
|
RC=1
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
@@ -188,9 +171,10 @@ let
|
|||||||
|
|
||||||
if [ "$RC" -eq 0 ]; then
|
if [ "$RC" -eq 0 ]; then
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✓ Update completed successfully"
|
echo " ✓ Update staged successfully"
|
||||||
|
echo " Reboot required to activate the new system"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo "SUCCESS" > "$STATUS"
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
||||||
else
|
else
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✗ Update failed — see errors above"
|
echo " ✗ Update failed — see errors above"
|
||||||
|
|||||||
@@ -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
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
+59
-21
@@ -34,8 +34,8 @@ lib.mkIf config.sovran_systemsOS.features.element-calling {
|
|||||||
};
|
};
|
||||||
|
|
||||||
####### ENSURE SERVICES START AFTER KEY EXISTS #######
|
####### ENSURE SERVICES START AFTER KEY EXISTS #######
|
||||||
systemd.services.livekit.after = [ "livekit-key-setup.service" ];
|
systemd.services.livekit.after = [ "livekit-key-setup.service" "livekit-turn-setup.service" ];
|
||||||
systemd.services.livekit.wants = [ "livekit-key-setup.service" ];
|
systemd.services.livekit.wants = [ "livekit-key-setup.service" "livekit-turn-setup.service" ];
|
||||||
systemd.services.lk-jwt-service.after = [ "livekit-key-setup.service" ];
|
systemd.services.lk-jwt-service.after = [ "livekit-key-setup.service" ];
|
||||||
systemd.services.lk-jwt-service.wants = [ "livekit-key-setup.service" ];
|
systemd.services.lk-jwt-service.wants = [ "livekit-key-setup.service" ];
|
||||||
|
|
||||||
@@ -68,10 +68,7 @@ $MATRIX {
|
|||||||
header /.well-known/matrix/* Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
header /.well-known/matrix/* Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
header /.well-known/matrix/* Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization"
|
header /.well-known/matrix/* Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization"
|
||||||
respond /.well-known/matrix/client \`{ "m.homeserver": {"base_url": "https://$MATRIX" }, "org.matrix.msc4143.rtc_foci": [{ "type":"livekit", "livekit_service_url":"https://$ELEMENT_CALLING/livekit/jwt" }] }\`
|
respond /.well-known/matrix/client \`{ "m.homeserver": {"base_url": "https://$MATRIX" }, "org.matrix.msc4143.rtc_foci": [{ "type":"livekit", "livekit_service_url":"https://$ELEMENT_CALLING/livekit/jwt" }] }\`
|
||||||
}
|
respond /.well-known/matrix/server \`{"m.server":"$MATRIX:443"}\`
|
||||||
|
|
||||||
$MATRIX:8448 {
|
|
||||||
reverse_proxy http://localhost:8008
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$ELEMENT_CALLING {
|
$ELEMENT_CALLING {
|
||||||
@@ -92,11 +89,17 @@ EOF
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
####### LIVEKIT RUNTIME CONFIG #######
|
####### LIVEKIT TURN SETUP (runtime cert + config) #######
|
||||||
systemd.services.livekit-runtime-config = {
|
# Replaces the old dead livekit-runtime-config.service. At runtime this:
|
||||||
description = "Generate LiveKit runtime config from domain files";
|
# * reads the matrix domain from /var/lib/domains/matrix (never hardcoded)
|
||||||
|
# * copies Caddy's already-issued matrix cert/key into /var/lib/livekit
|
||||||
|
# so LoadCredential can stage them for the (DynamicUser) livekit unit
|
||||||
|
# * writes a complete LiveKit config (with turn.domain substituted) that the
|
||||||
|
# overridden ExecStart loads.
|
||||||
|
systemd.services.livekit-turn-setup = {
|
||||||
|
description = "Stage TURN cert and generate LiveKit runtime config from domain files";
|
||||||
|
after = [ "caddy.service" "livekit-key-setup.service" ];
|
||||||
before = [ "livekit.service" ];
|
before = [ "livekit.service" ];
|
||||||
after = [ "livekit-key-setup.service" ];
|
|
||||||
requiredBy = [ "livekit.service" ];
|
requiredBy = [ "livekit.service" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
@@ -106,20 +109,42 @@ EOF
|
|||||||
unitConfig = {
|
unitConfig = {
|
||||||
ConditionPathExists = "/var/lib/domains/element-calling";
|
ConditionPathExists = "/var/lib/domains/element-calling";
|
||||||
};
|
};
|
||||||
path = [ pkgs.coreutils ];
|
path = [ pkgs.coreutils pkgs.findutils ];
|
||||||
script = ''
|
script = ''
|
||||||
MATRIX=$(cat /var/lib/domains/matrix)
|
MATRIX=$(cat /var/lib/domains/matrix)
|
||||||
|
|
||||||
mkdir -p /run/livekit
|
mkdir -p /run/livekit
|
||||||
|
|
||||||
cat > /run/livekit/runtime-config.yaml <<EOF
|
# Copy Caddy's already-issued matrix cert/key into LiveKit's state dir.
|
||||||
|
# The ACME CA hostname directory can vary, so glob for the domain dir.
|
||||||
|
CRT=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.crt" | head -n1)
|
||||||
|
KEY=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.key" | head -n1)
|
||||||
|
cp "$CRT" /var/lib/livekit/turn.crt
|
||||||
|
cp "$KEY" /var/lib/livekit/turn.key
|
||||||
|
chmod 640 /var/lib/livekit/turn.crt /var/lib/livekit/turn.key
|
||||||
|
|
||||||
|
# Generate the full LiveKit config the daemon will load. turn.domain is
|
||||||
|
# only known at runtime, so it is substituted here. The cert/key paths
|
||||||
|
# point at the LoadCredential-staged copies under /run/credentials.
|
||||||
|
cat > /run/livekit/livekit.yaml <<EOF
|
||||||
|
port: 7880
|
||||||
|
rtc:
|
||||||
|
use_external_ip: true
|
||||||
|
udp_port: 7882
|
||||||
|
port_range_start: 30000
|
||||||
|
port_range_end: 40000
|
||||||
|
room:
|
||||||
|
auto_create: false
|
||||||
turn:
|
turn:
|
||||||
|
enabled: true
|
||||||
domain: $MATRIX
|
domain: $MATRIX
|
||||||
cert_file: /var/lib/livekit/$MATRIX.crt
|
tls_port: 5349
|
||||||
key_file: /var/lib/livekit/$MATRIX.key
|
udp_port: 3478
|
||||||
|
cert_file: /run/credentials/livekit.service/turn-cert
|
||||||
|
key_file: /run/credentials/livekit.service/turn-key
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 640 /run/livekit/runtime-config.yaml
|
chmod 644 /run/livekit/livekit.yaml
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +156,8 @@ EOF
|
|||||||
settings = {
|
settings = {
|
||||||
rtc.use_external_ip = true;
|
rtc.use_external_ip = true;
|
||||||
rtc.udp_port = 7882;
|
rtc.udp_port = 7882;
|
||||||
|
rtc.port_range_start = 30000;
|
||||||
|
rtc.port_range_end = 40000;
|
||||||
room.auto_create = false;
|
room.auto_create = false;
|
||||||
turn = {
|
turn = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
@@ -140,14 +167,25 @@ EOF
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Override ExecStart to load the runtime-generated config (which carries the
|
||||||
|
# runtime-only turn.domain), mirroring the Caddy ExecStart override pattern in
|
||||||
|
# modules/core/caddy.nix. Deliver the TURN cert/key via LoadCredential so they
|
||||||
|
# are readable under the upstream unit's DynamicUser=true sandbox without
|
||||||
|
# weakening it. Everything else about the standard unit is left intact.
|
||||||
|
systemd.services.livekit.serviceConfig.ExecStart = lib.mkForce [
|
||||||
|
""
|
||||||
|
"${pkgs.livekit}/bin/livekit-server --config /run/credentials/livekit.service/livekit-config --key-file /run/credentials/livekit.service/livekit-secrets"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.livekit.serviceConfig.LoadCredential = [
|
||||||
|
"livekit-config:/run/livekit/livekit.yaml"
|
||||||
|
"livekit-secrets:${livekitKeyFile}"
|
||||||
|
"turn-cert:/var/lib/livekit/turn.crt"
|
||||||
|
"turn-key:/var/lib/livekit/turn.key"
|
||||||
|
];
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
||||||
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
|
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
|
||||||
networking.firewall.allowedUDPPortRanges = [
|
|
||||||
{ from = 30000; to = 40000; }
|
|
||||||
];
|
|
||||||
networking.firewall.allowedTCPPortRanges = [
|
|
||||||
{ from = 30000; to = 40000; }
|
|
||||||
];
|
|
||||||
|
|
||||||
####### JWT SERVICE RUNTIME CONFIG #######
|
####### JWT SERVICE RUNTIME CONFIG #######
|
||||||
systemd.services.lk-jwt-service-runtime-config = {
|
systemd.services.lk-jwt-service-runtime-config = {
|
||||||
|
|||||||
@@ -250,9 +250,6 @@ CREDS
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 8448 ];
|
|
||||||
networking.firewall.allowedUDPPorts = [ 8448 ];
|
|
||||||
|
|
||||||
sovran_systemsOS.domainRequirements = [
|
sovran_systemsOS.domainRequirements = [
|
||||||
{ name = "matrix"; label = "Matrix Synapse"; example = "matrix.yourdomain.com"; }
|
{ name = "matrix"; label = "Matrix Synapse"; example = "matrix.yourdomain.com"; }
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user