29 Commits

Author SHA1 Message Date
Sovran Systems 4e2264d5db Fix Element Calling 30000-40000 port note: single rule with both TCP and UDP
The previous change incorrectly split the 30000-40000 TURN relay range into two
rows and told users to create two separate forwarding rules. On most routers
this range is a single port-forwarding rule with a protocol selector set to
"Both" (or TCP/UDP). Revert to a single row (protocol "TCP & UDP"), update the
note to say it's one rule with both protocols enabled, and restore the totals to
3 required + 5 optional = 8.
2026-06-13 11:59:18 -05:00
Sovran Systems 82a6221880 Clarify Element Calling 30000-40000 range needs separate TCP and UDP forwards
The onboarding Step 4 port table listed the 30000-40000 TURN relay range as a
single "TCP/UDP" row, which is ambiguous on most routers where TCP and UDP
forwards are separate entries. Split it into two explicit rows (TCP and UDP),
add a clarifying note, and update the totals so users create both forwarding
rules.
2026-06-13 11:50:43 -05:00
Sovran Systems 6ec1faf3e6 Merge pull request #311 from naturallaw777/copilot/migrate-federation-to-well-known
Migrate Matrix federation from port 8448 to .well-known/matrix/server delegation on 443
2026-06-13 10:04:42 -05:00
copilot-swe-agent[bot] d3beee602d feat: migrate Matrix federation from port 8448 to .well-known/matrix/server on 443
- modules/core/caddy.nix: replace $MATRIX:8448 site with .well-known/matrix/server handler inside $MATRIX vhost
- modules/element-calling.nix: add .well-known/matrix/server respond directive, remove $MATRIX:8448 block
- modules/synapse.nix: remove 8448 TCP/UDP firewall openings
- app/sovran_systemsos_web/server.py: remove _PORTS_MATRIX_FEDERATION constant, set matrix-synapse.service to [], simplify api_service_detail to only check LiveKit extra ports
- app/sovran_systemsos_web/static/onboarding.js: remove 8448 row from Step 4, update totals to 3/8 (3 required + 5 optional)
2026-06-13 14:52:26 +00:00
copilot-swe-agent[bot] 29960e9937 Initial plan 2026-06-13 14:49:39 +00:00
Sovran Systems 417456485a Merge pull request #310 from naturallaw777/copilot/fix-iso-installer-imports
iso: remove orphaned branding.nix import from common.nix
2026-06-10 13:42:55 -05:00
copilot-swe-agent[bot] 0945092dde Remove orphaned ./branding.nix import from iso/common.nix 2026-06-10 18:41:49 +00:00
copilot-swe-agent[bot] 6f12117521 Initial plan 2026-06-10 18:40:41 +00:00
naturallaw777 8bf8814fa7 nixpkgs update 2026-06-09 11:03:40 -05:00
Sovran Systems d90b5b091b Add files via upload 2026-06-05 15:15:19 -05:00
Sovran Systems 4275ac1d2f Add files via upload 2026-06-05 13:27:04 -05:00
Sovran Systems 07b36d62d2 Merge pull request #309 from naturallaw777/copilot/add-desktop-screenshot
Add desktop screenshot to README
2026-06-05 13:20:41 -05:00
copilot-swe-agent[bot] 5fb8279d61 Improve screenshot alt text for accessibility 2026-06-05 18:14:13 +00:00
copilot-swe-agent[bot] 6eb63d3f85 Add desktop screenshot placeholder and README embed 2026-06-05 18:13:46 +00:00
copilot-swe-agent[bot] 2702854513 Plan README changes with placeholder screenshot 2026-06-05 18:13:16 +00:00
copilot-swe-agent[bot] 106537cc63 Initial plan 2026-06-05 17:56:09 +00:00
naturallaw777 dabb96e1b3 sync and removed element-desktop and bitwarden desktop 2026-06-05 10:29:20 -05:00
naturallaw777 2b5a154b99 updated nix packages 2026-06-05 10:27:51 -05:00
naturallaw777 e475b0f47d updated stable branch 2026-06-05 10:04:13 -05:00
naturallaw777 8f81f8f1e2 moved to new bitcoin-knots with bip110 2026-06-04 16:06:58 -05:00
Sovran Systems cd753a7e28 Merge pull request #308 from naturallaw777/copilot/fix-bip110-detection
Detect Knots `reduced_data` (RDTS) as BIP-110 in live status and add regression coverage
2026-06-04 15:18:10 -05:00
copilot-swe-agent[bot] 7ac1985508 Refine BIP110 matching and add regression coverage 2026-06-04 20:15:35 +00:00
copilot-swe-agent[bot] 0ecf2eb651 Fix BIP110 detection for reduced_data deployments 2026-06-04 20:11:58 +00:00
copilot-swe-agent[bot] 18c7095aaf Initial plan 2026-06-04 20:07:44 +00:00
Sovran Systems dcad276c59 Merge pull request #307 from naturallaw777/copilot/update-bip110-status-surface
Surface live BIP-110 deployment status on Bitcoin Knots tile
2026-06-04 14:51:22 -05:00
copilot-swe-agent[bot] 06988d0ff0 Fix docstring accuracy, extract _firstElementFromHtml helper, address all code review feedback 2026-06-04 19:49:01 +00:00
copilot-swe-agent[bot] 69b84153b4 Address code review: tighten bip110 key matching, fix redundant condition, extract shared badge config, add CSS classes 2026-06-04 19:46:40 +00:00
copilot-swe-agent[bot] df08a7c413 Add live BIP-110 deployment status: new helpers, endpoint, badge UI 2026-06-04 19:42:23 +00:00
copilot-swe-agent[bot] 602464189f Initial plan 2026-06-04 19:36:51 +00:00
17 changed files with 525 additions and 107 deletions
+8
View File
@@ -10,6 +10,14 @@
</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
+179 -19
View File
@@ -281,9 +281,6 @@ FEATURE_SERVICE_MAP = {
}
# 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 = [
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
@@ -296,7 +293,7 @@ SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
# Infrastructure
"caddy.service": [],
# Communication
"matrix-synapse.service": _PORTS_MATRIX_FEDERATION,
"matrix-synapse.service": [],
"livekit.service": _PORTS_ELEMENT_CALLING,
# Domain-based apps (80/443 handled by end-to-end domain reachability checks)
"btcpayserver.service": [],
@@ -2253,6 +2250,16 @@ _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
# Cache for ``bitcoin-cli getdeploymentinfo`` output (BIP-110 live status)
_btc_deployment_cache: tuple[float, dict | None] = (0.0, None)
# Bitcoin Knots exposes BIP-110 as the `reduced_data` versionbits deployment
# (RDTS, bit 4) in getdeploymentinfo. See Knots src/deploymentinfo.cpp,
# src/kernel/chainparams.cpp, and doc/bips.md.
BIP110_DEPLOYMENT_NAMES = {"reduced_data", "rdts", "bip110", "uasf-bip110"}
BIP110_VERSIONBITS_BIT = 4
BIP110_SUBVERSION_MARKERS = {"bip110", "uasf-bip110", "reduced_data", "rdts"}
# ── Generic service version detection (NixOS store path) ─────────
@@ -2367,6 +2374,154 @@ def _get_bitcoin_version_info() -> dict | None:
return None
def _get_bitcoin_deployment_info() -> dict | None:
"""Call bitcoin-cli getdeploymentinfo and return parsed JSON, or None on error.
Results are cached for _BTC_VERSION_CACHE_TTL seconds. Never raises.
"""
global _btc_deployment_cache
now = time.monotonic()
cached_at, cached_val = _btc_deployment_cache
if now - cached_at < _BTC_VERSION_CACHE_TTL:
return cached_val
try:
result = subprocess.run(
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getdeploymentinfo"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
_btc_deployment_cache = (now, None)
return None
info = json.loads(result.stdout)
_btc_deployment_cache = (now, info)
return info
except Exception:
_btc_deployment_cache = (now, None)
return None
def _get_bip110_status() -> dict:
"""Return a dict describing the live BIP-110 deployment/signaling state.
The returned struct has four stable keys::
{
"supported": bool, # node build is BIP-110-capable
"signaling": bool, # node is actively signaling / locked-in / active
"state": str, # "active" | "locked_in" | "signaling" |
# "not_signaling" | "unsupported" | "unknown"
"source": str, # "getdeploymentinfo" | "subversion" | "none"
}
Resolution order (authoritative → fallback → honest unknown):
1. ``getdeploymentinfo`` (authoritative) — scan ``deployments`` for BIP-110.
Bitcoin Knots currently exposes BIP-110 as ``reduced_data`` (RDTS, bit 4;
see Knots deploymentinfo.cpp / chainparams.cpp / doc/bips.md), so matching
first uses known deployment names, then falls back to versionbits bit 4.
2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
recognisable BIP-110 entry, inspect the ``subversion`` field from
``getnetworkinfo``. A case-insensitive match for known BIP-110 markers
(including "bip110", "uasf-bip110", "reduced_data", "rdts") is treated as
"signaling".
3. Unknown — if the node is entirely unreachable or neither source is
conclusive, return state="unknown", signaling=False, source="none".
"""
_unknown: dict = {"supported": False, "signaling": False, "state": "unknown", "source": "none"}
def _deployment_bit(entry: dict) -> int | None:
bip9 = entry.get("bip9", {}) or {}
bip8 = entry.get("bip8", {}) or {}
bit = bip9.get("bit")
if bit is None:
bit = bip8.get("bit")
if bit is None:
bit = entry.get("bit")
return bit
# ── 1. getdeploymentinfo (authoritative) ──────────────────────────
deploy_info = _get_bitcoin_deployment_info()
if deploy_info is not None:
deployments = deploy_info.get("deployments", {})
if isinstance(deployments, dict):
matched_entry: dict | None = None
# Primary match: known deployment names (case-insensitive exact match)
for key, entry in deployments.items():
if not isinstance(entry, dict):
continue
key_lower = key.lower()
if key_lower not in BIP110_DEPLOYMENT_NAMES:
continue
matched_entry = entry
break
# Secondary match: versionbits bit (fallback only)
if matched_entry is None:
for _, entry in deployments.items():
if not isinstance(entry, dict):
continue
if _deployment_bit(entry) != BIP110_VERSIONBITS_BIT:
continue
matched_entry = entry
break
if matched_entry is not None:
entry = matched_entry
# bip9 / bip8 status field
bip9 = entry.get("bip9", {}) or {}
bip8 = entry.get("bip8", {}) or {}
status = (
bip9.get("status")
or bip8.get("status")
or entry.get("status")
or ""
).lower()
active = entry.get("active", False)
if active or status == "active":
return {"supported": True, "signaling": True, "state": "active", "source": "getdeploymentinfo"}
if status == "locked_in":
return {"supported": True, "signaling": True, "state": "locked_in", "source": "getdeploymentinfo"}
if status in ("started", "defined"):
# Check whether deployment is currently signaling in this period.
stats = bip9.get("statistics") or bip8.get("statistics") or {}
# Some Knots outputs expose only ``count`` (not explicit signaling bool),
# so treat count>0 as a conservative signaling indicator for this period.
count = stats.get("count")
signaling = bool(
stats.get("signaling")
or stats.get("signalling")
or (isinstance(count, int) and count > 0)
)
if signaling:
return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
if status == "failed":
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
# Entry found but status unrecognised — node supports BIP-110 but state unclear
return {"supported": True, "signaling": False, "state": "unknown", "source": "getdeploymentinfo"}
# ── 2. Subversion fallback ─────────────────────────────────────────
net_info = _get_bitcoin_version_info()
if net_info is not None:
subversion = net_info.get("subversion", "") or ""
sv_lower = subversion.lower()
if any(marker in sv_lower for marker in BIP110_SUBVERSION_MARKERS):
return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
# Node is reachable via RPC but no BIP-110 marker found anywhere
return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
# ── 3. Node unreachable / RPC not ready ───────────────────────────
return _unknown
def _get_bitcoind_version() -> str | None:
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
@@ -2481,6 +2636,19 @@ async def api_bitcoin_version():
}
@app.get("/api/bitcoin/bip110")
async def api_bitcoin_bip110():
"""Return live BIP-110 deployment/signaling status from bitcoin-cli.
Always returns HTTP 200. When bitcoind is unreachable or the node is mid-IBD
the response will contain ``state = "unknown"`` so the UI can render a neutral
badge rather than an error toast.
"""
loop = asyncio.get_event_loop()
status = await loop.run_in_executor(None, _get_bip110_status)
return status
@app.get("/api/services")
async def api_services():
cfg = load_config()
@@ -2661,6 +2829,8 @@ async def api_services():
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_data["bitcoin_version"] = btc_ver # backwards compat
service_data["version"] = btc_ver
if icon == "bip110":
service_data["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_data
results = await asyncio.gather(*[get_status(s) for s in services])
@@ -2810,28 +2980,16 @@ async def api_service_detail(unit: str, icon: str | None = None):
"status": ps,
"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:
domain_check_steps.append({
"step": 4,
"label": "Federation Port" if unit == "matrix-synapse.service" else "Additional Ports Required",
"label": "Additional Ports Required",
"status": "skipped",
"detail": "Skipped until Steps 1-3 are complete",
})
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:
extra_open = all(p["status"] != "closed" for p in extra_ports)
domain_check_steps.append({
@@ -2945,6 +3103,8 @@ async def api_service_detail(unit: str, icon: str | None = None):
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_detail["bitcoin_version"] = btc_ver # backwards compat
service_detail["version"] = btc_ver
if icon == "bip110":
service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_detail
@@ -155,6 +155,69 @@
white-space: nowrap;
}
/* ── BIP-110 status badge (tile + detail modal) ───────────────────── */
.tile-bip110-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.64rem;
font-weight: 600;
border-radius: 4px;
padding: 2px 6px;
margin-top: 4px;
white-space: nowrap;
letter-spacing: 0.02em;
}
.tile-bip110-badge--active {
background: rgba(109, 191, 139, 0.18);
color: var(--green);
border: 1px solid rgba(109, 191, 139, 0.3);
}
.tile-bip110-badge--locked_in {
background: rgba(94, 173, 138, 0.15);
color: var(--accent-color);
border: 1px solid rgba(94, 173, 138, 0.3);
}
.tile-bip110-badge--signaling {
background: rgba(94, 173, 138, 0.12);
color: var(--accent-color);
border: 1px solid rgba(94, 173, 138, 0.2);
}
.tile-bip110-badge--not_signaling {
background: rgba(229, 165, 10, 0.12);
color: var(--yellow);
border: 1px solid rgba(229, 165, 10, 0.25);
}
.tile-bip110-badge--unsupported {
background: rgba(94, 122, 106, 0.12);
color: var(--grey);
border: 1px solid rgba(94, 122, 106, 0.2);
}
.tile-bip110-badge--unknown {
background: transparent;
color: var(--text-dim);
border: 1px solid var(--border-color);
}
.bip110-status-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.bip110-source-label {
color: var(--text-dim);
font-size: 0.75rem;
}
/* ── Service detail modal sections ───────────────────────────────── */
.svc-detail-section {
@@ -60,3 +60,17 @@ async function apiFetch(path, options) {
}
return res.json();
}
// ── BIP-110 badge state config ────────────────────────────────────
// Shared lookup used by tiles.js and service-detail.js.
// Keys match the "state" values returned by /api/bitcoin/bip110.
var BIP110_BADGE_CONFIG = {
active: { cls: 'tile-bip110-badge--active', label: 'Active', title: 'BIP-110 is active on this node' },
locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'Locked In', title: 'BIP-110 is locked in and will activate shortly' },
signaling: { cls: 'tile-bip110-badge--signaling', label: 'Signaling', title: 'Node is signaling readiness for BIP-110' },
not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' },
unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'Not Supported', title: 'This node build does not include BIP-110' },
unknown: { cls: 'tile-bip110-badge--unknown', label: '\u2014', title: 'Status unavailable (node syncing or RPC not ready)' }
};
@@ -107,6 +107,21 @@ async function openServiceDetailModal(unit, name, icon) {
'</div>' +
'</div>';
// Section B2: BIP-110 live status (bip110 tile only)
if (icon === 'bip110' && data.bip110) {
var bip110 = data.bip110;
var bip110State = bip110.state || 'unknown';
var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
var bip110Source = bip110.source ? ' <span class="bip110-source-label">(source: ' + escHtml(bip110.source) + ')</span>' : '';
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">BIP-110 Deployment Status</div>' +
'<div class="bip110-status-row">' +
'<span class="tile-bip110-badge ' + bip110Cfg.cls + '" title="' + escHtml(bip110Cfg.title) + '">' + escHtml(bip110Cfg.label) + '</span>' +
bip110Source +
'</div>' +
'</div>';
}
// Section C: Domain diagnostics (domain services)
if (data.needs_domain) {
var steps = data.domain_check_steps || [];
+34 -1
View File
@@ -4,6 +4,21 @@
// Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {};
// ── BIP-110 badge helper ──────────────────────────────────────────
function _renderBip110Badge(bip110) {
if (!bip110) return '';
var state = bip110.state || 'unknown';
var cfg = BIP110_BADGE_CONFIG[state] || BIP110_BADGE_CONFIG.unknown;
return '<div class="tile-bip110-badge ' + cfg.cls + '" title="' + escHtml(cfg.title) + '">' + escHtml(cfg.label) + '</div>';
}
function _firstElementFromHtml(html) {
var tmp = document.createElement("div");
tmp.innerHTML = html;
return tmp.firstElementChild || null;
}
// ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) {
@@ -165,7 +180,8 @@ function buildTile(svc) {
var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : '';
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + bip110Badge + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
tile.style.cursor = "pointer";
tile.addEventListener("click", function() {
@@ -265,6 +281,23 @@ function updateTiles(services) {
}
}
}
// Update BIP-110 badge for bip110 tiles
if (svc.icon === 'bip110') {
var badgeHtml = _renderBip110Badge(svc.bip110);
var badgeEl = tile.querySelector(".tile-bip110-badge");
if (badgeEl) {
// Replace existing badge in-place
var newBadge = _firstElementFromHtml(badgeHtml);
if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); }
} else if (badgeHtml) {
// Insert badge after version label (or after tile-name if no version)
var anchorEl = tile.querySelector(".tile-version") || tile.querySelector(".tile-name");
if (anchorEl) {
var newBadgeEl = _firstElementFromHtml(badgeHtml);
if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
}
}
}
}
}
}
@@ -355,9 +355,9 @@ async function loadStep3() {
+ '<strong>Before you continue:</strong>'
+ '<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>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>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 like.</li>'
+ '<li>In the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine\'s external IP address:<br>'
+ '<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>'
+ '<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;">' + escHtml(externalIp) + '</span></li>'
+ '<li>Njal.la will give you a curl command like:<br>'
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=sub.domain.com&amp;k=abc123&amp;auto"</code></li>'
+ '<li>Enter the subdomain and paste that curl command below for each service</li>'
@@ -370,7 +370,7 @@ async function loadStep3() {
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 += '<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 &quot;https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&amp;k=abc123&amp;auto&quot;" />';
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl &quot;https://njal.la/update/?h=...&amp;k=...&amp;auto&quot;" />';
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 += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
@@ -545,14 +545,13 @@ async function loadStep4() {
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">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">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 += '</div>';
// Optional ports table
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 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);">These 5 additional port openings are required on top of the 3 required ports above.</div>';
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 += '<tbody>';
@@ -560,14 +559,15 @@ async function loadStep4() {
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">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/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">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 += '</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>';
// Totals
html += '<div class="onboarding-port-totals">';
html += '<strong>Total port openings: 4</strong> (without Element Calling)<br>';
html += '<strong>Total port openings: 9</strong> (with Element Calling — 4 required + 5 optional)';
html += '<strong>Total port openings: 3</strong> (without Element Calling)<br>';
html += '<strong>Total port openings: 8</strong> (with Element Calling — 3 required + 5 optional)';
html += '</div>';
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
+166
View File
@@ -0,0 +1,166 @@
import unittest
from unittest.mock import patch
from pathlib import Path
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 Bip110StatusTests(unittest.TestCase):
def _status(self, deploy_info, net_info):
with patch.object(server, "_get_bitcoin_deployment_info", return_value=deploy_info), patch.object(
server, "_get_bitcoin_version_info", return_value=net_info
):
return server._get_bip110_status()
def test_started_reduced_data_reports_signaling(self):
deploy_info = {
"deployments": {
"reduced_data": {
"type": "bip9",
"active": False,
"bip9": {
"bit": 4,
"status": "started",
"statistics": {"elapsed": 833, "count": 4, "threshold": 1109},
"signalling": "--#--",
},
}
}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
self.assertEqual(
result,
{"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"},
)
def test_active_reduced_data_reports_active(self):
deploy_info = {
"deployments": {"reduced_data": {"active": True, "bip9": {"bit": 4, "status": "active"}}}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
self.assertEqual(result["state"], "active")
self.assertTrue(result["supported"])
self.assertTrue(result["signaling"])
self.assertEqual(result["source"], "getdeploymentinfo")
def test_locked_in_reduced_data_reports_locked_in(self):
deploy_info = {
"deployments": {"reduced_data": {"active": False, "bip9": {"bit": 4, "status": "locked_in"}}}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
self.assertEqual(result["state"], "locked_in")
self.assertTrue(result["supported"])
self.assertTrue(result["signaling"])
self.assertEqual(result["source"], "getdeploymentinfo")
def test_no_bip110_deployment_and_plain_subversion_reports_unsupported(self):
deploy_info = {
"deployments": {
"taproot": {"type": "bip9", "active": True, "bip9": {"bit": 2, "status": "active"}},
}
}
result = self._status(deploy_info, {"subversion": "/Satoshi:27.0.0/"})
self.assertEqual(
result,
{"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"},
)
def test_node_unreachable_reports_unknown(self):
result = self._status(None, None)
self.assertEqual(result, {"supported": False, "signaling": False, "state": "unknown", "source": "none"})
if __name__ == "__main__":
unittest.main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

+2 -2
View File
@@ -145,10 +145,10 @@
ranger fastfetch gedit openssl pwgen
aspell aspellDicts.en lm_sensors
hunspell hunspellDicts.en_US
synadm brave dua bitwarden-desktop
synadm brave dua
gparted pv unzip parted screen zenity
libargon2 gnome-terminal libreoffice-fresh
dig firefox element-desktop wp-cli axel
dig firefox wp-cli axel
lk-jwt-service livekit-libwebrtc livekit-cli livekit
matrix-synapse age
];
Generated
+29 -63
View File
@@ -1,33 +1,15 @@
{
"nodes": {
"bip110": {
"btc-clients": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1778967282,
"narHash": "sha256-0g9RvVCD6zxY2vy54GhbB1OeeEZdKuxTr9r0whcpRjQ=",
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"rev": "8d23ed98940d70e42ee870d719677a073a0a5920",
"type": "github"
},
"original": {
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"type": "github"
}
},
"btc-clients": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1779889285,
"narHash": "sha256-5QOMNn/rxJjsy9n2pAG5+AwUXOAPXSzcr62y1tGHXKA=",
"lastModified": 1781013869,
"narHash": "sha256-XlEUtL+8M6kbPdmIh4sQQ7G02/1CwHQEk1RPvIMEWOs=",
"owner": "emmanuelrosa",
"repo": "btc-clients-nix",
"rev": "9a3dd86e11ea5fb17ace9043aa3d0d5ed359a3ca",
"rev": "9a6c78204dc8961840375b110bca595b1f6f084c",
"type": "github"
},
"original": {
@@ -105,7 +87,7 @@
"inputs": {
"extra-container": "extra-container",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs_2",
"nixpkgs-25_05": "nixpkgs-25_05",
"nixpkgs-unstable": "nixpkgs-unstable"
},
@@ -126,16 +108,15 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1777728799,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=",
"lastModified": 1780218263,
"narHash": "sha256-T/f0pPDrH3Qc1VXyQXbK7yfHWRn90l3xwplc/nsxin4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5",
"rev": "7fc393d1b46fa000d48ff14e8b6a3c9985f03af0",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
@@ -158,16 +139,16 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1751274312,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
"lastModified": 1780902259,
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-24.11",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
@@ -189,21 +170,6 @@
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1777728799,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1778737229,
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
@@ -219,13 +185,13 @@
"type": "github"
}
},
"nixpkgs_4": {
"nixpkgs_3": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"lastModified": 1780749050,
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
"type": "github"
},
"original": {
@@ -235,13 +201,13 @@
"type": "github"
}
},
"nixpkgs_5": {
"nixpkgs_4": {
"locked": {
"lastModified": 1779259093,
"narHash": "sha256-7DKWmH23hL2eYdkxCKeqj2i+yljTKuU+3Nk1UPHOnxc=",
"lastModified": 1780336545,
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d99b013d5d1931ad77fe3912ed218170dec5d9a4",
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
"type": "github"
},
"original": {
@@ -254,15 +220,15 @@
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_5",
"nixpkgs": "nixpkgs_4",
"systems": "systems_2"
},
"locked": {
"lastModified": 1779816597,
"narHash": "sha256-Kgod3gZlhSp6WozZ2pFaclXbWpjs6kQLAtldoxb85Lc=",
"lastModified": 1780995253,
"narHash": "sha256-6Lsoyw2XPvY8YNMCtPnsyw0JVVtHsXP2xtrFJBBTAOQ=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "297f9341476ba7f821a42d7a2805e206ef8c6ef8",
"rev": "43a7e6f82978ac975c3bba6728869b231e7a1ba0",
"type": "github"
},
"original": {
@@ -273,10 +239,9 @@
},
"root": {
"inputs": {
"bip110": "bip110",
"btc-clients": "btc-clients",
"nix-bitcoin": "nix-bitcoin",
"nixpkgs": "nixpkgs_4",
"nixpkgs": "nixpkgs_3",
"nixpkgs-stable": "nixpkgs-stable",
"nixvim": "nixvim"
}
@@ -298,15 +263,16 @@
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"type": "github"
},
"original": {
"owner": "nix-systems",
"ref": "future-26.11",
"repo": "default",
"type": "github"
}
+1 -1
View File
@@ -6,7 +6,7 @@
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
nixvim.url = "github:nix-community/nixvim";
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-24.11";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
};
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
-1
View File
@@ -16,7 +16,6 @@ in
{
imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
./branding.nix
];
image.baseName = lib.mkForce "Sovran_SystemsOS";
+1 -1
View File
@@ -4,7 +4,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
services.bitcoind = {
enable = true;
package = config.nix-bitcoin.pkgs.bitcoind-knots;
package = pkgs.bitcoind-knots;
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
txindex = true;
tor.proxy = true;
+3 -3
View File
@@ -94,10 +94,10 @@ EOF
$MATRIX {
reverse_proxy /_matrix/* 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
fi
+1 -4
View File
@@ -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-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" }] }\`
}
$MATRIX:8448 {
reverse_proxy http://localhost:8008
respond /.well-known/matrix/server \`{"m.server":"$MATRIX:443"}\`
}
$ELEMENT_CALLING {
-3
View File
@@ -250,9 +250,6 @@ CREDS
'';
};
networking.firewall.allowedTCPPorts = [ 8448 ];
networking.firewall.allowedUDPPorts = [ 8448 ];
sovran_systemsOS.domainRequirements = [
{ name = "matrix"; label = "Matrix Synapse"; example = "matrix.yourdomain.com"; }
];