51 Commits

Author SHA1 Message Date
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
Sovran Systems 67f4cdc99e Merge pull request #306 from naturallaw777/copilot/remove-bip110-feature-toggle
bip110 deprecation shim: tolerate stale custom.nix and auto-clean on Hub startup
2026-06-04 14:26:06 -05:00
copilot-swe-agent[bot] f8c717db25 Address code review: fix whitespace and log migration exceptions 2026-06-04 19:18:28 +00:00
copilot-swe-agent[bot] 268abddb28 Add deprecated bip110 no-op shim and Hub migration
- modules/core/roles.nix: re-declare bip110 as a nullOr bool no-op
  option so existing custom.nix files with `lib.mkForce true` continue
  to evaluate; add config.warnings block that fires only when the stale
  flag is explicitly set
- server.py: add DEPRECATED_FEATURE_IDS constant; skip deprecated ids
  in _read_hub_overrides and _write_hub_overrides; add
  _migrate_strip_deprecated_features helper that rewrites the Hub
  Managed section without deprecated lines on startup; add
  @app.on_event("startup") handler _startup_migrate_deprecated_features
2026-06-04 19:16:36 +00:00
copilot-swe-agent[bot] c1119b03a8 Initial plan 2026-06-04 19:12:56 +00:00
Sovran Systems 0c273b758d Merge pull request #305 from naturallaw777/copilot/update-hub-feature-manager
Retire deprecated bip110 flake input; collapse Bitcoin node tiles from three to two
2026-06-04 13:57:52 -05:00
copilot-swe-agent[bot] 6f98c478e8 Fix bitcoin-core confirmation dialog: show always when enabling (not just on conflicts) 2026-06-04 18:55:46 +00:00
copilot-swe-agent[bot] 875a6a9297 Retire deprecated bip110 flake input; collapse Bitcoin node tiles to two 2026-06-04 18:52:28 +00:00
copilot-swe-agent[bot] 1dbfe3cd94 Initial plan 2026-06-04 18:47:49 +00:00
naturallaw777 e0d4b3544d updated config for gnome 50 support 2026-05-27 11:15:09 -05:00
Sovran Systems 3e3fbed470 Merge pull request #304 from naturallaw777/copilot/fix-sovran-hub-updater-retry-issue
Harden Hub updater against cache.nixos.org narinfo stalls
2026-05-27 11:06:11 -05:00
Sovran Systems ada9f25c41 Merge pull request #303 from naturallaw777/copilot/fix-missing-status-text-update-modal
Add unified asset-version cache busting for Hub templates to prevent stale update modal UI
2026-05-27 11:05:49 -05:00
copilot-swe-agent[bot] 66cacaaf9d Harden asset-version fallback hashing separators 2026-05-27 16:03:31 +00:00
copilot-swe-agent[bot] 15cd07d12f Add resilient Nix download/fallback settings for hub update flows 2026-05-27 16:02:37 +00:00
copilot-swe-agent[bot] fae57c0375 Initial plan 2026-05-27 15:59:57 +00:00
copilot-swe-agent[bot] 3745eedd74 Add unified template asset cache-busting version 2026-05-27 15:59:50 +00:00
copilot-swe-agent[bot] 1cd4fc8b40 Initial plan 2026-05-27 15:56:18 +00:00
Sovran Systems 732ab6f2aa Merge pull request #302 from naturallaw777/staging-dev
overall nixpkgs update
2026-05-27 09:38:22 -05:00
naturallaw777 bea26c55c3 overall nixpkgs update 2026-05-27 09:35:36 -05:00
Sovran Systems a841665b07 Refine networking and security section in README 2026-05-23 15:46:24 -05:00
Sovran Systems d574f96379 Update README.md 2026-05-23 15:42:59 -05:00
Sovran Systems 2388039b63 Add Sovran Hub icon SVG referenced by README 2026-05-23 11:35:09 -05:00
Sovran Systems aa69d40f08 README: use new Sovran Hub icon 2026-05-23 11:33:46 -05:00
Sovran Systems 31cb48cc2b Add new Sovran Hub icon (v3) 2026-05-23 11:30:45 -05:00
naturallaw777 170bd14a34 update readme 2026-05-23 11:28:02 -05:00
Sovran Systems 2553e0dce0 Merge pull request #301 from naturallaw777/copilot/fix-udp-port-range-in-onboarding
Sync Element Calling port guidance with LiveKit UDP mux (7882)
2026-05-21 22:39:58 -05:00
copilot-swe-agent[bot] b8e7b2b4cc fix: update element calling onboarding UDP mux port text
Agent-Logs-Url: https://github.com/naturallaw777/sovran-systems/sessions/d45e1a45-fc4e-4bd5-adfd-c798c0ff3987

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-22 03:37:52 +00:00
copilot-swe-agent[bot] 24098a209a Initial plan 2026-05-22 03:34:50 +00:00
Sovran Systems 8ad7509b02 Merge pull request #300 from naturallaw777/copilot/fix-rtc-udp-port-configuration
Fix LiveKit ICE failure: use single integer for rtc.udp_port
2026-05-21 22:30:11 -05:00
copilot-swe-agent[bot] a350d4e2f7 Fix LiveKit rtc.udp_port: use integer 7882 instead of string range, update firewall rules
Agent-Logs-Url: https://github.com/naturallaw777/sovran-systems/sessions/f531f757-8ab7-4742-9c75-8d1e57d73380

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-22 03:23:10 +00:00
copilot-swe-agent[bot] ec3782991d Initial plan 2026-05-22 03:21:40 +00:00
24 changed files with 958 additions and 192 deletions
+159 -1
View File
@@ -1 +1,159 @@
### Testing Branch <div align="center">
<img src="iso/assets/sovran-hub-icon.svg" alt="Sovran Systems" width="160" />
# Sovran_SystemsOS
`Base Development` · NixOS Flake · AGPL-3.0
[Sovran Systems](https://sovransystems.com)
</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
1. [What This Repo Is](#what-this-repo-is)
2. [Architecture](#architecture)
3. [Module Catalog](#module-catalog)
4. [The Three Modes (internal reference)](#the-three-modes-internal-reference)
5. [Build & Deploy Reference](#build--deploy-reference)
6. [Networking & Reverse Proxy](#networking--reverse-proxy)
7. [Security Posture](#security-posture)
8. [Backups & Recovery](#backups--recovery)
9. [License](#license)
---
## What This Repo Is
Sovran_SystemsOS is defined entirely as a **Nix flake** (`flake.nix`) and built from source. There is no pre-built binary — the System Installer is produced from this tree. Everything the system does is declared here.
The control center is the **Hub** — a built-in panel that lets the operator launch, monitor, and toggle services without touching a terminal. Under the hood, the Hub writes to `custom.nix`, which feeds back into the flake.
## Architecture
```
┌─────────────────────────┐
│ flake.nix │
│ inputs: nixpkgs, │
│ nix-bitcoin, nixvim, │
│ btc-clients │
└───────────┬─────────────┘
│ nixosModules.Sovran_SystemsOS
┌──────────────────────────┐ imports ┌──────────────────────────┐
│ configuration.nix │────────────▶│ modules/modules.nix │
│ boot / fs / users / │ │ core/* + services + opt │
│ desktop / nix settings │ │ features │
└──────────────────────────┘ └──────────┬───────────────┘
▲ │
│ ./role-state.nix (mode/role) ▼
│ ./custom.nix (user overrides) ┌────────────────────┐
│ │ modules/*.nix │
└───────── sovran-hub writes ───────▶│ synapse / wordpress│
│ nextcloud / etc. │
└────────────────────┘
```
- **`flake.nix`** declares two NixOS configurations:
- `nixosConfigurations.nixos` — the running system.
- `nixosConfigurations.sovran_systemsos-iso` — the System Installer.
- **`configuration.nix`** owns host concerns (boot, filesystems, users, desktop, locale, Nix settings, firewall, audio, backups).
- **`modules/modules.nix`** is the service router. Every other module is opt-in via flags read from `role-state.nix` and `custom.nix`.
## Module Catalog
Defaults follow the import order in `modules/modules.nix`. Toggles live in `custom.nix` (the Hub writes them) and `role-state.nix`.
| Module | Default | Purpose |
|---|---|---|
| `core/*` | **on** | Roles, Caddy, Njalla, Hub, desktop, perf, ssh-bootstrap |
| `php.nix`, `credentials.nix` | **on** | Required by web services & secrets |
| `synapse.nix` | **on** | Matrix homeserver |
| `wordpress.nix` | **on** | WordPress + PHP-FPM vhost |
| `nextcloud.nix` | **on** | Files / calendar / contacts |
| `vaultwarden.nix` | **on** | Bitwarden-compatible secrets vault |
| `bitcoinecosystem.nix` | **on** | bitcoind/electrs/LND/RTL/BTCPay (over Tor) |
| `wallet-autoconnect.nix` | **on** | Sparrow/Bisq ↔ node handshake |
| `haven.nix` | off | Nostr relay |
| `element-calling.nix` | off | LiveKit + JWT for E2E calling |
| `mempool.nix` | off | Mempool.space dashboard |
| `bitcoin-core.nix` | off | Switch node to Bitcoin Core (replaces default Bitcoin Knots + BIP110) |
| `rdp.nix` | off | xrdp remote desktop |
| `sshd.nix` | off | Public-facing OpenSSH |
> Tor is wired directly into the Bitcoin stack. In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true` and `tor.proxy = true`, and onion services are exposed for them.
## The Three Modes (internal reference)
Selected by `role-state.nix`, resolved by `modules/core/role-logic.nix`. All three configurations are produced from this same flake.
| Mode | What's enabled on top of the base NixOS + GNOME |
|---|---|
| **Desktop** | Private daily-driver. Sparrow + Bisq included. |
| **Node** | Desktop + full Bitcoin stack (bitcoind/electrs/LND/RTL/BTCPay over Tor). |
| **Server+Desktop** | Node + self-hosting services (Synapse, Nextcloud, WordPress, Vaultwarden, Element Calling, etc.). |
## Build & Deploy Reference
Internal commands. Run from the flake root.
| Action | Command |
|---|---|
| Build the System Installer | `nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage` |
| Switch now | `sudo nixos-rebuild switch --flake .#nixos` |
| Test in current boot only | `sudo nixos-rebuild test --flake .#nixos` |
| Stage for next boot | `sudo nixos-rebuild boot --flake .#nixos` |
| Build only (no activation) | `nixos-rebuild build --flake .#nixos` |
| Update pinned inputs | `nix flake update` (then rebuild) |
| Rollback last switch | `sudo nixos-rebuild switch --rollback` |
| Garbage-collect (>7 days) | Automatic weekly; manual: `sudo nix-collect-garbage -d` |
## Networking & Reverse Proxy
- **Firewall on by default** (`networking.firewall.enable = true`). Port are opened by the module that needs it.
- **Caddy** (`modules/core/caddy.nix`) terminates TLS for all HTTP services.
- **Njalla** dynamic DNS (`modules/core/njalla.nix`) keeps records in sync via a 15-minute cron job.
- **Tor** is enabled with `torsocks` available. The Bitcoin stack uses it directly — see [Security Posture](#security-posture).
- **SSH:** localhost-only by default (`core/sshd-localhost.nix`).
## Security Posture
Facts about the defaults, straight from `configuration.nix` and the modules:
- **Reproducible builds.** Every artifact derives from `flake.lock`. The same commit produces the same OS.
- **Bitcoin stack over Tor.** In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true`, and onion services are exposed for `bitcoind`, `electrs`, `lnd`, and friends.
- **Firewall on, public sshd off, RDP off, auto-login off, fail2bain active**
- **Kernel surface trimmed.** `boot.blacklistedKernelModules = [ "rxrpc" ];`
- **Weekly garbage collection** with `--delete-older-than 7d`.
## Backups & Recovery
`services.rsnapshot` snapshots hourly and daily to `/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup`:
```
backup /home/ localhost/
backup /var/lib/ localhost/
backup /etc/nixos/ localhost/
backup /etc/nix-bitcoin-secrets/ localhost/
retain hourly 5
retain daily 5
cron hourly 0 * * * *
cron daily 50 21 * * *
```
The second drive is mounted by label (`BTCEcoandBackup`) with `nofail` so a missing drive doesn't block boot.
## License
Licensed under the **GNU Affero General Public License v3.0** — see [`LICENSE`](./LICENSE).
+277 -43
View File
@@ -204,7 +204,7 @@ FEATURE_REGISTRY = [
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"}, {"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
{"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"}, {"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"},
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"}, {"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"}, {"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"}, {"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"}, {"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"}, {"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
@@ -222,28 +222,16 @@ FEATURE_REGISTRY = [
"conflicts_with": [], "conflicts_with": [],
"port_requirements": [], "port_requirements": [],
}, },
{
"id": "bip110",
"name": "Bitcoin Knots + BIP110",
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Knots + BIP110 consensus changes. It will disable the currently active alternative.",
"category": "bitcoin",
"needs_domain": False,
"domain_name": None,
"needs_ddns": False,
"extra_fields": [],
"conflicts_with": ["bitcoin-core"],
"port_requirements": [],
},
{ {
"id": "bitcoin-core", "id": "bitcoin-core",
"name": "Bitcoin Core", "name": "Bitcoin Core",
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Core. It will disable the currently active alternative.", "description": "Only one Bitcoin node implementation can be active: Bitcoin Knots + BIP110 (default) or Bitcoin Core. Enabling this replaces Knots + BIP110 with Bitcoin Core. Your timechain data is preserved.",
"category": "bitcoin", "category": "bitcoin",
"needs_domain": False, "needs_domain": False,
"domain_name": None, "domain_name": None,
"needs_ddns": False, "needs_ddns": False,
"extra_fields": [], "extra_fields": [],
"conflicts_with": ["bip110"], "conflicts_with": [],
"port_requirements": [], "port_requirements": [],
}, },
{ {
@@ -277,13 +265,16 @@ FEATURE_REGISTRY = [
}, },
] ]
# Feature ids that have been removed/deprecated. The Hub must never write these
# back into custom.nix, and should strip any it finds (see startup migration).
DEPRECATED_FEATURE_IDS: set[str] = {"bip110"}
# Map feature IDs to their systemd units in config.json # Map feature IDs to their systemd units in config.json
FEATURE_SERVICE_MAP = { FEATURE_SERVICE_MAP = {
"rdp": "gnome-remote-desktop.service", "rdp": "gnome-remote-desktop.service",
"haven": "haven-relay.service", "haven": "haven-relay.service",
"element-calling": "livekit.service", "element-calling": "livekit.service",
"mempool": "mempool.service", "mempool": "mempool.service",
"bip110": None,
"bitcoin-core": None, "bitcoin-core": None,
"btcpay-web": "btcpayserver.service", "btcpay-web": "btcpayserver.service",
"sshd": "sshd.service", "sshd": "sshd.service",
@@ -295,7 +286,7 @@ _PORTS_MATRIX_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-7894", "protocol": "UDP", "description": "LiveKit media streams"}, {"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"}, {"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"}, {"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"}, {"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
@@ -331,7 +322,6 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
# For features that share a unit, disambiguate by icon field # For features that share a unit, disambiguate by icon field
FEATURE_ICON_MAP = { FEATURE_ICON_MAP = {
"bip110": "bip110",
"bitcoin-core": "bitcoin-core", "bitcoin-core": "bitcoin-core",
} }
@@ -352,7 +342,7 @@ ROLE_CATEGORIES: dict[str, set[str] | None] = {
ROLE_FEATURES: dict[str, set[str] | None] = { ROLE_FEATURES: dict[str, set[str] | None] = {
"server_plus_desktop": None, "server_plus_desktop": None,
"desktop": {"rdp", "sshd"}, "desktop": {"rdp", "sshd"},
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web", "sshd"}, "node": {"rdp", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
} }
SERVICE_DESCRIPTIONS: dict[str, str] = { SERVICE_DESCRIPTIONS: dict[str, str] = {
@@ -652,6 +642,37 @@ templates = Jinja2Templates(directory=os.path.join(_BASE_DIR, "templates"))
# ── Static asset cache-busting ──────────────────────────────────── # ── Static asset cache-busting ────────────────────────────────────
def _compute_asset_version() -> str:
"""Return a 16-char asset version from Nix store hash or static/template metadata."""
nix_match = re.search(r"/nix/store/([a-z0-9]{32})-", os.path.realpath(_BASE_DIR))
if nix_match:
return nix_match.group(1)[:16]
hasher = hashlib.sha256()
for root in (
os.path.join(_BASE_DIR, "static"),
os.path.join(_BASE_DIR, "templates"),
):
if not os.path.isdir(root):
continue
for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort()
for filename in sorted(filenames):
path = os.path.join(dirpath, filename)
try:
stat = os.stat(path)
except OSError:
continue
hasher.update(path.encode())
hasher.update(b"\0")
hasher.update(f"{stat.st_mtime_ns}:{stat.st_size}".encode())
hasher.update(b"\0")
return hasher.hexdigest()[:16]
ASSET_VERSION = _compute_asset_version()
def _file_hash(filename: str) -> str: def _file_hash(filename: str) -> str:
"""Return first 8 chars of the MD5 hex digest for a static file.""" """Return first 8 chars of the MD5 hex digest for a static file."""
path = os.path.join(_BASE_DIR, "static", filename) path = os.path.join(_BASE_DIR, "static", filename)
@@ -894,7 +915,7 @@ def _get_firewall_allowed_ports() -> dict[str, set[int]]:
def _port_range_to_ints(port_str: str) -> list[int]: def _port_range_to_ints(port_str: str) -> list[int]:
"""Convert a port string like ``"443"``, ``"7882-7894"`` to a list of ints.""" """Convert a port string like ``"443"``, ``"30000-40000"`` to a list of ints."""
port_str = port_str.strip() port_str = port_str.strip()
if re.match(r'^\d+$', port_str): if re.match(r'^\d+$', port_str):
return [int(port_str)] return [int(port_str)]
@@ -1488,7 +1509,9 @@ def _read_hub_overrides() -> tuple[dict, str | None, str | None, str | None]:
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;', r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
section, section,
): ):
features[m.group(1)] = m.group(2) == "true" feat_id = m.group(1)
if feat_id not in DEPRECATED_FEATURE_IDS:
features[feat_id] = m.group(2) == "true"
for m in re.finditer( for m in re.finditer(
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;', r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
section, section,
@@ -1521,6 +1544,8 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
"""Write the Hub Managed section inside custom.nix.""" """Write the Hub Managed section inside custom.nix."""
lines = [] lines = []
for feat_id, enabled in features.items(): for feat_id, enabled in features.items():
if feat_id in DEPRECATED_FEATURE_IDS:
continue
val = "true" if enabled else "false" val = "true" if enabled else "false"
if feat_id == "btcpay-web": if feat_id == "btcpay-web":
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};") lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
@@ -1566,6 +1591,40 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
f.write(content) f.write(content)
def _migrate_strip_deprecated_features() -> None:
"""One-time migration: remove deprecated feature lines from the Hub Managed
section of custom.nix. Any feature id in DEPRECATED_FEATURE_IDS is dropped
while all other Hub-managed settings (other features, nostr_npub, timezone,
locale) are preserved byte-for-byte in meaning.
This is a no-op (and never raises) if CUSTOM_NIX is missing, unreadable, or
contains no deprecated lines.
"""
try:
with open(CUSTOM_NIX, "r") as f:
content = f.read()
except (FileNotFoundError, OSError):
return
# Quick-exit: if none of the deprecated ids appear, nothing to do.
hub_begin = content.find(HUB_BEGIN)
hub_end = content.find(HUB_END)
if hub_begin == -1 or hub_end == -1:
return
section = content[hub_begin:hub_end]
if not any(f"features.{dep_id}" in section for dep_id in DEPRECATED_FEATURE_IDS):
return
try:
features, nostr_npub, timezone, locale = _read_hub_overrides()
# _read_hub_overrides already excludes DEPRECATED_FEATURE_IDS, so
# calling _write_hub_overrides with its output drops the stale lines.
_write_hub_overrides(features, nostr_npub, timezone, locale)
except Exception:
# Never let a migration failure break startup.
logger.exception("_migrate_strip_deprecated_features: unexpected error (non-fatal)")
# ── Feature status helpers ───────────────────────────────────────── # ── Feature status helpers ─────────────────────────────────────────
def _is_feature_enabled_in_config(feature_id: str) -> bool | None: def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
@@ -1575,7 +1634,7 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
return False # Default off in Node role; only on via explicit hub toggle return False # Default off in Node role; only on via explicit hub toggle
unit = FEATURE_SERVICE_MAP.get(feature_id) unit = FEATURE_SERVICE_MAP.get(feature_id)
if unit is None: if unit is None:
return None # bip110, bitcoin-core — can't determine from config return None # bitcoin-core — can't determine from config
cfg = load_config() cfg = load_config()
for svc in cfg.get("services", []): for svc in cfg.get("services", []):
if svc.get("unit") == unit: if svc.get("unit") == unit:
@@ -1894,7 +1953,10 @@ 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", {"request": request}) return templates.TemplateResponse("login.html", {
"request": request,
"asset_version": ASSET_VERSION,
})
@app.get("/auto-login") @app.get("/auto-login")
@@ -1961,6 +2023,7 @@ async def api_logout(request: Request):
async def index(request: Request): async def index(request: Request):
return templates.TemplateResponse("index.html", { return templates.TemplateResponse("index.html", {
"request": request, "request": request,
"asset_version": ASSET_VERSION,
}) })
@@ -1969,6 +2032,7 @@ async def onboarding(request: Request):
_ensure_onboarding_reopened_for_migration() _ensure_onboarding_reopened_for_migration()
return templates.TemplateResponse("onboarding.html", { return templates.TemplateResponse("onboarding.html", {
"request": request, "request": request,
"asset_version": ASSET_VERSION,
"onboarding_js_hash": _ONBOARDING_JS_HASH, "onboarding_js_hash": _ONBOARDING_JS_HASH,
}) })
@@ -2189,6 +2253,16 @@ _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
# Cache for ``bitcoind --version`` output (available even before RPC is ready) # Cache for ``bitcoind --version`` output (available even before RPC is ready)
_btcd_version_cache: tuple[float, str | None] = (0.0, None) _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) ───────── # ── Generic service version detection (NixOS store path) ─────────
@@ -2303,12 +2377,160 @@ def _get_bitcoin_version_info() -> dict | None:
return 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: def _get_bitcoind_version() -> str | None:
"""Run ``bitcoind --version`` and return the raw version string, or None on error. """Run ``bitcoind --version`` and return the raw version string, or None on error.
Parses the first output line to extract the token after "version ". Parses the first output line to extract the token after "version ".
For example: "Bitcoin Knots daemon version v29.3.knots20260210+bip110-v0.4.1" For example: "Bitcoin Knots daemon version v29.3.knots20260508"
returns "v29.3.knots20260210+bip110-v0.4.1". returns "v29.3.knots20260508".
Works regardless of whether the RPC server is ready (IBD, warmup, etc.). Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL). Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
@@ -2343,25 +2565,12 @@ def _get_bitcoind_version() -> str | None:
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str: def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
"""Format a raw version string from ``bitcoind --version`` for tile display. """Format a raw version string from ``bitcoind --version`` for tile display.
Strips the ``+bip110-vX.Y.Z`` patch suffix so the base version is shown For the BIP110 tile (icon == "bip110") a " (bip110)" tag is appended,
cleanly (e.g. "v29.3.knots20260210+bip110-v0.4.1""v29.3.knots20260210"). since mainline Bitcoin Knots (29.3.knots20260508+) now includes BIP-110
For the BIP110 tile (icon == "bip110") a " (bip110 vX.Y.Z)" tag is appended and no longer carries a separate ``+bip110-vX.Y.Z`` suffix.
including the patch version.
""" """
# Extract the BIP110 patch version before stripping the suffix display = raw_version
bip110_ver = "" if icon == "bip110" and "(bip110)" not in display.lower():
bip_match = re.search(r"\+bip110-v(\S+)", raw_version)
if bip_match:
bip110_ver = bip_match.group(1)
# Strip the +bip110... suffix for the base Knots version
display = re.sub(r"\+bip110\S*", "", raw_version)
# For BIP110 tile, append both the tag and the patch version
if icon == "bip110":
if bip110_ver:
display += f" (bip110 v{bip110_ver})"
elif "(bip110)" not in display.lower():
display += " (bip110)" display += " (bip110)"
return display return display
@@ -2430,6 +2639,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") @app.get("/api/services")
async def api_services(): async def api_services():
cfg = load_config() cfg = load_config()
@@ -2610,6 +2832,8 @@ async def api_services():
btc_ver = _format_bitcoin_version(raw_ver, icon=icon) btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_data["bitcoin_version"] = btc_ver # backwards compat service_data["bitcoin_version"] = btc_ver # backwards compat
service_data["version"] = btc_ver service_data["version"] = btc_ver
if icon == "bip110":
service_data["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_data return service_data
results = await asyncio.gather(*[get_status(s) for s in services]) results = await asyncio.gather(*[get_status(s) for s in services])
@@ -2894,6 +3118,8 @@ async def api_service_detail(unit: str, icon: str | None = None):
btc_ver = _format_bitcoin_version(raw_ver, icon=icon) btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_detail["bitcoin_version"] = btc_ver # backwards compat service_detail["bitcoin_version"] = btc_ver # backwards compat
service_detail["version"] = btc_ver service_detail["version"] = btc_ver
if icon == "bip110":
service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
return service_detail return service_detail
@@ -4561,6 +4787,14 @@ async def _startup_recover_stale_status():
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT) await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
@app.on_event("startup")
async def _startup_migrate_deprecated_features():
"""Strip deprecated feature lines (e.g. bip110) from the Hub Managed section
of custom.nix so they are never re-written and do not cause stale warnings."""
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, _migrate_strip_deprecated_features)
async def _background_domain_reachability_checker(): async def _background_domain_reachability_checker():
"""Periodically curl configured domains and cache reachability results.""" """Periodically curl configured domains and cache reachability results."""
await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY) await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY)
@@ -155,6 +155,69 @@
white-space: nowrap; 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 ───────────────────────────────── */ /* ── Service detail modal sections ───────────────────────────────── */
.svc-detail-section { .svc-detail-section {
@@ -413,16 +413,11 @@ function handleFeatureToggle(feat, newEnabled) {
}); });
} }
if (conflictNames.length > 0) { if (feat.id === "bitcoin-core") {
var confirmMsg; var confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will replace Bitcoin Knots + BIP110 as the active node. Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
if (feat.id === "bip110") {
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Knots + BIP110 will disable Bitcoin Core (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
} else if (feat.id === "bitcoin-core") {
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will disable Bitcoin Knots + BIP110 (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
} else {
confirmMsg = "This will disable " + conflictNames.join(", ") + ". Continue?";
}
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck); openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
} else if (conflictNames.length > 0) {
openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck);
} else { } else {
proceedAfterConflictCheck(); proceedAfterConflictCheck();
} }
@@ -60,3 +60,17 @@ async function apiFetch(path, options) {
} }
return res.json(); 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>' +
'</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) // Section C: Domain diagnostics (domain services)
if (data.needs_domain) { if (data.needs_domain) {
var steps = data.domain_check_steps || []; var steps = data.domain_check_steps || [];
@@ -242,7 +257,7 @@ async function openServiceDetailModal(unit, name, icon) {
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary"; var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
// Section title: use a more specific label for mutually-exclusive Bitcoin node features // Section title: use a more specific label for mutually-exclusive Bitcoin node features
var addonSectionTitle = (feat.id === "bip110" || feat.id === "bitcoin-core") var addonSectionTitle = (feat.id === "bitcoin-core")
? "\u20BF Bitcoin Node Selection" ? "\u20BF Bitcoin Node Selection"
: "\uD83D\uDD27 Addon Feature"; : "\uD83D\uDD27 Addon Feature";
+34 -1
View File
@@ -4,6 +4,21 @@
// Keyed by tileId: { progress: float, timestamp: ms } // Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {}; 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 ───────────────────────────────────────── // ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) { function buildTiles(services, categoryLabels) {
@@ -165,7 +180,8 @@ function buildTile(svc) {
var ver = svc.version || svc.bitcoin_version || ''; var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : ''; 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.style.cursor = "pointer";
tile.addEventListener("click", function() { 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);
}
}
}
} }
} }
} }
@@ -557,7 +557,7 @@ async function loadStep4() {
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;to</th><th>Purpose</th></tr></thead>'; html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;to</th><th>Purpose</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">78827894</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media streams</td></tr>'; html += '<tr><td class="port-req-port">7882</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media (UDP mux)</td></tr>';
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>'; html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>'; html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
html += '<tr><td class="port-req-port">3000040000</td><td class="port-req-proto">TCP/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/UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
+22 -22
View File
@@ -4,17 +4,17 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran_SystemsOS Hub</title> <title>Sovran_SystemsOS Hub</title>
<link rel="stylesheet" href="/static/css/base.css" /> <link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/buttons.css" /> <link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/header.css" /> <link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/layout.css" /> <link rel="stylesheet" href="/static/css/layout.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/tiles.css" /> <link rel="stylesheet" href="/static/css/tiles.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/modals.css" /> <link rel="stylesheet" href="/static/css/modals.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/features.css" /> <link rel="stylesheet" href="/static/css/features.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/onboarding.css" /> <link rel="stylesheet" href="/static/css/onboarding.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/support.css" /> <link rel="stylesheet" href="/static/css/support.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/domain-setup.css" /> <link rel="stylesheet" href="/static/css/domain-setup.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/security.css" /> <link rel="stylesheet" href="/static/css/security.css?v={{ asset_version }}" />
</head> </head>
<body> <body>
@@ -263,16 +263,16 @@
</div> </div>
</div> </div>
<script src="/static/js/constants.js"></script> <script src="/static/js/constants.js?v={{ asset_version }}"></script>
<script src="/static/js/state.js"></script> <script src="/static/js/state.js?v={{ asset_version }}"></script>
<script src="/static/js/helpers.js"></script> <script src="/static/js/helpers.js?v={{ asset_version }}"></script>
<script src="/static/js/tiles.js"></script> <script src="/static/js/tiles.js?v={{ asset_version }}"></script>
<script src="/static/js/service-detail.js"></script> <script src="/static/js/service-detail.js?v={{ asset_version }}"></script>
<script src="/static/js/support.js"></script> <script src="/static/js/support.js?v={{ asset_version }}"></script>
<script src="/static/js/update.js"></script> <script src="/static/js/update.js?v={{ asset_version }}"></script>
<script src="/static/js/rebuild.js"></script> <script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
<script src="/static/js/features.js"></script> <script src="/static/js/features.js?v={{ asset_version }}"></script>
<script src="/static/js/security.js"></script> <script src="/static/js/security.js?v={{ asset_version }}"></script>
<script src="/static/js/events.js"></script> <script src="/static/js/events.js?v={{ asset_version }}"></script>
</body> </body>
</html> </html>
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran Hub — Login</title> <title>Sovran Hub — Login</title>
<link rel="stylesheet" href="/static/css/base.css" /> <link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/buttons.css" /> <link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
</head> </head>
<body> <body>
<div class="login-wrapper"> <div class="login-wrapper">
+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

+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/>
<stop offset="55%" stop-color="#0F241B"/>
<stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/>
<stop offset="45%" stop-color="#28D978"/>
<stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/>
<stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 .18 0"/>
</filter>
</defs>
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
fill="none" stroke="rgba(255,255,255,0.08)"/>
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
fill="none" filter="url(#innerShade)"/>
<path d="M128 32 A96 96 0 1 1 58 196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+9 -3
View File
@@ -26,6 +26,13 @@
nix.settings = { nix.settings = {
experimental-features = [ "nix-command" "flakes" ]; experimental-features = [ "nix-command" "flakes" ];
download-buffer-size = 524288000; download-buffer-size = 524288000;
# Network resilience for cache.nixos.org (Fastly) flakiness.
connect-timeout = 10; # fail-fast on dead TCP connects (default: 0 = unlimited)
stalled-download-timeout = 90; # default 300s; retry sooner on stalled transfers
download-attempts = 7; # default 5
http-connections = 25; # cap concurrency (helps MTU/middlebox paths)
fallback = true; # build locally if a substitute can't be fetched
}; };
# ── Networking ────────────────────────────────────────────── # ── Networking ──────────────────────────────────────────────
@@ -63,7 +70,6 @@
# ── Desktop ──────────────────────────────────────────────── # ── Desktop ────────────────────────────────────────────────
services.displayManager.gdm.enable = true; services.displayManager.gdm.enable = true;
services.displayManager.gdm.autoSuspend = false; services.displayManager.gdm.autoSuspend = false;
services.displayManager.gdm.wayland = true;
services.desktopManager.gnome.enable = true; services.desktopManager.gnome.enable = true;
services.printing.enable = true; services.printing.enable = true;
systemd.enableEmergencyMode = false; systemd.enableEmergencyMode = false;
@@ -139,10 +145,10 @@
ranger fastfetch gedit openssl pwgen ranger fastfetch gedit openssl pwgen
aspell aspellDicts.en lm_sensors aspell aspellDicts.en lm_sensors
hunspell hunspellDicts.en_US hunspell hunspellDicts.en_US
synadm brave dua bitwarden-desktop synadm brave dua
gparted pv unzip parted screen zenity gparted pv unzip parted screen zenity
libargon2 gnome-terminal libreoffice-fresh 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 lk-jwt-service livekit-libwebrtc livekit-cli livekit
matrix-synapse age matrix-synapse age
]; ];
Generated
+29 -63
View File
@@ -1,33 +1,15 @@
{ {
"nodes": { "nodes": {
"bip110": { "btc-clients": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1778967282, "lastModified": 1781013869,
"narHash": "sha256-0g9RvVCD6zxY2vy54GhbB1OeeEZdKuxTr9r0whcpRjQ=", "narHash": "sha256-XlEUtL+8M6kbPdmIh4sQQ7G02/1CwHQEk1RPvIMEWOs=",
"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": 1779373552,
"narHash": "sha256-9FCY5+WZmoZi4zN0xbdq3lNcPudvYjONpv21/9ddV/0=",
"owner": "emmanuelrosa", "owner": "emmanuelrosa",
"repo": "btc-clients-nix", "repo": "btc-clients-nix",
"rev": "94797b5b75bbc021b0ceb83473110bcb58683542", "rev": "9a6c78204dc8961840375b110bca595b1f6f084c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -105,7 +87,7 @@
"inputs": { "inputs": {
"extra-container": "extra-container", "extra-container": "extra-container",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3", "nixpkgs": "nixpkgs_2",
"nixpkgs-25_05": "nixpkgs-25_05", "nixpkgs-25_05": "nixpkgs-25_05",
"nixpkgs-unstable": "nixpkgs-unstable" "nixpkgs-unstable": "nixpkgs-unstable"
}, },
@@ -126,16 +108,15 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1777728799, "lastModified": 1780218263,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=", "narHash": "sha256-T/f0pPDrH3Qc1VXyQXbK7yfHWRn90l3xwplc/nsxin4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5", "rev": "7fc393d1b46fa000d48ff14e8b6a3c9985f03af0",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "master",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -158,16 +139,16 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1751274312, "lastModified": 1780902259,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", "narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", "rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixos-24.11", "ref": "nixos-26.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -189,21 +170,6 @@
} }
}, },
"nixpkgs_2": { "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": { "locked": {
"lastModified": 1778737229, "lastModified": 1778737229,
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=", "narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
@@ -219,13 +185,13 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_4": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1778869304, "lastModified": 1780749050,
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -235,13 +201,13 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_5": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1779259093, "lastModified": 1780336545,
"narHash": "sha256-7DKWmH23hL2eYdkxCKeqj2i+yljTKuU+3Nk1UPHOnxc=", "narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d99b013d5d1931ad77fe3912ed218170dec5d9a4", "rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -254,15 +220,15 @@
"nixvim": { "nixvim": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_5", "nixpkgs": "nixpkgs_4",
"systems": "systems_2" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1779399125, "lastModified": 1780995253,
"narHash": "sha256-MWvSXTl1xUJsf74f1wD9mCWWsYN4uR1iAJNmUEnknqw=", "narHash": "sha256-6Lsoyw2XPvY8YNMCtPnsyw0JVVtHsXP2xtrFJBBTAOQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixvim", "repo": "nixvim",
"rev": "7f6f92a3c9f5e1d61d417bf761b4934ac5acc401", "rev": "43a7e6f82978ac975c3bba6728869b231e7a1ba0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -273,10 +239,9 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"bip110": "bip110",
"btc-clients": "btc-clients", "btc-clients": "btc-clients",
"nix-bitcoin": "nix-bitcoin", "nix-bitcoin": "nix-bitcoin",
"nixpkgs": "nixpkgs_4", "nixpkgs": "nixpkgs_3",
"nixpkgs-stable": "nixpkgs-stable", "nixpkgs-stable": "nixpkgs-stable",
"nixvim": "nixvim" "nixvim": "nixvim"
} }
@@ -298,15 +263,16 @@
}, },
"systems_2": { "systems_2": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1774449309,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"owner": "nix-systems", "owner": "nix-systems",
"repo": "default", "repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-systems", "owner": "nix-systems",
"ref": "future-26.11",
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
+2 -4
View File
@@ -6,11 +6,10 @@
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release"; nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
nixvim.url = "github:nix-community/nixvim"; nixvim.url = "github:nix-community/nixvim";
btc-clients.url = "github:emmanuelrosa/btc-clients-nix"; 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";
bip110.url = "github:emmanuelrosa/bitcoin-knots-bip-110-nix";
}; };
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, bip110, ... }: outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
let let
overlay-stable = final: prev: { overlay-stable = final: prev: {
@@ -56,7 +55,6 @@
btc-clients.packages.${pkgs.system}.bisq2 btc-clients.packages.${pkgs.system}.bisq2
btc-clients.packages.${pkgs.system}.sparrow btc-clients.packages.${pkgs.system}.sparrow
]; ];
sovran_systemsOS.packages.bip110 = bip110.packages.${pkgs.system}.bitcoind-knots-bip-110;
}; };
}; };
}; };
+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/>
<stop offset="55%" stop-color="#0F241B"/>
<stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/>
<stop offset="45%" stop-color="#28D978"/>
<stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/>
<stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 .18 0"/>
</filter>
</defs>
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
fill="none" stroke="rgba(255,255,255,0.08)"/>
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
fill="none" filter="url(#innerShade)"/>
<path d="M128 32 A96 96 0 1 1 58 196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

-23
View File
@@ -1,23 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sovran_systemsOS;
in
{
options.sovran_systemsOS.packages.bip110 = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
description = "BIP110 Bitcoin package";
};
config = lib.mkIf (
cfg.features.bip110 &&
cfg.packages.bip110 != null
) {
services.bitcoind.package = lib.mkForce cfg.packages.bip110;
environment.systemPackages = [
cfg.packages.bip110
];
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
services.bitcoind = { services.bitcoind = {
enable = true; enable = true;
package = config.nix-bitcoin.pkgs.bitcoind-knots; package = pkgs.bitcoind-knots;
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"; dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
txindex = true; txindex = true;
tor.proxy = true; tor.proxy = true;
+1 -2
View File
@@ -24,7 +24,7 @@
}) })
# ── Bitcoin Node Only Role ──────────────────────────────── # ── Bitcoin Node Only Role ────────────────────────────────
# Bitcoin ecosystem + mempool + bip110, BTCPay runs but not exposed via Caddy # Bitcoin ecosystem + mempool, BTCPay runs but not exposed via Caddy
(lib.mkIf config.sovran_systemsOS.roles.node { (lib.mkIf config.sovran_systemsOS.roles.node {
sovran_systemsOS.services = { sovran_systemsOS.services = {
bitcoin = lib.mkDefault true; bitcoin = lib.mkDefault true;
@@ -36,7 +36,6 @@
sovran_systemsOS.features = { sovran_systemsOS.features = {
mempool = lib.mkDefault true; mempool = lib.mkDefault true;
bip110 = lib.mkDefault true;
}; };
sovran_systemsOS.web.btcpayserver = lib.mkDefault false; sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
+24 -1
View File
@@ -43,12 +43,24 @@
# ── Features (default OFF — user can enable in custom.nix) ── # ── Features (default OFF — user can enable in custom.nix) ──
features = { features = {
haven = lib.mkEnableOption "Haven NOSTR relay"; haven = lib.mkEnableOption "Haven NOSTR relay";
bip110 = lib.mkEnableOption "BIP-110 Bitcoin Better Money";
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer"; mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
element-calling = lib.mkEnableOption "Element Video and Audio Calling"; element-calling = lib.mkEnableOption "Element Video and Audio Calling";
bitcoin-core = lib.mkEnableOption "Bitcoin Core"; bitcoin-core = lib.mkEnableOption "Bitcoin Core";
rdp = lib.mkEnableOption "Gnome Remote Desktop"; rdp = lib.mkEnableOption "Gnome Remote Desktop";
sshd = lib.mkEnableOption "SSH remote access"; sshd = lib.mkEnableOption "SSH remote access";
# Deprecated: BIP-110 is now built into mainline Bitcoin Knots and is the
# default node. This option is retained ONLY so that existing machines with
# `sovran_systemsOS.features.bip110 = lib.mkForce true;` left in their local
# custom.nix continue to evaluate. It has no effect and will be removed in a
# future release once the Hub has cleaned up old custom.nix files.
bip110 = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
internal = true;
visible = false;
description = "(Deprecated, no-op) BIP-110 is now built into Bitcoin Knots.";
};
}; };
# ── Web exposure (controls Caddy vhosts) ────────────────── # ── Web exposure (controls Caddy vhosts) ──────────────────
@@ -89,4 +101,15 @@
description = "Nostr public key (npub1...) for Haven relay"; description = "Nostr public key (npub1...) for Haven relay";
}; };
}; };
config = lib.mkIf (config.sovran_systemsOS.features.bip110 != null) {
warnings = [
''
sovran_systemsOS.features.bip110 is deprecated and has no effect:
BIP-110 is now built into mainline Bitcoin Knots, which is the default node.
You can safely remove the `sovran_systemsOS.features.bip110` line from
/etc/nixos/custom.nix. The Sovran Hub will also remove it automatically.
''
];
};
} }
+26 -9
View File
@@ -29,10 +29,7 @@ let
] ]
# ── Bitcoin Base (node implementations) ──────────────────── # ── Bitcoin Base (node implementations) ────────────────────
++ lib.optionals cfg.services.bitcoin [ ++ lib.optionals cfg.services.bitcoin [
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.features.bip110; category = "bitcoin-base"; credentials = [ { name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
{ name = "Bitcoin Knots"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core && !cfg.features.bip110; category = "bitcoin-base"; credentials = [
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; } { label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; } ]; }
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [ { name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
@@ -138,7 +135,11 @@ let
RC=0 RC=0
echo " Step 1/3: nix flake update " echo " Step 1/3: nix flake update "
if ! nix flake update --flake /etc/nixos --print-build-logs 2>&1; then if ! nix flake update --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 "[ERROR] nix flake update failed" echo "[ERROR] nix flake update failed"
RC=1 RC=1
fi fi
@@ -146,7 +147,11 @@ let
if [ "$RC" -eq 0 ]; then if [ "$RC" -eq 0 ]; then
echo " Step 2/3: nixos-rebuild " echo " Step 2/3: nixos-rebuild "
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1) SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1)
SWITCH_RC=$? SWITCH_RC=$?
echo "$SWITCH_OUT" echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then if [ "$SWITCH_RC" -eq 0 ]; then
@@ -155,7 +160,11 @@ let
echo "" echo ""
echo " Build succeeded a reboot is required to apply this update" echo " Build succeeded a reboot is required to apply this update"
echo " (Critical system components changed; running nixos-rebuild boot instead)" echo " (Critical system components changed; running nixos-rebuild boot instead)"
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then 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" echo "REBOOT_REQUIRED" > "$STATUS"
exit 0 exit 0
else else
@@ -209,7 +218,11 @@ let
echo "" echo ""
echo "" echo ""
echo " Rebuilding system configuration " echo " Rebuilding system configuration "
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1) SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \
--option stalled-download-timeout 90 \
--option download-attempts 7 \
--option fallback true 2>&1)
SWITCH_RC=$? SWITCH_RC=$?
echo "$SWITCH_OUT" echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then if [ "$SWITCH_RC" -eq 0 ]; then
@@ -222,7 +235,11 @@ let
echo "" echo ""
echo " Build succeeded a reboot is required to apply this rebuild" echo " Build succeeded a reboot is required to apply this rebuild"
echo " (Critical system components changed; running nixos-rebuild boot instead)" echo " (Critical system components changed; running nixos-rebuild boot instead)"
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then 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" echo "REBOOT_REQUIRED" > "$STATUS"
else else
echo "[ERROR] nixos-rebuild boot also failed" echo "[ERROR] nixos-rebuild boot also failed"
+2 -3
View File
@@ -130,7 +130,7 @@ EOF
keyFile = livekitKeyFile; keyFile = livekitKeyFile;
settings = { settings = {
rtc.use_external_ip = true; rtc.use_external_ip = true;
rtc.udp_port = "7882-7894"; rtc.udp_port = 7882;
room.auto_create = false; room.auto_create = false;
turn = { turn = {
enabled = true; enabled = true;
@@ -141,9 +141,8 @@ EOF
}; };
networking.firewall.allowedTCPPorts = [ 5349 7881 ]; networking.firewall.allowedTCPPorts = [ 5349 7881 ];
networking.firewall.allowedUDPPorts = [ 3478 ]; networking.firewall.allowedUDPPorts = [ 3478 7882 ];
networking.firewall.allowedUDPPortRanges = [ networking.firewall.allowedUDPPortRanges = [
{ from = 7882; to = 7894; }
{ from = 30000; to = 40000; } { from = 30000; to = 40000; }
]; ];
networking.firewall.allowedTCPPortRanges = [ networking.firewall.allowedTCPPortRanges = [
-1
View File
@@ -31,7 +31,6 @@
# ── Features (default OFF — enable in custom.nix) ───────── # ── Features (default OFF — enable in custom.nix) ─────────
./haven.nix ./haven.nix
./bip110.nix
./element-calling.nix ./element-calling.nix
./mempool.nix ./mempool.nix
./bitcoin-core.nix ./bitcoin-core.nix